From 64f3a774119f33d24a3ff9c786020ace675404f5 Mon Sep 17 00:00:00 2001 From: Mike Teehan Date: Sat, 15 Aug 2026 01:54:50 +0000 Subject: [PATCH] auth: slim components/auth.js to ceremony helpers + doLogin --- webui/static/hoover/components/auth.js | 154 ++++++------------------- 1 file changed, 35 insertions(+), 119 deletions(-) diff --git a/webui/static/hoover/components/auth.js b/webui/static/hoover/components/auth.js index 8954c0c..97629e2 100644 --- a/webui/static/hoover/components/auth.js +++ b/webui/static/hoover/components/auth.js @@ -1,142 +1,58 @@ /** - * Hoover — auth.js + * Hoover — auth.js (components) * - * Token refresh scheduler, session check, logout. - */ - -import { apiFetch, setAuthToken, clearAuthTokens, redirectLogin, getAuthToken, tryRefreshToken, toast } from '../api.js'; - -/** - * Schedule a token refresh based on the access token TTL stored in sessionStorage. - * The refresh fires at TTL - 60 seconds to allow the browser to refresh smoothly. - */ -export function scheduleTokenRefresh() { - if (typeof window.__authRefreshTimer__ !== 'undefined') { - clearTimeout(window.__authRefreshTimer__); - } - const ttl = parseInt(sessionStorage.getItem('vw:access_ttl'), 10) || 300000; - const delay = Math.max(ttl - 60000, 30000); - - window.__authRefreshTimer__ = setTimeout(async () => { - const ok = await tryRefreshToken(); - if (ok) { - scheduleTokenRefresh(); - } - }, delay); -} - -/** - * Stop the refresh timer (e.g. user logs out or page unloads). - */ -export function cancelTokenRefresh() { - if (typeof window.__authRefreshTimer__ !== 'undefined') { - clearTimeout(window.__authRefreshTimer__); - window.__authRefreshTimer__ = undefined; - } -} - -/** - * Check the current session by calling GET /api/auth/session. - * Returns true if the session is valid. + * Thin ceremony layer over the auth model (hoover/auth_model.js), which is + * the single source of truth for token storage, refresh scheduling, session + * validation, and login/logout state transitions. * - * @returns {Promise} + * This file only contains: + * - logout() — POST /api/auth/logout then drive the model to terminal + * - doLogin() — drive the model through the 'login' action, then navigate + * - WebAuthn (passkey) ceremony helpers — not state management */ -export async function checkSession() { - const result = await apiFetch('/api/auth/session'); - if (result.ok) { - const { user, permissions } = result.data || {}; - if (user) { - sessionStorage.setItem('vw:user', JSON.stringify(user)); - if (permissions) { - sessionStorage.setItem('vw:permissions', JSON.stringify(permissions)); - } - return true; - } - } - return false; -} + +import { apiFetch } from '../api.js'; +import { modelFetch } from '../model.js'; +import { getAuthData } from '../auth_model.js'; /** - * Logout: blacklist current token and clear auth state, then redirect to login. + * Logout: blacklist the current tokens server-side, then drive the model to + * the terminal all-nulls state. The model's onSuccess clears storage, + * redirects to #/login, and dispatches auth:logout (app.js closes the WS + * socket via disconnect() — no explicit WS close here). The server reads + * jti/username from the request context; refresh_token is sent in the body + * per the blueprint contract (webui/api/auth.py:75). */ export async function logout() { - const token = getAuthToken(); - if (token) { + const auth = getAuthData(); + if (auth?.token) { try { - const headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': 'Bearer ' + token, - 'X-Session-Id': sessionStorage.getItem('vw:session_id'), - }; - const refresh = sessionStorage.getItem('vw:refresh'); await fetch('/api/auth/logout', { method: 'POST', - headers, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer ' + auth.token, + 'X-Session-Id': auth.session_id || '', + }, credentials: 'same-origin', - body: JSON.stringify({ refresh_token: refresh || '' }), + body: JSON.stringify({ refresh_token: auth.refresh || '' }), }); - } catch { - // ignore errors, we're clearing everything anyway - } + } catch { /* ignore — we're clearing everything anyway */ } } - cancelTokenRefresh(); - clearAuthTokens(); - redirectLogin(); + modelFetch('auth', { action: 'logout' }); } /** - * Initialize auth state on page load. - * Checks stored tokens, validates session, and schedules refresh. - * Since sessionStorage is cleared on tab close, a closed/reopened tab - * will always fall through to reauth. + * Handle a successful login: drive the auth model through the 'login' + * action (onSuccess persists the session, schedules the TTL refresh, and + * fires auth:login), then navigate. * - * @returns {Promise} true if authenticated - */ -export async function initAuth() { - const token = getAuthToken(); - if (token) { - const saved = JSON.parse(sessionStorage.getItem('vw:user') || 'null'); - if (saved) { - const ok = await checkSession(); - if (ok) { - scheduleTokenRefresh(); - return true; - } - } - } - clearAuthTokens(); - return false; -} - -/** - * Handle login response: store tokens, schedule refresh, redirect. - * - * @param {object} data — login/migrate response data + * @param {object} data — login response data ({ tokens, user, permissions, access_ttl }) * @param {string} [redirectPath] — where to navigate after login */ -export function handleLoginSuccess(data, redirectPath = '/dashboard') { - const { tokens, user, permissions } = data || {}; - if (tokens) { - setAuthToken(tokens.access_token); - sessionStorage.setItem('vw:refresh', tokens.refresh_token); - sessionStorage.setItem('vw:access_ttl', String((data.access_ttl || 300) * 1000)); - sessionStorage.setItem('vw:session_id', tokens.session_id); - if (user) { - sessionStorage.setItem('vw:user', JSON.stringify(user)); - if (permissions) { - sessionStorage.setItem('vw:permissions', JSON.stringify(permissions)); - } - } - scheduleTokenRefresh(); - } - // Notify app.js that auth is established (used to set router.isAuthenticated) - window.dispatchEvent(new CustomEvent('auth:login', { - detail: { - permissions: sessionStorage.getItem('vw:permissions') ? - JSON.parse(sessionStorage.getItem('vw:permissions')) : {}, - }, - })); +export function doLogin(data, redirectPath = '/dashboard') { + modelFetch('auth', { action: 'login', payload: data }); window.location.hash = redirectPath; }