fix: resolve critical security and UI session mismatch by clearing React Query cache on login/logout and invalidating on subscription updates
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m24s

This commit is contained in:
2026-06-14 11:15:09 +02:00
parent 136d40c7d8
commit c7506e6aca
6 changed files with 28 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { Eye, EyeOff, Mail, Lock, ArrowRight, Loader2, Languages } from 'lucide-react'; import { Eye, EyeOff, Mail, Lock, ArrowRight, Loader2, Languages } from 'lucide-react';
@@ -31,6 +32,8 @@ export function LoginForm() {
const { clientId: googleClientId, enabled: googleEnabled } = useGoogleConfig(); const { clientId: googleClientId, enabled: googleEnabled } = useGoogleConfig();
const queryClient = useQueryClient();
useEffect(() => { useEffect(() => {
if (loginMutation.isError && loginMutation.error) { if (loginMutation.isError && loginMutation.error) {
notify({ notify({
@@ -39,7 +42,7 @@ export function LoginForm() {
variant: 'destructive', variant: 'destructive',
}); });
} }
}, [loginMutation.isError, loginMutation.error, notify]); }, [loginMutation.isError, loginMutation.error, notify, t]);
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -55,6 +58,7 @@ export function LoginForm() {
{ credential: credentialResponse.credential }, { credential: credentialResponse.credential },
); );
const { access_token, refresh_token } = response.data; const { access_token, refresh_token } = response.data;
queryClient.clear();
localStorage.setItem('token', access_token); localStorage.setItem('token', access_token);
localStorage.setItem('refresh_token', refresh_token); localStorage.setItem('refresh_token', refresh_token);
router.push(redirect); router.push(redirect);
@@ -67,7 +71,7 @@ export function LoginForm() {
} finally { } finally {
setGoogleLoading(false); setGoogleLoading(false);
} }
}, [redirect, router, notify, t]); }, [redirect, router, notify, t, queryClient]);
const handleGoogleError = useCallback(() => { const handleGoogleError = useCallback(() => {
notify({ notify({

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useMutation } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { apiClient, ApiClientError } from '@/lib/apiClient'; import { apiClient, ApiClientError } from '@/lib/apiClient';
import type { LoginRequest, LoginResponse } from './types'; import type { LoginRequest, LoginResponse } from './types';
@@ -9,6 +9,7 @@ export function useLogin() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirect = searchParams.get('redirect') || '/dashboard'; const redirect = searchParams.get('redirect') || '/dashboard';
const queryClient = useQueryClient();
return useMutation<LoginResponse, ApiClientError, LoginRequest>({ return useMutation<LoginResponse, ApiClientError, LoginRequest>({
mutationFn: async (credentials: LoginRequest) => { mutationFn: async (credentials: LoginRequest) => {
@@ -19,6 +20,7 @@ export function useLogin() {
return response.data; return response.data;
}, },
onSuccess: (data) => { onSuccess: (data) => {
queryClient.clear();
localStorage.setItem('token', data.access_token); localStorage.setItem('token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token); localStorage.setItem('refresh_token', data.refresh_token);
router.push(redirect); router.push(redirect);

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useMutation } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { apiClient } from '@/lib/apiClient'; import { apiClient } from '@/lib/apiClient';
import type { RegisterRequest, RegisterResponse } from './types'; import type { RegisterRequest, RegisterResponse } from './types';
@@ -15,6 +15,7 @@ export function useRegister() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirect = searchParams.get('redirect') || '/dashboard'; const redirect = searchParams.get('redirect') || '/dashboard';
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (data: RegisterRequest) => { mutationFn: async (data: RegisterRequest) => {
@@ -27,6 +28,7 @@ export function useRegister() {
return loginResponse.data; return loginResponse.data;
}, },
onSuccess: (data) => { onSuccess: (data) => {
queryClient.clear();
localStorage.setItem('token', data.access_token); localStorage.setItem('token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token); localStorage.setItem('refresh_token', data.refresh_token);
router.push(redirect); router.push(redirect);

View File

@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useSearchParams, useRouter } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { API_BASE } from '@/lib/config'; import { API_BASE } from '@/lib/config';
import { CheckCircle2, XCircle, RefreshCw } from 'lucide-react'; import { CheckCircle2, XCircle, RefreshCw } from 'lucide-react';
@@ -15,6 +16,7 @@ export default function CheckoutSuccessPage() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const sessionId = searchParams.get('session_id'); const sessionId = searchParams.get('session_id');
const queryClient = useQueryClient();
const [status, setStatus] = useState<'syncing' | 'ok' | 'error'>('syncing'); const [status, setStatus] = useState<'syncing' | 'ok' | 'error'>('syncing');
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
@@ -42,6 +44,7 @@ export default function CheckoutSuccessPage() {
if (res.ok) { if (res.ok) {
setStatus('ok'); setStatus('ok');
setMessage(data.data?.plan ? `Forfait ${data.data.plan} activé !` : 'Abonnement activé !'); setMessage(data.data?.plan ? `Forfait ${data.data.plan} activé !` : 'Abonnement activé !');
queryClient.invalidateQueries({ queryKey: ['user', 'me'] });
// Redirect after 2s // Redirect after 2s
setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 2000); setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 2000);
} else { } else {

View File

@@ -16,6 +16,7 @@ import { ThemeToggle } from '@/components/ui/theme-toggle';
import { languages } from '@/lib/api'; import { languages } from '@/lib/api';
import { useTranslationStore } from '@/lib/store'; import { useTranslationStore } from '@/lib/store';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useQueryClient } from '@tanstack/react-query';
/* ── helpers ──────────────────────────────────────────────────── */ /* ── helpers ──────────────────────────────────────────────────── */
const PLAN_ICONS: Record<string, React.ElementType> = { const PLAN_ICONS: Record<string, React.ElementType> = {
@@ -90,6 +91,8 @@ export default function ProfilePage() {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
const authHeaders = { Authorization: `Bearer ${token}` }; const authHeaders = { Authorization: `Bearer ${token}` };
const queryClient = useQueryClient();
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
if (!token) { router.push('/auth/login?redirect=/dashboard/profile'); return; } if (!token) { router.push('/auth/login?redirect=/dashboard/profile'); return; }
try { try {
@@ -97,10 +100,15 @@ export default function ProfilePage() {
fetch(`${API_BASE}/api/v1/auth/me`, { headers: authHeaders }), fetch(`${API_BASE}/api/v1/auth/me`, { headers: authHeaders }),
fetch(`${API_BASE}/api/v1/auth/usage`, { headers: authHeaders }), fetch(`${API_BASE}/api/v1/auth/usage`, { headers: authHeaders }),
]); ]);
if (meRes.ok) { const j = await meRes.json(); setUser(j.data ?? j); } if (meRes.ok) {
const j = await meRes.json();
const userData = j.data ?? j;
setUser(userData);
queryClient.setQueryData(['user', 'me'], userData);
}
if (usageRes.ok) { const j = await usageRes.json(); setUsage(j.data ?? j); } if (usageRes.ok) { const j = await usageRes.json(); setUsage(j.data ?? j); }
} catch { /* ignore */ } finally { setLoading(false); } } catch { /* ignore */ } finally { setLoading(false); }
}, [token]); // eslint-disable-line react-hooks/exhaustive-deps }, [token, queryClient]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { fetchData(); }, [fetchData]); useEffect(() => { fetchData(); }, [fetchData]);
useEffect(() => { setDefaultLanguage(settings.defaultTargetLanguage); }, [settings.defaultTargetLanguage]); useEffect(() => { setDefaultLanguage(settings.defaultTargetLanguage); }, [settings.defaultTargetLanguage]);

View File

@@ -1,14 +1,17 @@
'use client'; 'use client';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
export function useLogout() { export function useLogout() {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient();
const logout = () => { const logout = () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('refresh_token'); localStorage.removeItem('refresh_token');
localStorage.removeItem('user'); localStorage.removeItem('user');
queryClient.clear();
router.push('/'); router.push('/');
}; };