@open-mercato/checkout 0.6.7-develop.6605.1.761dd4c810 → 0.6.7-develop.6621.1.cdbe9dee3d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/checkout/api/pay/[slug]/route.js +4 -4
- package/dist/modules/checkout/api/pay/[slug]/route.js.map +2 -2
- package/dist/modules/checkout/api/pay/[slug]/status/[transactionId]/route.js +4 -4
- package/dist/modules/checkout/api/pay/[slug]/status/[transactionId]/route.js.map +2 -2
- package/dist/modules/checkout/api/pay/[slug]/submit/route.js +5 -5
- package/dist/modules/checkout/api/pay/[slug]/submit/route.js.map +2 -2
- package/dist/modules/checkout/api/pay/[slug]/verify-password/route.js +4 -4
- package/dist/modules/checkout/api/pay/[slug]/verify-password/route.js.map +2 -2
- package/dist/modules/checkout/components/PayPage.js +18 -2
- package/dist/modules/checkout/components/PayPage.js.map +2 -2
- package/dist/modules/checkout/lib/rateLimiter.js +6 -0
- package/dist/modules/checkout/lib/rateLimiter.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/checkout/api/pay/[slug]/route.ts +4 -4
- package/src/modules/checkout/api/pay/[slug]/status/[transactionId]/route.ts +4 -4
- package/src/modules/checkout/api/pay/[slug]/submit/__tests__/route.test.ts +2 -1
- package/src/modules/checkout/api/pay/[slug]/submit/route.ts +5 -5
- package/src/modules/checkout/api/pay/[slug]/verify-password/route.ts +4 -4
- package/src/modules/checkout/components/PayPage.tsx +18 -2
- package/src/modules/checkout/i18n/de.json +1 -0
- package/src/modules/checkout/i18n/en.json +1 -0
- package/src/modules/checkout/i18n/es.json +1 -0
- package/src/modules/checkout/i18n/pl.json +1 -0
- package/src/modules/checkout/lib/__tests__/rateLimiter.test.ts +47 -0
- package/src/modules/checkout/lib/rateLimiter.ts +11 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { readEndpointRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/config";
|
|
2
|
+
import { getClientIp, RATE_LIMIT_FALLBACK_KEY } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
3
|
+
function buildCheckoutRateLimitKey(req, rateLimiter, namespace) {
|
|
4
|
+
const clientKey = getClientIp(req, rateLimiter.trustProxyDepth) ?? RATE_LIMIT_FALLBACK_KEY;
|
|
5
|
+
return `${namespace}:${clientKey}`;
|
|
6
|
+
}
|
|
2
7
|
const checkoutPublicViewRateLimitConfig = readEndpointRateLimitConfig("CHECKOUT_PUBLIC_VIEW", {
|
|
3
8
|
points: 60,
|
|
4
9
|
duration: 60,
|
|
@@ -24,6 +29,7 @@ const checkoutPasswordRateLimitConfig = readEndpointRateLimitConfig("CHECKOUT_PA
|
|
|
24
29
|
keyPrefix: "checkout-password"
|
|
25
30
|
});
|
|
26
31
|
export {
|
|
32
|
+
buildCheckoutRateLimitKey,
|
|
27
33
|
checkoutPasswordRateLimitConfig,
|
|
28
34
|
checkoutPublicViewRateLimitConfig,
|
|
29
35
|
checkoutStatusRateLimitConfig,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/checkout/lib/rateLimiter.ts"],
|
|
4
|
-
"sourcesContent": ["import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\n\nexport const checkoutPublicViewRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_PUBLIC_VIEW', {\n points: 60,\n duration: 60,\n blockDuration: 60,\n keyPrefix: 'checkout-public-view',\n})\n\nexport const checkoutStatusRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_STATUS', {\n points: 120,\n duration: 60,\n blockDuration: 60,\n keyPrefix: 'checkout-status',\n})\n\nexport const checkoutSubmitRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_SUBMIT', {\n points: 10,\n duration: 60,\n blockDuration: 60,\n keyPrefix: 'checkout-submit',\n})\n\nexport const checkoutPasswordRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_PASSWORD', {\n points: 5,\n duration: 60,\n blockDuration: 120,\n keyPrefix: 'checkout-password',\n})\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,mCAAmC;
|
|
4
|
+
"sourcesContent": ["import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { getClientIp, RATE_LIMIT_FALLBACK_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\n\nexport function buildCheckoutRateLimitKey(\n req: Request,\n rateLimiter: RateLimiterService,\n namespace: string,\n): string {\n const clientKey = getClientIp(req, rateLimiter.trustProxyDepth) ?? RATE_LIMIT_FALLBACK_KEY\n return `${namespace}:${clientKey}`\n}\n\nexport const checkoutPublicViewRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_PUBLIC_VIEW', {\n points: 60,\n duration: 60,\n blockDuration: 60,\n keyPrefix: 'checkout-public-view',\n})\n\nexport const checkoutStatusRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_STATUS', {\n points: 120,\n duration: 60,\n blockDuration: 60,\n keyPrefix: 'checkout-status',\n})\n\nexport const checkoutSubmitRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_SUBMIT', {\n points: 10,\n duration: 60,\n blockDuration: 60,\n keyPrefix: 'checkout-submit',\n})\n\nexport const checkoutPasswordRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_PASSWORD', {\n points: 5,\n duration: 60,\n blockDuration: 120,\n keyPrefix: 'checkout-password',\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,mCAAmC;AAC5C,SAAS,aAAa,+BAA+B;AAG9C,SAAS,0BACd,KACA,aACA,WACQ;AACR,QAAM,YAAY,YAAY,KAAK,YAAY,eAAe,KAAK;AACnE,SAAO,GAAG,SAAS,IAAI,SAAS;AAClC;AAEO,MAAM,oCAAoC,4BAA4B,wBAAwB;AAAA,EACnG,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe;AAAA,EACf,WAAW;AACb,CAAC;AAEM,MAAM,gCAAgC,4BAA4B,mBAAmB;AAAA,EAC1F,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe;AAAA,EACf,WAAW;AACb,CAAC;AAEM,MAAM,gCAAgC,4BAA4B,mBAAmB;AAAA,EAC1F,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe;AAAA,EACf,WAAW;AACb,CAAC;AAEM,MAAM,kCAAkC,4BAA4B,qBAAqB;AAAA,EAC9F,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe;AAAA,EACf,WAAW;AACb,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/checkout",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6621.1.cdbe9dee3d",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,18 +62,18 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@open-mercato/core": "0.6.7-develop.
|
|
66
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
65
|
+
"@open-mercato/core": "0.6.7-develop.6621.1.cdbe9dee3d",
|
|
66
|
+
"@open-mercato/ui": "0.6.7-develop.6621.1.cdbe9dee3d",
|
|
67
67
|
"bcryptjs": "^3.0.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
71
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
71
|
+
"@open-mercato/shared": "0.6.7-develop.6621.1.cdbe9dee3d",
|
|
72
72
|
"react": "^19.0.0",
|
|
73
73
|
"react-dom": "^19.0.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
76
|
+
"@open-mercato/shared": "0.6.7-develop.6621.1.cdbe9dee3d",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"@types/react": "^19.2.17",
|
|
79
79
|
"@types/react-dom": "^19.2.3",
|
|
@@ -3,11 +3,11 @@ import { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fiel
|
|
|
3
3
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
4
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
5
|
import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
|
|
6
|
-
import { checkRateLimit
|
|
6
|
+
import { checkRateLimit } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
7
7
|
import { CheckoutLink } from '../../../data/entities'
|
|
8
8
|
import { CHECKOUT_ENTITY_IDS } from '../../../lib/constants'
|
|
9
9
|
import { resolveCheckoutPublicCustomFields } from '../../../lib/customFields'
|
|
10
|
-
import { checkoutPublicViewRateLimitConfig } from '../../../lib/rateLimiter'
|
|
10
|
+
import { buildCheckoutRateLimitKey, checkoutPublicViewRateLimitConfig } from '../../../lib/rateLimiter'
|
|
11
11
|
import { handleCheckoutRouteError, readCheckoutAccessCookie, requirePreviewContext } from '../../helpers'
|
|
12
12
|
import {
|
|
13
13
|
isCheckoutLinkPublic,
|
|
@@ -32,8 +32,8 @@ export async function GET(req: Request, { params }: { params: Promise<{ slug: st
|
|
|
32
32
|
if (!previewRequested) {
|
|
33
33
|
try {
|
|
34
34
|
const rateLimiter = container.resolve('rateLimiterService') as RateLimiterService
|
|
35
|
-
const
|
|
36
|
-
const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutPublicViewRateLimitConfig,
|
|
35
|
+
const key = buildCheckoutRateLimitKey(req, rateLimiter, 'checkout-public-view')
|
|
36
|
+
const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutPublicViewRateLimitConfig, key, 'Too many checkout page requests. Please try again later.')
|
|
37
37
|
if (rateLimitResponse) return rateLimitResponse
|
|
38
38
|
} catch {
|
|
39
39
|
// Rate limiting is fail-open
|
|
@@ -2,10 +2,10 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
4
|
import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
|
|
5
|
-
import { checkRateLimit
|
|
5
|
+
import { checkRateLimit } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
6
6
|
import type { PaymentGatewayService } from '@open-mercato/core/modules/payment_gateways/lib/gateway-service'
|
|
7
7
|
import { CheckoutLink, CheckoutTransaction } from '../../../../../data/entities'
|
|
8
|
-
import { checkoutStatusRateLimitConfig } from '../../../../../lib/rateLimiter'
|
|
8
|
+
import { buildCheckoutRateLimitKey, checkoutStatusRateLimitConfig } from '../../../../../lib/rateLimiter'
|
|
9
9
|
import { handleCheckoutRouteError, requireCheckoutPasswordSession } from '../../../../helpers'
|
|
10
10
|
import { checkoutTag } from '../../../../openapi'
|
|
11
11
|
|
|
@@ -20,8 +20,8 @@ export async function GET(req: Request, { params }: { params: Promise<{ slug: st
|
|
|
20
20
|
const container = await createRequestContainer()
|
|
21
21
|
try {
|
|
22
22
|
const rateLimiter = container.resolve('rateLimiterService') as RateLimiterService
|
|
23
|
-
const
|
|
24
|
-
const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutStatusRateLimitConfig,
|
|
23
|
+
const key = buildCheckoutRateLimitKey(req, rateLimiter, 'checkout-status')
|
|
24
|
+
const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutStatusRateLimitConfig, key, 'Too many checkout status requests. Please try again later.')
|
|
25
25
|
if (rateLimitResponse) return rateLimitResponse
|
|
26
26
|
} catch {
|
|
27
27
|
// Rate limiting is fail-open
|
|
@@ -24,6 +24,7 @@ jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
|
|
|
24
24
|
jest.mock('@open-mercato/shared/lib/ratelimit/helpers', () => ({
|
|
25
25
|
checkRateLimit: jest.fn(),
|
|
26
26
|
getClientIp: jest.fn(),
|
|
27
|
+
RATE_LIMIT_FALLBACK_KEY: 'global',
|
|
27
28
|
}))
|
|
28
29
|
|
|
29
30
|
jest.mock('../../../../../events', () => ({
|
|
@@ -87,7 +88,7 @@ describe('POST /api/checkout/pay/[slug]/submit', () => {
|
|
|
87
88
|
;(getClientIp as jest.Mock).mockReturnValue('127.0.0.1')
|
|
88
89
|
;(createRequestContainer as jest.Mock).mockResolvedValue({
|
|
89
90
|
resolve: (name: string) => {
|
|
90
|
-
if (name === 'rateLimiterService') return {}
|
|
91
|
+
if (name === 'rateLimiterService') return { trustProxyDepth: 0 }
|
|
91
92
|
if (name === 'em') return { findOne: mockEmFindOne }
|
|
92
93
|
if (name === 'commandBus') return { execute: mockCommandExecute }
|
|
93
94
|
if (name === 'paymentGatewayService') {
|
|
@@ -6,7 +6,7 @@ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
|
6
6
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
7
7
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
8
8
|
import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
|
|
9
|
-
import { checkRateLimit
|
|
9
|
+
import { checkRateLimit } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
10
10
|
import type { PaymentGatewayClientSession } from '@open-mercato/shared/modules/payment_gateways/types'
|
|
11
11
|
import type { PaymentGatewayService } from '@open-mercato/core/modules/payment_gateways/lib/gateway-service'
|
|
12
12
|
import { GatewayTransaction } from '@open-mercato/core/modules/payment_gateways/data/entities'
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
validateDescriptorCurrencies,
|
|
23
23
|
} from '../../../../lib/utils'
|
|
24
24
|
import { validateCheckoutCustomerData } from '../../../../lib/customerDataValidation'
|
|
25
|
-
import { checkoutSubmitRateLimitConfig } from '../../../../lib/rateLimiter'
|
|
25
|
+
import { buildCheckoutRateLimitKey, checkoutSubmitRateLimitConfig } from '../../../../lib/rateLimiter'
|
|
26
26
|
import { checkoutTag } from '../../../openapi'
|
|
27
27
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
28
28
|
|
|
@@ -276,8 +276,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ slug: s
|
|
|
276
276
|
const container = await createRequestContainer()
|
|
277
277
|
try {
|
|
278
278
|
const rateLimiter = container.resolve('rateLimiterService') as RateLimiterService
|
|
279
|
-
const
|
|
280
|
-
const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutSubmitRateLimitConfig,
|
|
279
|
+
const key = buildCheckoutRateLimitKey(req, rateLimiter, 'checkout-submit')
|
|
280
|
+
const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutSubmitRateLimitConfig, key, 'Too many payment attempts. Please try again later.')
|
|
281
281
|
if (rateLimitResponse) return rateLimitResponse
|
|
282
282
|
} catch {
|
|
283
283
|
// Rate limiting is fail-open
|
|
@@ -525,7 +525,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ slug: s
|
|
|
525
525
|
providerKey: link.gatewayProviderKey,
|
|
526
526
|
err: error,
|
|
527
527
|
})
|
|
528
|
-
throw new CrudHttpError(502, { error: '
|
|
528
|
+
throw new CrudHttpError(502, { error: 'checkout.payPage.errors.sessionStart' })
|
|
529
529
|
}
|
|
530
530
|
const refreshedTransaction = await findOneWithDecryption(em, CheckoutTransaction, {
|
|
531
531
|
id: transaction.id,
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
4
|
import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
|
|
5
|
-
import { checkRateLimit
|
|
5
|
+
import { checkRateLimit } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
6
6
|
import { CheckoutLink } from '../../../../data/entities'
|
|
7
7
|
import { CHECKOUT_PASSWORD_COOKIE } from '../../../../lib/constants'
|
|
8
8
|
import { publicPasswordVerifySchema } from '../../../../data/validators'
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
signCheckoutAccessToken,
|
|
13
13
|
verifyCheckoutPassword,
|
|
14
14
|
} from '../../../../lib/utils'
|
|
15
|
-
import { checkoutPasswordRateLimitConfig } from '../../../../lib/rateLimiter'
|
|
15
|
+
import { buildCheckoutRateLimitKey, checkoutPasswordRateLimitConfig } from '../../../../lib/rateLimiter'
|
|
16
16
|
import { checkoutTag } from '../../../openapi'
|
|
17
17
|
|
|
18
18
|
export const metadata = {
|
|
@@ -25,8 +25,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ slug: s
|
|
|
25
25
|
const container = await createRequestContainer()
|
|
26
26
|
try {
|
|
27
27
|
const rateLimiter = container.resolve('rateLimiterService') as RateLimiterService
|
|
28
|
-
const
|
|
29
|
-
const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutPasswordRateLimitConfig,
|
|
28
|
+
const key = buildCheckoutRateLimitKey(req, rateLimiter, 'checkout-password')
|
|
29
|
+
const rateLimitResponse = await checkRateLimit(rateLimiter, checkoutPasswordRateLimitConfig, key, 'Too many password attempts. Please try again later.')
|
|
30
30
|
if (rateLimitResponse) return rateLimitResponse
|
|
31
31
|
} catch {
|
|
32
32
|
// Rate limiting is fail-open
|
|
@@ -803,6 +803,8 @@ export function PayPageCustomerForm({
|
|
|
803
803
|
const value = customerData[field.key]
|
|
804
804
|
const containerClass = field.kind === 'multiline' ? 'space-y-2 sm:col-span-2' : 'space-y-2'
|
|
805
805
|
const semanticType = getCheckoutCustomerFieldSemanticType(field)
|
|
806
|
+
const fieldId = `checkout-customer-${field.key}`
|
|
807
|
+
const fieldErrorId = `${fieldId}-error`
|
|
806
808
|
|
|
807
809
|
return (
|
|
808
810
|
<div key={field.key} className={containerClass}>
|
|
@@ -820,6 +822,9 @@ export function PayPageCustomerForm({
|
|
|
820
822
|
<Checkbox
|
|
821
823
|
checked={value === true}
|
|
822
824
|
disabled={inputsLocked}
|
|
825
|
+
aria-required={field.required ? true : undefined}
|
|
826
|
+
aria-invalid={fieldError ? true : undefined}
|
|
827
|
+
aria-describedby={fieldError ? fieldErrorId : undefined}
|
|
823
828
|
onCheckedChange={(checked) => onFieldChange(field.key, checked === true)}
|
|
824
829
|
/>
|
|
825
830
|
<span className="space-y-1">
|
|
@@ -836,17 +841,21 @@ export function PayPageCustomerForm({
|
|
|
836
841
|
</label>
|
|
837
842
|
) : (
|
|
838
843
|
<>
|
|
839
|
-
<label className="text-sm font-medium">
|
|
844
|
+
<label htmlFor={fieldId} className="text-sm font-medium">
|
|
840
845
|
{field.label}
|
|
841
846
|
{field.required ? ' *' : ''}
|
|
842
847
|
</label>
|
|
843
848
|
{field.kind === 'multiline' ? (
|
|
844
849
|
<Textarea
|
|
850
|
+
id={fieldId}
|
|
845
851
|
className={READABLE_INPUT_CLASSNAME}
|
|
846
852
|
value={typeof value === 'string' ? value : ''}
|
|
847
853
|
disabled={inputsLocked}
|
|
848
854
|
onChange={(event) => onFieldChange(field.key, event.target.value)}
|
|
849
855
|
placeholder={field.placeholder ?? undefined}
|
|
856
|
+
aria-required={field.required ? true : undefined}
|
|
857
|
+
aria-invalid={fieldError ? true : undefined}
|
|
858
|
+
aria-describedby={fieldError ? fieldErrorId : undefined}
|
|
850
859
|
style={buildReadableInputStyle(themeTokens, Boolean(fieldError), inputsLocked)}
|
|
851
860
|
/>
|
|
852
861
|
) : field.kind === 'select' || field.kind === 'radio' ? (
|
|
@@ -858,8 +867,11 @@ export function PayPageCustomerForm({
|
|
|
858
867
|
}}
|
|
859
868
|
>
|
|
860
869
|
<SelectTrigger
|
|
870
|
+
id={fieldId}
|
|
861
871
|
className={`rounded-xl ${READABLE_INPUT_CLASSNAME}`}
|
|
872
|
+
aria-required={field.required ? true : undefined}
|
|
862
873
|
aria-invalid={Boolean(fieldError)}
|
|
874
|
+
aria-describedby={fieldError ? fieldErrorId : undefined}
|
|
863
875
|
style={buildReadableInputStyle(themeTokens, Boolean(fieldError), inputsLocked)}
|
|
864
876
|
>
|
|
865
877
|
<SelectValue placeholder={t('checkout.payPage.fields.selectPlaceholder', 'Select...')} />
|
|
@@ -875,6 +887,7 @@ export function PayPageCustomerForm({
|
|
|
875
887
|
</Select>
|
|
876
888
|
) : (
|
|
877
889
|
<Input
|
|
890
|
+
id={fieldId}
|
|
878
891
|
className={READABLE_INPUT_CLASSNAME}
|
|
879
892
|
type={semanticType === 'email' ? 'email' : semanticType === 'phone' ? 'tel' : 'text'}
|
|
880
893
|
value={typeof value === 'string' ? value : ''}
|
|
@@ -882,13 +895,16 @@ export function PayPageCustomerForm({
|
|
|
882
895
|
onChange={(event) => onFieldChange(field.key, event.target.value)}
|
|
883
896
|
placeholder={field.placeholder ?? undefined}
|
|
884
897
|
autoComplete={semanticType === 'email' ? 'email' : semanticType === 'phone' ? 'tel' : undefined}
|
|
898
|
+
aria-required={field.required ? true : undefined}
|
|
899
|
+
aria-invalid={fieldError ? true : undefined}
|
|
900
|
+
aria-describedby={fieldError ? fieldErrorId : undefined}
|
|
885
901
|
style={buildReadableInputStyle(themeTokens, Boolean(fieldError), inputsLocked)}
|
|
886
902
|
/>
|
|
887
903
|
)}
|
|
888
904
|
</>
|
|
889
905
|
)}
|
|
890
906
|
{fieldError ? (
|
|
891
|
-
<p className="text-sm" style={buildValidationMessageStyle(themeTokens)}>
|
|
907
|
+
<p id={fieldErrorId} className="text-sm" style={buildValidationMessageStyle(themeTokens)}>
|
|
892
908
|
{translateValidationMessage(fieldError, fieldPath)}
|
|
893
909
|
</p>
|
|
894
910
|
) : null}
|
|
@@ -326,6 +326,7 @@
|
|
|
326
326
|
"checkout.payPage.errors.loadMessage": "Diese Zahlungsseite konnte nicht geladen werden. Bitte versuche es erneut.",
|
|
327
327
|
"checkout.payPage.errors.loadTitle": "Zahlungslink konnte nicht geladen werden",
|
|
328
328
|
"checkout.payPage.errors.password": "Das Passwort konnte nicht verifiziert werden. Bitte versuche es erneut.",
|
|
329
|
+
"checkout.payPage.errors.sessionStart": "Die Zahlungssitzung konnte nicht gestartet werden. Bitte versuche es erneut.",
|
|
329
330
|
"checkout.payPage.errors.submit": "Die Zahlung konnte nicht gestartet werden. Bitte versuche es erneut.",
|
|
330
331
|
"checkout.payPage.fields.booleanFalse": "Nein",
|
|
331
332
|
"checkout.payPage.fields.booleanTrue": "Ja",
|
|
@@ -326,6 +326,7 @@
|
|
|
326
326
|
"checkout.payPage.errors.loadMessage": "We couldn't load this payment page. Please try again.",
|
|
327
327
|
"checkout.payPage.errors.loadTitle": "Unable to load payment link",
|
|
328
328
|
"checkout.payPage.errors.password": "Unable to verify password. Please try again.",
|
|
329
|
+
"checkout.payPage.errors.sessionStart": "Unable to start the payment session. Please try again.",
|
|
329
330
|
"checkout.payPage.errors.submit": "Unable to start the payment. Please try again.",
|
|
330
331
|
"checkout.payPage.fields.booleanFalse": "No",
|
|
331
332
|
"checkout.payPage.fields.booleanTrue": "Yes",
|
|
@@ -326,6 +326,7 @@
|
|
|
326
326
|
"checkout.payPage.errors.loadMessage": "No pudimos cargar esta pagina de pago. Intentalo de nuevo.",
|
|
327
327
|
"checkout.payPage.errors.loadTitle": "No se pudo cargar el enlace de pago",
|
|
328
328
|
"checkout.payPage.errors.password": "No se pudo verificar la contrasena. Intentalo de nuevo.",
|
|
329
|
+
"checkout.payPage.errors.sessionStart": "No se pudo iniciar la sesión de pago. Inténtalo de nuevo.",
|
|
329
330
|
"checkout.payPage.errors.submit": "No se pudo iniciar el pago. Intentalo de nuevo.",
|
|
330
331
|
"checkout.payPage.fields.booleanFalse": "No",
|
|
331
332
|
"checkout.payPage.fields.booleanTrue": "Si",
|
|
@@ -326,6 +326,7 @@
|
|
|
326
326
|
"checkout.payPage.errors.loadMessage": "Nie udało się załadować tej strony płatności. Spróbuj ponownie.",
|
|
327
327
|
"checkout.payPage.errors.loadTitle": "Nie udało się załadować linku płatności",
|
|
328
328
|
"checkout.payPage.errors.password": "Nie udało się zweryfikować hasła. Spróbuj ponownie.",
|
|
329
|
+
"checkout.payPage.errors.sessionStart": "Nie udało się uruchomić sesji płatności. Spróbuj ponownie.",
|
|
329
330
|
"checkout.payPage.errors.submit": "Nie udało się rozpocząć płatności. Spróbuj ponownie.",
|
|
330
331
|
"checkout.payPage.fields.booleanFalse": "Nie",
|
|
331
332
|
"checkout.payPage.fields.booleanTrue": "Tak",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
|
|
2
|
+
import { buildCheckoutRateLimitKey } from '../rateLimiter'
|
|
3
|
+
|
|
4
|
+
function makeRateLimiter(trustProxyDepth: number): RateLimiterService {
|
|
5
|
+
return { trustProxyDepth } as RateLimiterService
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('buildCheckoutRateLimitKey', () => {
|
|
9
|
+
it('ignores spoofed forwarding headers in direct mode and uses the bounded fallback', () => {
|
|
10
|
+
const req = new Request('http://localhost', {
|
|
11
|
+
headers: {
|
|
12
|
+
'x-forwarded-for': 'attacker-controlled',
|
|
13
|
+
'x-real-ip': 'attacker-controlled',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
expect(buildCheckoutRateLimitKey(req, makeRateLimiter(0), 'checkout-submit'))
|
|
18
|
+
.toBe('checkout-submit:global')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('uses the trusted right edge with one proxy', () => {
|
|
22
|
+
const req = new Request('http://localhost', {
|
|
23
|
+
headers: { 'x-forwarded-for': 'spoofed, 203.0.113.10' },
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(buildCheckoutRateLimitKey(req, makeRateLimiter(1), 'checkout-password'))
|
|
27
|
+
.toBe('checkout-password:203.0.113.10')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('uses the configured trusted depth with multiple proxies', () => {
|
|
31
|
+
const req = new Request('http://localhost', {
|
|
32
|
+
headers: { 'x-forwarded-for': 'spoofed, 203.0.113.10, 192.0.2.5' },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
expect(buildCheckoutRateLimitKey(req, makeRateLimiter(2), 'checkout-status'))
|
|
36
|
+
.toBe('checkout-status:203.0.113.10')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('uses the bounded fallback when the forwarded chain is undersized', () => {
|
|
40
|
+
const req = new Request('http://localhost', {
|
|
41
|
+
headers: { 'x-forwarded-for': '203.0.113.10' },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(buildCheckoutRateLimitKey(req, makeRateLimiter(2), 'checkout-public-view'))
|
|
45
|
+
.toBe('checkout-public-view:global')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
|
|
2
|
+
import { getClientIp, RATE_LIMIT_FALLBACK_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
3
|
+
import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
|
|
4
|
+
|
|
5
|
+
export function buildCheckoutRateLimitKey(
|
|
6
|
+
req: Request,
|
|
7
|
+
rateLimiter: RateLimiterService,
|
|
8
|
+
namespace: string,
|
|
9
|
+
): string {
|
|
10
|
+
const clientKey = getClientIp(req, rateLimiter.trustProxyDepth) ?? RATE_LIMIT_FALLBACK_KEY
|
|
11
|
+
return `${namespace}:${clientKey}`
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
export const checkoutPublicViewRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_PUBLIC_VIEW', {
|
|
4
15
|
points: 60,
|