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)
This commit is contained in:
Agent Zero
2026-07-15 01:54:11 +02:00
parent a75665c1e6
commit 0d882eaca5
3 changed files with 29 additions and 2 deletions
+22
View File
@@ -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 onUnauthorized: (() => void) | null = null;
let onValidationError: ((errors: Record<string, string[]>) => void) | null = null; let onValidationError: ((errors: Record<string, string[]>) => void) | null = null;
@@ -28,6 +45,11 @@ export function setValidationErrorHandler(handler: (errors: Record<string, strin
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
// 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; return config;
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)
+6 -1
View File
@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 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'; import { useAuthStore } from '@/store/authStore';
export interface LoginPayload { export interface LoginPayload {
@@ -112,6 +112,10 @@ export function useLogin() {
onSuccess: (data: any) => { onSuccess: (data: any) => {
setUser(data.user || data); setUser(data.user || data);
setError(null); setError(null);
// Store CSRF token for subsequent unsafe requests
if (data.csrf_token) {
setCsrfToken(data.csrf_token);
}
}, },
onError: (error: any) => { onError: (error: any) => {
setError(error.message || 'Login failed'); setError(error.message || 'Login failed');
@@ -126,6 +130,7 @@ export function useLogout() {
mutationFn: () => apiPost('/auth/logout'), mutationFn: () => apiPost('/auth/logout'),
onSettled: () => { onSettled: () => {
logout(); logout();
setCsrfToken(null);
queryClient.clear(); queryClient.clear();
}, },
}); });
+1 -1
View File
@@ -401,7 +401,7 @@ export function deleteFolder(folderId: string): Promise<void> {
export function fetchMails(folderId: string, page: number): Promise<MailListResult> { export function fetchMails(folderId: string, page: number): Promise<MailListResult> {
const qs = new URLSearchParams({ folder_id: folderId, page: String(page) }).toString(); const qs = new URLSearchParams({ folder_id: folderId, page: String(page) }).toString();
return apiGet<MailListResult>(`/mail/?${qs}`); return apiGet<MailListResult>(`/mail?${qs}`);
} }
export function getMail(mailId: string): Promise<Mail> { export function getMail(mailId: string): Promise<Mail> {