import { createContext, useContext, useState, useEffect, useCallback, type ReactNode, } from 'react' import api from '../services/api' export interface AuthUser { id: string username: string email: string | null role: 'admin' | 'user' is_active: boolean } interface AuthContextValue { user: AuthUser | null isAdmin: boolean isLoading: boolean /** True when the backend has no users yet (first-run). */ needsSetup: boolean login: (username: string, password: string) => Promise logout: () => void /** Called after the setup endpoint creates the first admin. */ onSetupComplete: (accessToken: string, refreshToken: string) => Promise } const AuthContext = createContext(null) export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext) if (!ctx) throw new Error('useAuth must be used within AuthProvider') return ctx } // ── Token helpers ────────────────────────────────────────────────────── function getStoredToken(): string | null { return localStorage.getItem('access_token') } function storeToken(token: string) { localStorage.setItem('access_token', token) } function getStoredRefreshToken(): string | null { return localStorage.getItem('refresh_token') } function storeRefreshToken(token: string) { localStorage.setItem('refresh_token', token) } function clearTokens() { localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') } // ── Provider ─────────────────────────────────────────────────────────── export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) const [isLoading, setIsLoading] = useState(true) const [needsSetup, setNeedsSetup] = useState(false) const isAdmin = user?.role === 'admin' const fetchMe = useCallback(async () => { try { const res = await api.get('/auth/me') setUser(res.data) } catch (err: any) { // Only clear tokens on auth failure (401/403), not network errors. if (err?.response?.status === 401 || err?.response?.status === 403) { clearTokens() setUser(null) } // Network errors: leave tokens in place, user stays on login screen // but can retry without re-entering credentials. } }, []) // Boot: check if setup is needed, then try to restore session. useEffect(() => { ;(async () => { try { const statusRes = await api.get('/auth/status') if (!statusRes.data.setup_completed) { setNeedsSetup(true) setIsLoading(false) return } } catch { // Backend unreachable — fall through to login screen. } const token = getStoredToken() if (token) { await fetchMe() } setIsLoading(false) })() }, [fetchMe]) const login = useCallback( async (username: string, password: string) => { const res = await api.post('/auth/login', { username, password }) const { access_token, refresh_token } = res.data storeToken(access_token) storeRefreshToken(refresh_token) await fetchMe() }, [fetchMe], ) const logout = useCallback(() => { clearTokens() setUser(null) }, []) const onSetupComplete = useCallback( async (accessToken: string, refreshToken: string) => { storeToken(accessToken) storeRefreshToken(refreshToken) setNeedsSetup(false) await fetchMe() }, [fetchMe], ) // Axios interceptor: on 401, try to refresh once using the stored // refresh token. If that fails, sign out. useEffect(() => { const interceptor = api.interceptors.response.use( (res) => res, async (error) => { const original = error.config if ( error.response?.status === 401 && !original._retry && !original.url?.includes('/auth/refresh') && !original.url?.includes('/auth/login') ) { original._retry = true const rt = getStoredRefreshToken() if (rt) { try { const res = await api.post('/auth/refresh', { refresh_token: rt }) const { access_token, refresh_token } = res.data storeToken(access_token) storeRefreshToken(refresh_token) original.headers['Authorization'] = `Bearer ${access_token}` return api(original) } catch { clearTokens() setUser(null) } } } return Promise.reject(error) }, ) return () => api.interceptors.response.eject(interceptor) }, []) return ( {children} ) }