This commit implements CSRF token support for all session-based API requests to fix the "CSRF token missing" and "CSRF token mismatch" errors introduced after CSRF protection was added in commit 62c4cc84. Changes: - Created csrfService.ts utility for fetching and caching CSRF tokens - Added getPostHeadersWithCsrf() helper to authUtils for async token injection - Updated all service files (*Service.ts) to include CSRF tokens in POST/PUT/PATCH/DELETE requests - Updated components with inline fetch calls to use getCsrfToken() - Fixed CSRF middleware to use single lusca instance instead of creating new instances per request - Improved generateToken() to use req.csrfToken() when available - Added CalDAV path exemption to CSRF protection Technical details: - CSRF tokens are fetched from /api/csrf-token endpoint - Tokens are cached and reused across requests to avoid unnecessary fetches - Tokens are included in x-csrf-token header for state-changing requests - Public endpoints (login, register) remain exempt from CSRF protection - Bearer token authentication remains exempt from CSRF protection Files modified: - Backend: app.js, middleware/csrf.js - Frontend: 13 service files, 8 component files - New file: frontend/utils/csrfService.ts This ensures all session-based requests properly include CSRF tokens while maintaining support for API token authentication.
65 lines
1.8 KiB
TypeScript
65 lines
1.8 KiB
TypeScript
import { getCsrfToken } from './csrfService';
|
|
|
|
export const getDefaultHeaders = (): Record<string, string> => {
|
|
return {
|
|
Accept: 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
Origin: window.location.origin,
|
|
};
|
|
};
|
|
|
|
export const getPostHeaders = (): Record<string, string> => {
|
|
return {
|
|
...getDefaultHeaders(),
|
|
'Content-Type': 'application/json',
|
|
};
|
|
};
|
|
|
|
export const getPostHeadersWithCsrf = async (): Promise<
|
|
Record<string, string>
|
|
> => {
|
|
const token = await getCsrfToken();
|
|
return {
|
|
...getPostHeaders(),
|
|
'x-csrf-token': token,
|
|
};
|
|
};
|
|
|
|
let isRedirecting = false;
|
|
|
|
export const handleAuthResponse = async (
|
|
response: Response,
|
|
errorMessage: string
|
|
): Promise<Response> => {
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
if (window.location.pathname !== '/login' && !isRedirecting) {
|
|
isRedirecting = true;
|
|
setTimeout(() => {
|
|
window.location.href = '/login';
|
|
}, 100);
|
|
}
|
|
throw new Error('Authentication required');
|
|
}
|
|
let details: string[] | undefined;
|
|
try {
|
|
const body = await response.json();
|
|
if (body.details && Array.isArray(body.details)) {
|
|
details = body.details;
|
|
}
|
|
if (body.error) {
|
|
errorMessage = body.error;
|
|
}
|
|
} catch {
|
|
// response body is not JSON, use fallback errorMessage
|
|
}
|
|
const error = new Error(errorMessage);
|
|
(error as any).details = details;
|
|
throw error;
|
|
}
|
|
return response;
|
|
};
|
|
|
|
export const isAuthError = (error: any): boolean => {
|
|
return error?.message && error.message.includes('Authentication required');
|
|
};
|