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(null); const [authError, setAuthError] = useState(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, }; }