123 lines
3.7 KiB
TypeScript
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,
|
|
};
|
|
}
|