fix csrf mismatch

This commit is contained in:
Codex Agent
2025-12-22 14:33:36 +01:00
parent f9016fb8ab
commit 83712b9a3a
2 changed files with 67 additions and 12 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { resolvePaddleLocale } from '../steps/PaymentStep';
import { resolveCheckoutCsrfToken, resolvePaddleLocale } from '../steps/PaymentStep';
describe('resolvePaddleLocale', () => {
it('returns short locale when given region-specific tag', () => {
@@ -18,3 +18,27 @@ describe('resolvePaddleLocale', () => {
expect(resolvePaddleLocale('es')).toBe('es');
});
});
describe('resolveCheckoutCsrfToken', () => {
beforeEach(() => {
document.head.innerHTML = '';
document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
});
it('prefers the meta csrf token when present', () => {
const meta = document.createElement('meta');
meta.setAttribute('name', 'csrf-token');
meta.setAttribute('content', 'meta-token');
document.head.appendChild(meta);
document.cookie = 'XSRF-TOKEN=cookie-token';
expect(resolveCheckoutCsrfToken()).toBe('meta-token');
});
it('falls back to the XSRF-TOKEN cookie when meta is missing', () => {
document.cookie = 'XSRF-TOKEN=cookie-token';
expect(resolveCheckoutCsrfToken()).toBe('cookie-token');
});
});

View File

@@ -36,6 +36,45 @@ const PADDLE_SCRIPT_URL = 'https://cdn.paddle.com/paddle/v2/paddle.js';
const PADDLE_SUPPORTED_LOCALES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pt', 'sv', 'da', 'fi', 'no'];
const PRIMARY_CTA_STYLES = 'min-w-[200px] disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted disabled:hover:text-muted-foreground';
const getCookieValue = (name: string): string | null => {
if (typeof document === 'undefined') {
return null;
}
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
};
export function resolveCheckoutCsrfToken(): string {
if (typeof document === 'undefined') {
return '';
}
const metaToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (metaToken && metaToken.length > 0) {
return metaToken;
}
return getCookieValue('XSRF-TOKEN') ?? '';
}
function buildCheckoutHeaders(): HeadersInit {
const csrfToken = resolveCheckoutCsrfToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
if (csrfToken) {
headers['X-CSRF-TOKEN'] = csrfToken;
headers['X-XSRF-TOKEN'] = csrfToken;
}
return headers;
}
export function resolvePaddleLocale(rawLocale?: string | null): string {
if (!rawLocale) {
return 'en';
@@ -271,14 +310,9 @@ export const PaymentStep: React.FC = () => {
setAwaitingConfirmation(false);
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
const response = await fetch('/checkout/free-activate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
headers: buildCheckoutHeaders(),
credentials: 'same-origin',
body: JSON.stringify({
package_id: selectedPackage.id,
@@ -349,11 +383,8 @@ export const PaymentStep: React.FC = () => {
const response = await fetch('/paddle/create-checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
},
headers: buildCheckoutHeaders(),
credentials: 'same-origin',
body: JSON.stringify({
package_id: selectedPackage.id,
locale: paddleLocale,