Share CSRF headers across guest uploads
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-30 13:10:19 +01:00
parent 96aaea23e4
commit 3ba784154b
4 changed files with 93 additions and 46 deletions

View 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');
});
});

View 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;
}