feat: Implement CSRF protection and PostgreSQL support

- Added CSRF protection mechanism in the backend with utility functions for token management.
- Introduced a new CSRF route to fetch the active CSRF token for SPA bootstrap flows.
- Updated the auth routes to validate CSRF tokens on sensitive operations.
- Configured PostgreSQL as a database option in the environment settings and Docker Compose.
- Created a new SQLite configuration file for local development.
- Enhanced the API client to automatically attach CSRF tokens to requests.
- Updated various frontend components to utilize the new site origin utility for SEO purposes.
- Modified Nginx configuration to improve redirection and SEO headers.
- Added tests for CSRF token handling in the authentication routes.
This commit is contained in:
Your Name
2026-03-17 23:26:32 +02:00
parent 3f24a7ea3e
commit a2824b2132
24 changed files with 332 additions and 319 deletions

View File

@@ -1,4 +1,60 @@
import axios from 'axios';
import axios, { type InternalAxiosRequestConfig } from 'axios';
const CSRF_COOKIE_NAME = 'csrf_token';
const CSRF_HEADER_NAME = 'X-CSRF-Token';
function getCookieValue(name: string): string {
if (typeof document === 'undefined') {
return '';
}
const encodedName = `${encodeURIComponent(name)}=`;
const cookie = document.cookie
.split('; ')
.find((item) => item.startsWith(encodedName));
return cookie ? decodeURIComponent(cookie.slice(encodedName.length)) : '';
}
function shouldAttachCsrfToken(config: InternalAxiosRequestConfig): boolean {
const method = String(config.method || 'get').toUpperCase();
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
return false;
}
const headers = config.headers ?? {};
if ('X-API-Key' in headers || 'x-api-key' in headers) {
return false;
}
return !String(config.url || '').includes('/auth/csrf');
}
function setRequestHeader(config: InternalAxiosRequestConfig, key: string, value: string) {
if (!config.headers) {
config.headers = {};
}
if (typeof (config.headers as { set?: (header: string, headerValue: string) => void }).set === 'function') {
(config.headers as { set: (header: string, headerValue: string) => void }).set(key, value);
return;
}
(config.headers as Record<string, string>)[key] = value;
}
const csrfBootstrapClient = axios.create({
baseURL: '/api',
timeout: 15000,
withCredentials: true,
headers: {
Accept: 'application/json',
},
});
const api = axios.create({
baseURL: '/api',
@@ -11,7 +67,23 @@ const api = axios.create({
// Request interceptor for logging
api.interceptors.request.use(
(config) => config,
async (config) => {
if (!shouldAttachCsrfToken(config)) {
return config;
}
let csrfToken = getCookieValue(CSRF_COOKIE_NAME);
if (!csrfToken) {
await csrfBootstrapClient.get('/auth/csrf');
csrfToken = getCookieValue(CSRF_COOKIE_NAME);
}
if (csrfToken) {
setRequestHeader(config, CSRF_HEADER_NAME, csrfToken);
}
return config;
},
(error) => Promise.reject(error)
);
@@ -318,6 +390,10 @@ export async function getTaskStatus(taskId: string): Promise<TaskStatus> {
return response.data;
}
export function getApiClient() {
return api;
}
/**
* Send one message to the site assistant.
*/