app: use auth model for init, sidebar, login

This commit is contained in:
2026-08-15 02:08:16 +00:00
parent 64f3a77411
commit 4f74192302
3 changed files with 37 additions and 15 deletions
+25 -7
View File
@@ -1,4 +1,4 @@
import { h, render, Link, hComp, ToastContainer, connect, apiFetch, modelRegister, modelFetch, reactive, initAuth, getAuthToken, checkSession } from '/static/hoover/index.js'; import { h, render, Link, hComp, ToastContainer, connect, disconnect, apiFetch, modelRegister, modelFetch, reactive, createAuthModel, isAuthenticated, getAuthData } from '/static/hoover/index.js';
import DashboardPage from '/static/pages/dashboard.js'; import DashboardPage from '/static/pages/dashboard.js';
import InterfacesPage from '/static/pages/interfaces.js'; import InterfacesPage from '/static/pages/interfaces.js';
@@ -32,14 +32,17 @@ const _NavBase = [
]; ];
function getNav() { function getNav() {
const perms = getAuthData()?.permissions;
const nav = [..._NavBase]; const nav = [..._NavBase];
const perms = JSON.parse(sessionStorage.getItem('vw:permissions') || 'null');
if (perms && perms.auth === 'rw') { if (perms && perms.auth === 'rw') {
nav.push({ path: '/users', label: 'Users' }); nav.push({ path: '/users', label: 'Users' });
} }
return nav; return nav;
} }
/* ── Auth model (silent topic — the daemon never broadcasts 'auth') ── */
modelRegister('auth', createAuthModel());
modelRegister('firewall', { modelRegister('firewall', {
subsystem: 'firewall', subsystem: 'firewall',
fetch: async () => { fetch: async () => {
@@ -266,14 +269,29 @@ export async function initApp() {
// Listen for login events to update router state after auth // Listen for login events to update router state after auth
window.addEventListener('auth:login', () => { window.addEventListener('auth:login', () => {
router.isAuthenticated = true; router.isAuthenticated = true;
if (!router.state.path.startsWith('/login')) { // Defer to a macrotask: at dispatch time (microtask) the login form's
fetchInitialData(); // hash change has not run yet — router.state.path is still '/login'.
} // The deferred check runs after the hashchange task, so a fresh login
// fetches all models. connect() is idempotent (_wsConnect no-ops with
// a live connection) and gives the post-login session its WS — today
// WS only connects on an authenticated page load (pre-existing gap).
setTimeout(() => {
connect();
if (!router.state.path.startsWith('/login')) {
fetchInitialData();
}
}, 0);
});
// Terminal auth transition — close the WS socket so a same-tab relogin
// establishes a fresh connection with the new user's token.
window.addEventListener('auth:logout', () => {
disconnect();
}); });
// Check auth state before connecting WS // Check auth state before connecting WS
const ok = await initAuth(); await modelFetch('auth', { action: 'check' });
if (ok) { if (isAuthenticated()) {
router.isAuthenticated = true; router.isAuthenticated = true;
fetchInitialData(); fetchInitialData();
setTimeout(connect, 0); setTimeout(connect, 0);
+9 -3
View File
@@ -23,13 +23,19 @@ export { definePage, hComp } from './component.js';
export { createRouter, Link } from './router.js'; export { createRouter, Link } from './router.js';
/* ── WebSocket ───────────────────────────────────────────────── */ /* ── WebSocket ───────────────────────────────────────────────── */
export { connect, onMessage } from './websocket.js'; export { connect, onMessage, disconnect } from './websocket.js';
/* ── API & Toast ─────────────────────────────────────────────── */ /* ── API & Toast ─────────────────────────────────────────────── */
export { apiFetch, toast, dismissToast, apiSubmit, checkAbort, poll, refactorLoad, formAction, setAuthToken, clearAuthTokens, getAuthToken } from './api.js'; export { apiFetch, toast, dismissToast, apiSubmit, checkAbort, poll, refactorLoad, formAction }
from './api.js';
/* ── UI Components: Auth ──────────────────────────────────────── */ /* ── UI Components: Auth ──────────────────────────────────────── */
export { scheduleTokenRefresh, cancelTokenRefresh, checkSession, logout, initAuth, handleLoginSuccess, webauthnSupported, startRegistration, startAuthentication } from './components/auth.js'; export { logout, doLogin, webauthnSupported, checkWebAuthnCapable,
startRegistration, startAuthentication } from './components/auth.js';
/* ── Auth model ───────────────────────────────────────────────── */
export { createAuthModel, getAuthToken, isAuthenticated, refreshAuth, getAuthData }
from './auth_model.js';
/* ── Model ───────────────────────────────────────────────────── */ /* ── Model ───────────────────────────────────────────────────── */
export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js'; export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js';
+3 -5
View File
@@ -11,9 +11,7 @@ import {
html, html,
apiFetch, apiFetch,
toast, toast,
setAuthToken, doLogin,
getAuthToken,
handleLoginSuccess,
webauthnSupported, webauthnSupported,
startAuthentication, startAuthentication,
esc, esc,
@@ -89,7 +87,7 @@ async function doPasswordLogin() {
}); });
if (res.ok) { if (res.ok) {
handleLoginSuccess(res.data); doLogin(res.data);
toast('Welcome, ' + esc(username), 'success'); toast('Welcome, ' + esc(username), 'success');
} else { } else {
errEl.textContent = res.error || 'Login failed'; errEl.textContent = res.error || 'Login failed';
@@ -173,7 +171,7 @@ const passkeyClickHandler = async () => {
}); });
if (finishRes.ok) { if (finishRes.ok) {
handleLoginSuccess(finishRes.data); doLogin(finishRes.data);
toast('Welcome, ' + esc(username), 'success'); toast('Welcome, ' + esc(username), 'success');
} else { } else {
errEl.textContent = finishRes.error || 'Passkey authentication failed'; errEl.textContent = finishRes.error || 'Passkey authentication failed';