import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { DOORMILE_TOKEN_KEY, DOORMILE_USER_KEY, loginAdmin, logoutAdmin, readStoredToken, readStoredUser, } from '@/api/doormile'; /** * Auth for the Doormile Express console. * * `POST /admin/login` returns a JWT and the admin record, and every subsequent * request carries that token — so the session lives entirely in localStorage * and there is nothing to fetch on boot. The provider still resolves * asynchronously (`isLoadingAuth`) rather than reading storage inline, because * rendering a route tree before the token is known flashes the login page at an * already-signed-in operator on every refresh. * * The axios client clears the same two keys and hard-navigates to `/login` on a * 401, so an expired token ends the session the same way signing out does — a * shell that renders but 401s on every fetch is worse than being sent back. */ const AuthContext = createContext(/** @type {any} */ (null)); /** @param {any} props */ export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoadingAuth, setIsLoadingAuth] = useState(true); const [authChecked, setAuthChecked] = useState(false); const [authError, setAuthError] = useState(null); const checkUserAuth = useCallback(() => { setIsLoadingAuth(true); const token = readStoredToken(); const stored = readStoredUser(); setIsAuthenticated(Boolean(token)); setUser(token ? stored : null); setIsLoadingAuth(false); setAuthChecked(true); }, []); useEffect(() => { checkUserAuth(); }, [checkUserAuth]); /* A sign-out in another tab has to end the session in this one too — otherwise this tab keeps a rendered console around a token that is gone. */ useEffect(() => { const onStorage = (event) => { if (event.key === DOORMILE_TOKEN_KEY || event.key === DOORMILE_USER_KEY) checkUserAuth(); }; window.addEventListener('storage', onStorage); return () => window.removeEventListener('storage', onStorage); }, [checkUserAuth]); /** * Signs in. Resolves to the API's own `{ success, message }` rather than * throwing on a refused credential, so the login form can show the server's * wording instead of a generic failure. */ const login = useCallback(async (email, password) => { setAuthError(null); try { const result = await loginAdmin(email, password); if (result?.success) { setUser(result.user ?? readStoredUser()); setIsAuthenticated(true); return { success: true }; } const message = result?.message || 'Those credentials were not accepted'; setAuthError(message); return { success: false, message }; } catch (err) { const message = err?.message || 'Could not reach the Doormile API'; setAuthError(message); return { success: false, message }; } }, []); const logout = useCallback((shouldRedirect = true) => { logoutAdmin(); setUser(null); setIsAuthenticated(false); if (shouldRedirect) window.location.replace('/login'); }, []); const navigateToLogin = useCallback(() => window.location.replace('/login'), []); const value = useMemo( () => ({ user, isAuthenticated, isLoadingAuth, /* No remote app settings on this backend — kept so consumers that gate on it stay unchanged. */ isLoadingPublicSettings: false, appPublicSettings: null, authError, authChecked, login, logout, navigateToLogin, checkUserAuth, checkAppState: checkUserAuth, }), [user, isAuthenticated, isLoadingAuth, authError, authChecked, login, logout, navigateToLogin, checkUserAuth] ); return {children}; }; export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; };