fix csrf mismatch
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { resolvePaddleLocale } from '../steps/PaymentStep';
|
import { resolveCheckoutCsrfToken, resolvePaddleLocale } from '../steps/PaymentStep';
|
||||||
|
|
||||||
describe('resolvePaddleLocale', () => {
|
describe('resolvePaddleLocale', () => {
|
||||||
it('returns short locale when given region-specific tag', () => {
|
it('returns short locale when given region-specific tag', () => {
|
||||||
@@ -18,3 +18,27 @@ describe('resolvePaddleLocale', () => {
|
|||||||
expect(resolvePaddleLocale('es')).toBe('es');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 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 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 {
|
export function resolvePaddleLocale(rawLocale?: string | null): string {
|
||||||
if (!rawLocale) {
|
if (!rawLocale) {
|
||||||
return 'en';
|
return 'en';
|
||||||
@@ -271,14 +310,9 @@ export const PaymentStep: React.FC = () => {
|
|||||||
setAwaitingConfirmation(false);
|
setAwaitingConfirmation(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
|
||||||
const response = await fetch('/checkout/free-activate', {
|
const response = await fetch('/checkout/free-activate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: buildCheckoutHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
'X-CSRF-TOKEN': csrfToken,
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
package_id: selectedPackage.id,
|
package_id: selectedPackage.id,
|
||||||
@@ -349,11 +383,8 @@ export const PaymentStep: React.FC = () => {
|
|||||||
|
|
||||||
const response = await fetch('/paddle/create-checkout', {
|
const response = await fetch('/paddle/create-checkout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: buildCheckoutHeaders(),
|
||||||
'Content-Type': 'application/json',
|
credentials: 'same-origin',
|
||||||
Accept: 'application/json',
|
|
||||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
package_id: selectedPackage.id,
|
package_id: selectedPackage.id,
|
||||||
locale: paddleLocale,
|
locale: paddleLocale,
|
||||||
|
|||||||
Reference in New Issue
Block a user