TASK-004: Bootstrap frontend auth state from backend session check
Validates session on app mount by calling GET /api/auth/session instead of relying solely on cached sessionStorage. This ensures the UI state always reflects server reality — expired or revoked sessions are detected immediately. Changes: - Backend: Add GET /api/auth/session endpoint (requires valid session, returns 200/401) - Frontend: Add useSessionValidation hook for mount-time validation - Frontend: Add SessionValidationLoading component for validation spinner - Frontend: Update AuthProvider to call validation on mount with loading state - Frontend: Add validateSession API function - Docs: Update Features.md with session validation behavior - Docs: Update Web-Development.md with session validation pattern Handles three outcomes: 1. Valid session (200): Proceed with cached state 2. Invalid session (401): Clear sessionStorage and redirect to login 3. Network error: Don't logout (backend may be temporarily unreachable) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -11,12 +11,18 @@
|
||||
* accepts the request (returns 2xx) and not authenticated when the backend
|
||||
* rejects it (returns 401/403).
|
||||
*
|
||||
* On app mount, the provider validates the cached session state with the backend
|
||||
* by calling `GET /api/auth/session`. While this check is in flight, a loading
|
||||
* indicator is shown. If the backend returns 401/403, the session is cleared.
|
||||
* If a network error occurs, the user is not logged out (the backend may be
|
||||
* temporarily unreachable).
|
||||
*
|
||||
* The `isAuthenticated` state persists in `sessionStorage` to survive page
|
||||
* refreshes within the browser tab but is cleared on tab close. The session
|
||||
* cookie itself persists according to the backend's cookie settings.
|
||||
*/
|
||||
|
||||
import {
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -26,6 +32,8 @@ import {
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as authApi from "../api/auth";
|
||||
import { setUnauthorizedHandler } from "../api/client";
|
||||
import { SessionValidationLoading } from "../components/SessionValidationLoading";
|
||||
import { useSessionValidation } from "../hooks/useSessionValidation";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context
|
||||
@@ -56,6 +64,10 @@ const IS_AUTHENTICATED_KEY = "bangui_authenticated";
|
||||
*
|
||||
* Place this inside `<FluentProvider>` and `<BrowserRouter>` so all
|
||||
* descendants can call `useAuth()`.
|
||||
*
|
||||
* On mount, validates the cached session with the backend. While validation
|
||||
* is in progress, a loading indicator is shown. If validation fails with 401,
|
||||
* the user is logged out. Network errors do not cause logout.
|
||||
*/
|
||||
export function AuthProvider({
|
||||
children,
|
||||
@@ -66,6 +78,7 @@ export function AuthProvider({
|
||||
const stored = sessionStorage.getItem(IS_AUTHENTICATED_KEY);
|
||||
return stored === "true";
|
||||
});
|
||||
const [isValidating, setIsValidating] = useState<boolean>(isAuthenticated);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSessionExpired = useCallback((): void => {
|
||||
@@ -74,6 +87,18 @@ export function AuthProvider({
|
||||
navigate("/login", { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
const handleSessionValid = useCallback((): void => {
|
||||
// Session is already set from sessionStorage; no action needed.
|
||||
}, []);
|
||||
|
||||
const handleValidationError = useCallback(
|
||||
(error: Error): void => {
|
||||
// Network error — log but don't logout.
|
||||
console.warn("Session validation network error:", error);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect((): (() => void) => {
|
||||
setUnauthorizedHandler((): void => {
|
||||
handleSessionExpired();
|
||||
@@ -83,6 +108,16 @@ export function AuthProvider({
|
||||
};
|
||||
}, [handleSessionExpired]);
|
||||
|
||||
const validationResult = useSessionValidation(
|
||||
handleSessionValid,
|
||||
handleSessionExpired,
|
||||
handleValidationError,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsValidating(validationResult.isLoading);
|
||||
}, [validationResult.isLoading]);
|
||||
|
||||
const login = useCallback(async (password: string): Promise<void> => {
|
||||
await authApi.login(password);
|
||||
sessionStorage.setItem(IS_AUTHENTICATED_KEY, "true");
|
||||
@@ -104,5 +139,10 @@ export function AuthProvider({
|
||||
[isAuthenticated, login, logout],
|
||||
);
|
||||
|
||||
// Show loading spinner while validating session on mount.
|
||||
if (isValidating && isAuthenticated) {
|
||||
return <SessionValidationLoading />;
|
||||
}
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user