68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import {
|
|
fetchAuthStatus,
|
|
login as loginRequest,
|
|
logout as logoutRequest,
|
|
register as registerRequest,
|
|
type AuthUser,
|
|
} from "./authClient";
|
|
|
|
export interface AuthState {
|
|
/**
|
|
* Whether this deployment offers login at all (runtime flag, D-015).
|
|
* Starts false — the auth UI stays hidden until the /me probe confirms the
|
|
* endpoints exist, so a disabled deployment never flashes a Sign in button.
|
|
*/
|
|
enabled: boolean;
|
|
/** The signed-in user, or null while anonymous (the default). */
|
|
user: AuthUser | null;
|
|
/** Rejects with a UI-safe Error message on failure (e.g. 401). */
|
|
login: (username: string, password: string) => Promise<void>;
|
|
/** Rejects with a UI-safe Error message on failure (400/409). */
|
|
register: (username: string, password: string) => Promise<void>;
|
|
/** Always resolves; local state is cleared even if the server is down. */
|
|
logout: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Session state for the M4 auth UI. The session cookie is HttpOnly, so the
|
|
* only source of truth is GET /api/auth/me, probed once on mount.
|
|
* `fetchAuthStatus` never rejects — 401 resolves to anonymous, and 404
|
|
* (AUTH_ENABLED=false, D-015) or network failure (offline, server down)
|
|
* resolve to `enabled: false`, keeping the anonymous-first invariant:
|
|
* a failing /me must not surface any error.
|
|
*/
|
|
export function useAuth(): AuthState {
|
|
const [enabled, setEnabled] = useState(false);
|
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
fetchAuthStatus().then((status) => {
|
|
// Guard against StrictMode double-mounts / fast unmounts.
|
|
if (cancelled) return;
|
|
setEnabled(status.enabled);
|
|
if (status.user) setUser(status.user);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const login = useCallback(async (username: string, password: string) => {
|
|
setUser(await loginRequest(username, password));
|
|
}, []);
|
|
|
|
const register = useCallback(async (username: string, password: string) => {
|
|
// Register signs the user in immediately (the 201 sets the cookie).
|
|
setUser(await registerRequest(username, password));
|
|
}, []);
|
|
|
|
const logout = useCallback(async () => {
|
|
await logoutRequest();
|
|
setUser(null);
|
|
}, []);
|
|
|
|
return { enabled, user, login, register, logout };
|
|
}
|