feat: add auth subsystem with WebAuthn passkeys support
New modules: lib/auth, lib/auth_users, lib/db, lib/db_sqlite, lib/password, lib/webauthn, daemon/handlers/auth, scripts/bootstrap_auth, tests/test_auth Frontend: webui/api/auth, hoover/components/auth, pages/login, passkeys, users Updates: daemon/iface and server, lib/common and nginx, pyproject.toml deps, install script, server.py, app.js, and websocket/api clients
This commit is contained in:
@@ -10,10 +10,41 @@ import { refreshByTopic } from './model.js?v=9';
|
||||
|
||||
let _wsConn = null;
|
||||
let _wsReconnectMs = 0;
|
||||
let _wsFailCount = 0;
|
||||
|
||||
/** Direct onMessage handlers: { topics, handler, unsubscribed }[] */
|
||||
/** Direct onMessage handlers — { topics, handler, unsubscribed }[] */
|
||||
const _directHandlers = [];
|
||||
|
||||
/**
|
||||
* Refresh the access token. Does NOT redirect on failure — the caller
|
||||
* decides what to do when refresh fails.
|
||||
*
|
||||
* @returns {Promise<boolean>} true if token was refreshed
|
||||
*/
|
||||
async function _tryRefreshToken() {
|
||||
const refresh = localStorage.getItem('vw:refresh');
|
||||
if (!refresh) return false;
|
||||
try {
|
||||
const res = await fetch('/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: refresh }),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (res.status !== 200) return false;
|
||||
const json = await res.json();
|
||||
if (!json.ok || !json.data?.tokens) return false;
|
||||
const tokens = json.data.tokens;
|
||||
window.__auth_token__ = tokens.access_token;
|
||||
localStorage.setItem('vw:refresh', tokens.refresh_token);
|
||||
localStorage.setItem('vw:access_ttl', String((json.data.access_ttl || 900) * 1000));
|
||||
localStorage.setItem('vw:user', JSON.stringify(json.data.user));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the WebSocket URL. Supports an override via `window.__WS_URL__`
|
||||
* (useful for proxy setups). Falls back to port 9091 when the current
|
||||
@@ -25,17 +56,43 @@ function _wsUrl() {
|
||||
return proto + '//' + location.host + '/ws';
|
||||
}
|
||||
|
||||
/** Attempt a WebSocket connection. */
|
||||
/** Attempt a WebSocket connection.
|
||||
* Passes the JWT in the WebSocket subprotocol header (Sec-WebSocket-Protocol)
|
||||
* instead of a query parameter, keeping it out of logs and browser history.
|
||||
*/
|
||||
function _wsConnect() {
|
||||
if (_wsConn && _wsConn.readyState <= 1) return;
|
||||
|
||||
_wsConn = new WebSocket(_wsUrl());
|
||||
const token = window.__auth_token__;
|
||||
if (token) {
|
||||
_wsConn = new WebSocket(_wsUrl(), ['Bearer ' + token]);
|
||||
} else {
|
||||
_wsConn = new WebSocket(_wsUrl());
|
||||
}
|
||||
|
||||
_wsConn.onopen = () => {
|
||||
_wsReconnectMs = 0;
|
||||
_wsFailCount = 0;
|
||||
};
|
||||
|
||||
_wsConn.onclose = () => {
|
||||
if (!window.__auth_token__) return;
|
||||
_wsFailCount++;
|
||||
|
||||
if (_wsFailCount >= 3) {
|
||||
// Attempt token refresh after repeated failures. No redirect
|
||||
// on failure — the reconnect loop continues.
|
||||
(async () => {
|
||||
const ok = await _tryRefreshToken();
|
||||
if (ok) {
|
||||
_wsFailCount = 0;
|
||||
_wsReconnectMs = 0;
|
||||
_wsConn = null;
|
||||
setTimeout(_wsConnect, 100);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
_wsReconnectMs = Math.min(_wsReconnectMs * 2 + 1000, 15000);
|
||||
setTimeout(_wsConnect, _wsReconnectMs);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user