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. */ /** * Determines whether the signed-in user is a client/merchant account * rather than Doormile internal staff. */ export const isClientUser = (user) => { if (!user) { const storedTenant = localStorage.getItem('tenantid'); const storedAuth = (localStorage.getItem('authname') || '').toLowerCase(); if (storedTenant && storedTenant !== '0' && storedTenant !== 'null' && storedTenant !== '') return true; if (storedAuth.includes('dailygrubs.com')) return true; return false; } const email = (user.email || user.primaryemail || user.authname || '').toLowerCase(); if (email.includes('dailygrubs.com') || email === 'info@dailygrubs.com') return true; if (user.tenantid != null && user.tenantid !== '' && user.tenantid !== 0 && user.tenantid !== '0') return true; const role = String(user.role || user.roleid || '').toLowerCase(); if (role === 'client' || role === 'tenant' || role === 'merchant') return true; return false; }; 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 isClient = useMemo(() => isClientUser(user), [user]); const value = useMemo( () => ({ user, isClient, 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, isClient, 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; };