Files
doormilxpress_astryx/src/lib/AuthContext.jsx

123 lines
4.0 KiB
JavaScript

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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};