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
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m24s
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user