fix: daemon /run spawn hardening, auth guard before first paint, WS refresh cap, interfaces runtime state

systemd: pre-create volatile /run paths so vacuum-walld's ProtectSystem=strict namespace setup cannot fail with 226/NAMESPACE — RuntimeDirectory=vacuum-wall nginx plus a tmpfiles.d spec (installed to /etc/tmpfiles.d/) covering /run/firewalld and /run/nginx.pid. Drop /run/sudo from ReadWritePaths: NOPASSWD children never need it, and its absence crash-looped restarts after sudo removed /run/sudo.

webui: run the auth session check before mounting the shell so logged-out visitors never flash the sidebar or a protected page; router guard and sidebar now react to auth state, and the login page renders full-bleed.

ws: cap refresh->reconnect episodes at 2 consecutive failures; if the WS path stays dead after a token refresh, abandon reconnection instead of looping refreshAuth forever (UI keeps working via REST until reload).

api: GET /api/network/interfaces now includes loopback and returns per-interface {config, runtime}; dashboard reads runtime.state (carrier counts as up) and the interfaces page filters lo client-side.

daemon: re-collect nginx state after lazy config migration (cached list went stale when the on-disk format changed under it), skip system_import.nginx when config.json already exists (re-parsing vacuum-wall's own generated sites is lossy), and poll nginx (60s) / acme (300s) state so file drift self-heals.
This commit is contained in:
2026-08-19 15:32:36 +00:00
parent 4bd4c374fd
commit 9c9f92ad04
16 changed files with 244 additions and 32 deletions
+49 -19
View File
@@ -219,21 +219,45 @@ const Pages = {
/* ── Router ────────────────────────────────────────────────── */
const router = {
state: reactive({ path: location.hash.slice(1) || '/dashboard' }),
isAuthenticated: false,
component() {
const name = this.state.path.replace(/^\//, '');
const { path } = this.state;
// Auth guard: unauthenticated users see the login page for any
// protected route (manual hash entry, back/forward, runtime
// expiry). Reactive — the auth model's data mutation re-renders
// this function, so the real page appears the instant login
// completes.
if (path !== '/login' && !isAuthenticated()) {
return hComp(LoginPage, '/login');
}
const name = path.replace(/^\//, '');
const page = Pages[name] || NotFoundPage;
return hComp(page, this.state.path);
return hComp(page, path);
},
};
// Set once the bootstrap session check settles (and implicitly on every
// later login/logout transition — isAuthenticated flips reactively). Until
// then the hashchange clamp below must NOT force unauthenticated hashes to
// #/login: a valid-session reload arrives with its 'check' still in flight,
// and clamping early would strand the user on login.
let authChecked = false;
window.location.hash || (window.location.hash = router.state.path);
window.addEventListener('hashchange', () => {
router.state.path = location.hash.slice(1) || '/dashboard';
const raw = location.hash.slice(1) || '/dashboard';
const path = raw !== '/login' && authChecked && !isAuthenticated() ? '/login' : raw;
router.state.path = path;
// Keep the URL in sync with the clamped path (loop-safe: the follow-up
// hashchange lands on the already-clamped '/login').
if (location.hash.slice(1) !== path) location.hash = path;
});
/* ── Sidebar render root ───────────────────────────────────── */
function Sidebar() {
// No nav when logged out — unauthenticated users get the full-bleed
// login page. Reactive: the auth model's terminal transition (logout /
// session expiry) re-renders this root back to null.
if (!isAuthenticated()) return null;
const current = router.state.path;
const nav = getNav();
return h('div', { class: 'sidebar' },
@@ -260,16 +284,8 @@ function MainContent() {
/* ── Init ──────────────────────────────────────────────────── */
export async function initApp() {
const sidebarEl = document.getElementById('sidebar');
const mainEl = document.getElementById('main');
if (sidebarEl && mainEl) {
render(sidebarEl, Sidebar);
render(mainEl, MainContent);
}
// Listen for login events to update router state after auth
window.addEventListener('auth:login', () => {
router.isAuthenticated = true;
// Defer to a macrotask: at dispatch time (microtask) the login form's
// hash change has not run yet — router.state.path is still '/login'.
// The deferred check runs after the hashchange task, so a fresh login
@@ -290,17 +306,31 @@ export async function initApp() {
disconnect();
});
// Check auth state before connecting WS
// Check auth state BEFORE mounting the shell: an unauthenticated visitor
// must never flash the sidebar or a protected page before the redirect
// to #/login lands.
await modelFetch('auth', { action: 'check' });
authChecked = true;
if (isAuthenticated()) {
router.isAuthenticated = true;
// A session restored at bootstrap (or a reload) may leave the URL on
// #/login — the auth guard renders the login form for that hash even
// when authenticated. Bounce to the default page so a valid session
// never strands the user on a stale login screen.
if (router.state.path === '/login') {
window.location.hash = '/dashboard';
}
fetchInitialData();
setTimeout(connect, 0);
} else {
// No valid session — redirect to login
if (router.state.path !== '/login') {
window.location.hash = '/login';
}
} else if (router.state.path !== '/login') {
// No valid session — redirect to login before the first paint.
window.location.hash = '/login';
}
const sidebarEl = document.getElementById('sidebar');
const mainEl = document.getElementById('main');
if (sidebarEl && mainEl) {
render(sidebarEl, Sidebar);
render(mainEl, MainContent);
}
}