Files
MobilityOps/frontend/src/context/AuthContext.tsx
T

116 lines
3.3 KiB
TypeScript

import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
import { api, onUnauthorized } from "../api/client";
import type { CurrentUser, Role, SystemStatus } from "../api/types";
interface AuthState {
user: CurrentUser | null;
loading: boolean;
demoMode: boolean;
loginAs: (role: Role) => Promise<void>;
loginWithPassword: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthState | undefined>(undefined);
const STORAGE_KEY = "mobilityops.demo-user";
function readCachedUser(): CurrentUser | null {
const stored = sessionStorage.getItem(STORAGE_KEY);
return stored ? (JSON.parse(stored) as CurrentUser) : null;
}
function cacheUser(user: CurrentUser | null) {
if (user) {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(user));
} else {
sessionStorage.removeItem(STORAGE_KEY);
}
}
export function AuthProvider({ children }: { children: ReactNode }) {
// The cached value only avoids a login-screen flash while the server round trip below is
// in flight; it is never trusted on its own. `loading` stays true until the server has
// confirmed (or rejected) the session cookie.
const [user, setUser] = useState<CurrentUser | null>(() => readCachedUser());
const [loading, setLoading] = useState(true);
const [demoMode, setDemoMode] = useState(true);
useEffect(() => {
let cancelled = false;
api.get<SystemStatus>("/api/v1/system/status")
.then((system) => setDemoMode(system.demo_mode))
.catch(() => setDemoMode(true))
.finally(() => api
.get<CurrentUser>("/api/v1/auth/session")
.then((confirmed) => {
if (cancelled) return;
setUser(confirmed);
cacheUser(confirmed);
})
.catch(() => {
if (cancelled) return;
setUser(null);
cacheUser(null);
})
.finally(() => {
if (!cancelled) setLoading(false);
}));
return () => {
cancelled = true;
};
}, []);
useEffect(
() =>
onUnauthorized(() => {
setUser(null);
cacheUser(null);
}),
[],
);
const loginAs = useCallback(async (role: Role) => {
setLoading(true);
try {
const loggedIn = await api.post<CurrentUser>("/api/v1/demo/login", { role });
setUser(loggedIn);
cacheUser(loggedIn);
} finally {
setLoading(false);
}
}, []);
const loginWithPassword = useCallback(async (email: string, password: string) => {
setLoading(true);
try {
const loggedIn = await api.post<CurrentUser>("/api/v1/auth/login", { email, password });
setUser(loggedIn);
cacheUser(loggedIn);
} finally {
setLoading(false);
}
}, []);
const logout = useCallback(async () => {
setUser(null);
cacheUser(null);
try {
await api.post("/api/v1/auth/logout");
} catch {
// Best effort: the cookie is cleared server-side when it works, and the client has
// already dropped its own state either way.
}
}, []);
return (
<AuthContext.Provider value={{ user, loading, demoMode, loginAs, loginWithPassword, logout }}>{children}</AuthContext.Provider>
);
}
export function useAuth(): AuthState {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}