auth: slim components/auth.js to ceremony helpers + doLogin
This commit is contained in:
@@ -1,142 +1,58 @@
|
|||||||
/**
|
/**
|
||||||
* Hoover — auth.js
|
* Hoover — auth.js (components)
|
||||||
*
|
*
|
||||||
* Token refresh scheduler, session check, logout.
|
* 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.
|
||||||
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.
|
|
||||||
*
|
*
|
||||||
* @returns {Promise<boolean>}
|
* 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');
|
import { apiFetch } from '../api.js';
|
||||||
if (result.ok) {
|
import { modelFetch } from '../model.js';
|
||||||
const { user, permissions } = result.data || {};
|
import { getAuthData } from '../auth_model.js';
|
||||||
if (user) {
|
|
||||||
sessionStorage.setItem('vw:user', JSON.stringify(user));
|
|
||||||
if (permissions) {
|
|
||||||
sessionStorage.setItem('vw:permissions', JSON.stringify(permissions));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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() {
|
export async function logout() {
|
||||||
const token = getAuthToken();
|
const auth = getAuthData();
|
||||||
if (token) {
|
if (auth?.token) {
|
||||||
try {
|
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', {
|
await fetch('/api/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + auth.token,
|
||||||
|
'X-Session-Id': auth.session_id || '',
|
||||||
|
},
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
body: JSON.stringify({ refresh_token: refresh || '' }),
|
body: JSON.stringify({ refresh_token: auth.refresh || '' }),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch { /* ignore — we're clearing everything anyway */ }
|
||||||
// ignore errors, we're clearing everything anyway
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cancelTokenRefresh();
|
modelFetch('auth', { action: 'logout' });
|
||||||
clearAuthTokens();
|
|
||||||
redirectLogin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize auth state on page load.
|
* Handle a successful login: drive the auth model through the 'login'
|
||||||
* Checks stored tokens, validates session, and schedules refresh.
|
* action (onSuccess persists the session, schedules the TTL refresh, and
|
||||||
* Since sessionStorage is cleared on tab close, a closed/reopened tab
|
* fires auth:login), then navigate.
|
||||||
* will always fall through to reauth.
|
|
||||||
*
|
*
|
||||||
* @returns {Promise<boolean>} true if authenticated
|
* @param {object} data — login response data ({ tokens, user, permissions, access_ttl })
|
||||||
*/
|
|
||||||
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 {string} [redirectPath] — where to navigate after login
|
* @param {string} [redirectPath] — where to navigate after login
|
||||||
*/
|
*/
|
||||||
export function handleLoginSuccess(data, redirectPath = '/dashboard') {
|
export function doLogin(data, redirectPath = '/dashboard') {
|
||||||
const { tokens, user, permissions } = data || {};
|
modelFetch('auth', { action: 'login', payload: 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')) : {},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
window.location.hash = redirectPath;
|
window.location.hash = redirectPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user