Files
Sybil-2/web/src/hooks/use-session-auth.ts
T

123 lines
3.7 KiB
TypeScript

import { useEffect, useState } from "preact/hooks";
import { getConfiguredToken, setAuthToken, verifySession } from "@/lib/api";
import { revokeAgentBrowserPush, startAgentBrowserSession } from "@/lib/agent-push-browser";
const TOKEN_STORAGE_KEY = "sybil_admin_token";
export type AuthMode = "open" | "token";
function readStoredToken() {
return localStorage.getItem(TOKEN_STORAGE_KEY)?.trim() || null;
}
function persistToken(token: string | null) {
if (token) {
localStorage.setItem(TOKEN_STORAGE_KEY, token);
return;
}
localStorage.removeItem(TOKEN_STORAGE_KEY);
}
function normalizeAuthError(message: string) {
if (message.includes("invalid bearer token")) {
return "Incorrect token. Please try again.";
}
if (message.includes("missing bearer token")) {
return "Enter your admin token.";
}
return message;
}
export function useSessionAuth() {
const initialToken = readStoredToken() ?? getConfiguredToken() ?? "";
const [authTokenInput, setAuthTokenInput] = useState(initialToken);
const [isCheckingSession, setIsCheckingSession] = useState(true);
const [isSigningIn, setIsSigningIn] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [authMode, setAuthMode] = useState<AuthMode | null>(null);
const [authError, setAuthError] = useState<string | null>(null);
const completeSessionCheck = async (tokenCandidate: string | null) => {
setAuthToken(tokenCandidate);
const session = await verifySession();
setIsAuthenticated(true);
setAuthMode(session.mode);
setAuthError(null);
persistToken(tokenCandidate);
};
const handleAuthFailure = (message: string | null) => {
void revokeAgentBrowserPush();
setIsAuthenticated(false);
setAuthMode(null);
setAuthError(message ? normalizeAuthError(message) : null);
setAuthToken(null);
persistToken(null);
};
const handleSignIn = async (tokenCandidate: string | null) => {
setIsSigningIn(true);
setAuthError(null);
try {
await completeSessionCheck(tokenCandidate);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setAuthError(normalizeAuthError(message));
setIsAuthenticated(false);
setAuthMode(null);
} finally {
setIsSigningIn(false);
}
};
const logout = async () => {
await revokeAgentBrowserPush();
setAuthToken(null);
persistToken(null);
setIsAuthenticated(false);
setAuthMode(null);
setAuthError(null);
};
useEffect(() => {
if (!isAuthenticated || authMode !== "token") return;
return startAgentBrowserSession();
}, [isAuthenticated, authMode]);
useEffect(() => {
// Signing out in one tab retires the others instead of using a stale token.
const signedOut = (event: StorageEvent) => { if (event.key === TOKEN_STORAGE_KEY && !event.newValue) handleAuthFailure(null); };
window.addEventListener("storage", signedOut);
return () => window.removeEventListener("storage", signedOut);
}, []);
useEffect(() => {
const token = readStoredToken() ?? getConfiguredToken();
void (async () => {
try {
await completeSessionCheck(token);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
// A first visit without a token is an invitation to sign in, not a failed attempt.
handleAuthFailure(!token && message.includes("missing bearer token") ? null : message);
} finally {
setIsCheckingSession(false);
}
})();
}, []);
return {
authTokenInput,
setAuthTokenInput,
isCheckingSession,
isSigningIn,
isAuthenticated,
authMode,
authError,
handleAuthFailure,
handleSignIn,
logout,
};
}