From 0d882eaca5e0979f40eccb776e69241db105d0c8 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Wed, 15 Jul 2026 01:54:11 +0200 Subject: [PATCH] fix: mail plugin CSRF token handling and trailing slash - Add CSRF token storage in sessionStorage (persists across reloads) - Send X-CSRF-Token header on POST/PATCH/DELETE requests - Store CSRF token on login, clear on logout - Fix trailing slash in mail list API call (/mail/ -> /mail) --- frontend/src/api/client.ts | 22 ++++++++++++++++++++++ frontend/src/api/hooks.ts | 7 ++++++- frontend/src/api/mail.ts | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 479a577..4c5fde9 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -15,6 +15,23 @@ export const apiClient = axios.create({ }, }); +// CSRF token storage — persisted in sessionStorage, sent on all unsafe methods +const CSRF_KEY = 'leocrm_csrf_token'; +let csrfToken: string | null = sessionStorage.getItem(CSRF_KEY); + +export function setCsrfToken(token: string | null) { + csrfToken = token; + if (token) { + sessionStorage.setItem(CSRF_KEY, token); + } else { + sessionStorage.removeItem(CSRF_KEY); + } +} + +export function getCsrfToken(): string | null { + return csrfToken; +} + let onUnauthorized: (() => void) | null = null; let onValidationError: ((errors: Record) => void) | null = null; @@ -28,6 +45,11 @@ export function setValidationErrorHandler(handler: (errors: Record { + // Attach CSRF token on unsafe methods + const unsafe = ['post', 'put', 'patch', 'delete']; + if (unsafe.includes(config.method?.toLowerCase() ?? '') && csrfToken) { + config.headers['X-CSRF-Token'] = csrfToken; + } return config; }, (error) => Promise.reject(error) diff --git a/frontend/src/api/hooks.ts b/frontend/src/api/hooks.ts index 94f8191..d413515 100644 --- a/frontend/src/api/hooks.ts +++ b/frontend/src/api/hooks.ts @@ -1,5 +1,5 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { apiPost, apiGet, apiPatch, apiDelete, apiClient } from './client'; +import { apiPost, apiGet, apiPatch, apiDelete, apiClient, setCsrfToken } from './client'; import { useAuthStore } from '@/store/authStore'; export interface LoginPayload { @@ -112,6 +112,10 @@ export function useLogin() { onSuccess: (data: any) => { setUser(data.user || data); setError(null); + // Store CSRF token for subsequent unsafe requests + if (data.csrf_token) { + setCsrfToken(data.csrf_token); + } }, onError: (error: any) => { setError(error.message || 'Login failed'); @@ -126,6 +130,7 @@ export function useLogout() { mutationFn: () => apiPost('/auth/logout'), onSettled: () => { logout(); + setCsrfToken(null); queryClient.clear(); }, }); diff --git a/frontend/src/api/mail.ts b/frontend/src/api/mail.ts index b995743..164819c 100644 --- a/frontend/src/api/mail.ts +++ b/frontend/src/api/mail.ts @@ -401,7 +401,7 @@ export function deleteFolder(folderId: string): Promise { export function fetchMails(folderId: string, page: number): Promise { const qs = new URLSearchParams({ folder_id: folderId, page: String(page) }).toString(); - return apiGet(`/mail/?${qs}`); + return apiGet(`/mail?${qs}`); } export function getMail(mailId: string): Promise {