From 83712b9a3a508c46cbaa7324e4f24d1e28036eb3 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 22 Dec 2025 14:33:36 +0100 Subject: [PATCH] fix csrf mismatch --- .../__tests__/PaymentStep.locale.test.ts | 26 ++++++++- .../marketing/checkout/steps/PaymentStep.tsx | 53 +++++++++++++++---- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/resources/js/pages/marketing/checkout/__tests__/PaymentStep.locale.test.ts b/resources/js/pages/marketing/checkout/__tests__/PaymentStep.locale.test.ts index 6af7887..df48a69 100644 --- a/resources/js/pages/marketing/checkout/__tests__/PaymentStep.locale.test.ts +++ b/resources/js/pages/marketing/checkout/__tests__/PaymentStep.locale.test.ts @@ -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'); + }); +}); diff --git a/resources/js/pages/marketing/checkout/steps/PaymentStep.tsx b/resources/js/pages/marketing/checkout/steps/PaymentStep.tsx index 71753aa..e98b2d6 100644 --- a/resources/js/pages/marketing/checkout/steps/PaymentStep.tsx +++ b/resources/js/pages/marketing/checkout/steps/PaymentStep.tsx @@ -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,