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:
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user