123 lines
4.0 KiB
JavaScript
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;
|
|
};
|