Share CSRF headers across guest uploads
This commit is contained in:
36
resources/js/guest/lib/__tests__/csrf.test.ts
Normal file
36
resources/js/guest/lib/__tests__/csrf.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
||||
import { buildCsrfHeaders } from '../csrf';
|
||||
|
||||
describe('buildCsrfHeaders', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('device-id', 'device-123');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
document.head.querySelectorAll('meta[name="csrf-token"]').forEach((node) => node.remove());
|
||||
document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||||
});
|
||||
|
||||
it('reads token from meta tag', () => {
|
||||
const meta = document.createElement('meta');
|
||||
meta.setAttribute('name', 'csrf-token');
|
||||
meta.setAttribute('content', 'meta-token');
|
||||
document.head.appendChild(meta);
|
||||
|
||||
const headers = buildCsrfHeaders('device-xyz');
|
||||
expect(headers['X-CSRF-TOKEN']).toBe('meta-token');
|
||||
expect(headers['X-XSRF-TOKEN']).toBe('meta-token');
|
||||
expect(headers['X-Device-Id']).toBe('device-xyz');
|
||||
});
|
||||
|
||||
it('falls back to cookie token', () => {
|
||||
const raw = btoa('cookie-token');
|
||||
document.cookie = `XSRF-TOKEN=${raw}; path=/`;
|
||||
|
||||
const headers = buildCsrfHeaders();
|
||||
expect(headers['X-CSRF-TOKEN']).toBe('cookie-token');
|
||||
expect(headers['X-XSRF-TOKEN']).toBe('cookie-token');
|
||||
expect(headers['X-Device-Id']).toBe('device-123');
|
||||
});
|
||||
});
|
||||
49
resources/js/guest/lib/csrf.ts
Normal file
49
resources/js/guest/lib/csrf.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { getDeviceId } from './device';
|
||||
|
||||
function getCsrfToken(): string | null {
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metaToken = document.querySelector('meta[name="csrf-token"]');
|
||||
if (metaToken instanceof HTMLMetaElement) {
|
||||
return metaToken.getAttribute('content') || null;
|
||||
}
|
||||
|
||||
const name = 'XSRF-TOKEN=';
|
||||
const decodedCookie = decodeURIComponent(document.cookie ?? '');
|
||||
const parts = decodedCookie.split(';');
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trimStart();
|
||||
if (!trimmed.startsWith(name)) {
|
||||
continue;
|
||||
}
|
||||
const token = trimmed.substring(name.length);
|
||||
try {
|
||||
return decodeURIComponent(atob(token));
|
||||
} catch {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildCsrfHeaders(deviceId?: string): Record<string, string> {
|
||||
const token = getCsrfToken();
|
||||
const resolvedDeviceId = deviceId ?? (typeof window !== 'undefined' ? getDeviceId() : undefined);
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
if (resolvedDeviceId) {
|
||||
headers['X-Device-Id'] = resolvedDeviceId;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['X-CSRF-TOKEN'] = token;
|
||||
headers['X-XSRF-TOKEN'] = token;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
Reference in New Issue
Block a user