mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 16:45:25 +02:00
fix(auth): resolve concurrent session token refresh and state syncing issues to prevent frequent logouts
This commit is contained in:
+7
-3
@@ -182,13 +182,17 @@ export default function App() {
|
||||
|
||||
// Sync session state when localStorage is updated (e.g. by token refresh)
|
||||
useEffect(() => {
|
||||
const handleStorage = (e: StorageEvent) => {
|
||||
if (e.key === 'craze_auth_session') {
|
||||
const handleStorage = (e: Event) => {
|
||||
if (e.type === 'session-refreshed' || (e as StorageEvent).key === 'craze_auth_session') {
|
||||
setSession(getStoredSession());
|
||||
}
|
||||
};
|
||||
window.addEventListener('storage', handleStorage);
|
||||
return () => window.removeEventListener('storage', handleStorage);
|
||||
window.addEventListener('session-refreshed', handleStorage);
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorage);
|
||||
window.removeEventListener('session-refreshed', handleStorage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Users, Search, CheckCircle, XCircle, Trash2, Shield, Loader2, AlertCircle, Clock } from 'lucide-react';
|
||||
import { AuthSession } from '../lib/auth';
|
||||
import { safeFetch } from '../lib/supabase';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -27,11 +28,10 @@ export function UserManagementView({ session }: UserManagementViewProps) {
|
||||
setError(null);
|
||||
setWarning(null);
|
||||
try {
|
||||
const res = await fetch('/api/users-admin', {
|
||||
const res = await safeFetch('/api/users-admin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${session.access_token}`
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ action: 'list' })
|
||||
});
|
||||
@@ -61,11 +61,10 @@ export function UserManagementView({ session }: UserManagementViewProps) {
|
||||
setError(null);
|
||||
setWarning(null);
|
||||
try {
|
||||
const res = await fetch('/api/users-admin', {
|
||||
const res = await safeFetch('/api/users-admin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${session.access_token}`
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'validate',
|
||||
@@ -104,11 +103,10 @@ export function UserManagementView({ session }: UserManagementViewProps) {
|
||||
setError(null);
|
||||
setDeleteConfirmUser(null);
|
||||
try {
|
||||
const res = await fetch('/api/users-admin', {
|
||||
const res = await safeFetch('/api/users-admin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${session.access_token}`
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete',
|
||||
|
||||
@@ -72,10 +72,19 @@ export async function signIn(email: string, password: string): Promise<AuthSessi
|
||||
};
|
||||
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(session));
|
||||
window.dispatchEvent(new Event('session-refreshed'));
|
||||
return session;
|
||||
}
|
||||
|
||||
let activeRefreshPromise: Promise<AuthSession> | null = null;
|
||||
|
||||
export async function refreshSession(refreshToken: string): Promise<AuthSession> {
|
||||
if (activeRefreshPromise) {
|
||||
return activeRefreshPromise;
|
||||
}
|
||||
|
||||
activeRefreshPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -86,8 +95,13 @@ export async function refreshSession(refreshToken: string): Promise<AuthSession>
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 400 || response.status === 401 || response.status === 403) {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
window.dispatchEvent(new Event('session-refreshed'));
|
||||
throw new Error('Session expired. Please sign in again.');
|
||||
} else {
|
||||
throw new Error(`Server error (${response.status}). Please try again later.`);
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -98,11 +112,22 @@ export async function refreshSession(refreshToken: string): Promise<AuthSession>
|
||||
};
|
||||
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(session));
|
||||
window.dispatchEvent(new Event('session-refreshed'));
|
||||
return session;
|
||||
} catch (error) {
|
||||
// If the error was not already thrown as "Session expired", we just propagate it.
|
||||
throw error;
|
||||
} finally {
|
||||
activeRefreshPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return activeRefreshPromise;
|
||||
}
|
||||
|
||||
export function signOut(): void {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
window.dispatchEvent(new Event('session-refreshed'));
|
||||
}
|
||||
|
||||
export function getStoredSession(): AuthSession | null {
|
||||
|
||||
+22
-2
@@ -17,9 +17,27 @@ export async function safeFetch(url: string, options: RequestInit = {}): Promise
|
||||
|
||||
let response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401 && session?.refresh_token) {
|
||||
if (response.status === 401) {
|
||||
const latestSession = getStoredSession();
|
||||
if (latestSession) {
|
||||
// If the access token in localStorage is already different (newer) than the one we used,
|
||||
// try retrying the request with that new token first without doing a refresh.
|
||||
if (latestSession.access_token !== token) {
|
||||
const retryHeaders = {
|
||||
...options.headers,
|
||||
'apikey': SUPABASE_ANON_KEY,
|
||||
'Authorization': `Bearer ${latestSession.access_token}`,
|
||||
};
|
||||
response = await fetch(url, { ...options, headers: retryHeaders });
|
||||
if (response.status !== 401) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// If we still get a 401, perform the refresh using the latest refresh token
|
||||
if (latestSession.refresh_token) {
|
||||
try {
|
||||
const newSession = await refreshSession(session.refresh_token);
|
||||
const newSession = await refreshSession(latestSession.refresh_token);
|
||||
const newHeaders = {
|
||||
...options.headers,
|
||||
'apikey': SUPABASE_ANON_KEY,
|
||||
@@ -30,6 +48,8 @@ export async function safeFetch(url: string, options: RequestInit = {}): Promise
|
||||
console.error('Session refresh failed:', refreshError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user