Files
cameleer-server/ui/src/auth/use-auth.ts
hsiegeln 6794e4c234
All checks were successful
CI / build (push) Successful in 1m9s
CI / docker (push) Successful in 47s
CI / deploy (push) Successful in 29s
Fix 401 race condition: wire getAccessToken at module level
The auth store loads tokens from localStorage synchronously at import
time, but configureAuth() was deferred to a useEffect — so the first
API requests fired before the token getter was wired, causing 401s on
hard refresh. Now getAccessToken reads from the store by default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:09:46 +01:00

45 lines
1.4 KiB
TypeScript

import { useEffect } from 'react';
import { useAuthStore } from './auth-store';
import { configureAuth } from '../api/client';
import { useNavigate } from 'react-router';
export function useAuth() {
const { accessToken, isAuthenticated, refresh, logout } = useAuthStore();
const navigate = useNavigate();
// Wire onUnauthorized handler (needs navigate from router context)
useEffect(() => {
configureAuth({
onUnauthorized: async () => {
const ok = await useAuthStore.getState().refresh();
if (!ok) {
useAuthStore.getState().logout();
navigate('/login', { replace: true });
}
},
});
}, [navigate]);
// Auto-refresh: check token expiry every 30s
useEffect(() => {
if (!isAuthenticated) return;
const interval = setInterval(async () => {
const token = useAuthStore.getState().accessToken;
if (!token) return;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const expiresIn = payload.exp * 1000 - Date.now();
// Refresh when less than 5 minutes remaining
if (expiresIn < 5 * 60 * 1000) {
await refresh();
}
} catch {
// Token parse failure — ignore, will fail on next API call
}
}, 30_000);
return () => clearInterval(interval);
}, [isAuthenticated, refresh]);
return { accessToken, isAuthenticated, logout };
}