fix: auth reconnection loop, duplicate login listeners, modal double-disable
- websocket: clear tokens on refresh failure to prevent infinite 401 loop - api: write vw:user to sessionStorage on refresh for consistency with WS - api: remove vw:user from sessionStorage in clearAuthTokens - login: guard listener setup with flags to prevent duplicate attachment - modal: skip inline button disable when handler uses processing state - users: remove unused requestUpdate import
This commit is contained in:
@@ -46,6 +46,7 @@ function clearAuthTokens() {
|
|||||||
sessionStorage.removeItem('vw:refresh');
|
sessionStorage.removeItem('vw:refresh');
|
||||||
sessionStorage.removeItem('vw:access_ttl');
|
sessionStorage.removeItem('vw:access_ttl');
|
||||||
sessionStorage.removeItem('vw:session_id');
|
sessionStorage.removeItem('vw:session_id');
|
||||||
|
sessionStorage.removeItem('vw:user');
|
||||||
localStorage.removeItem('vw:user');
|
localStorage.removeItem('vw:user');
|
||||||
if (typeof window.__authRefreshTimer__ !== 'undefined') {
|
if (typeof window.__authRefreshTimer__ !== 'undefined') {
|
||||||
clearTimeout(window.__authRefreshTimer__);
|
clearTimeout(window.__authRefreshTimer__);
|
||||||
@@ -102,6 +103,7 @@ async function tryRefreshToken() {
|
|||||||
sessionStorage.setItem('vw:session_id', tokens.session_id);
|
sessionStorage.setItem('vw:session_id', tokens.session_id);
|
||||||
}
|
}
|
||||||
if (json.data.user) {
|
if (json.data.user) {
|
||||||
|
sessionStorage.setItem('vw:user', JSON.stringify(json.data.user));
|
||||||
localStorage.setItem('vw:user', JSON.stringify(json.data.user));
|
localStorage.setItem('vw:user', JSON.stringify(json.data.user));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -191,8 +191,15 @@ export function formModal(inner, title, fields, actions) {
|
|||||||
if (a.handler) {
|
if (a.handler) {
|
||||||
const origHandler = a.handler;
|
const origHandler = a.handler;
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
// Only inline-disable if the handler doesn't use processing
|
||||||
|
// state. apiSubmit/formAction already call
|
||||||
|
// setModalProcessing + refreshModals which re-creates the
|
||||||
|
// button in processing state. Without the guard the old
|
||||||
|
// button is discarded before the handler even starts.
|
||||||
|
if (!a.processing) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<span class="btn-spinner"></span>';
|
btn.innerHTML = '<span class="btn-spinner"></span>';
|
||||||
|
}
|
||||||
origHandler();
|
origHandler();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { refreshByTopic } from './model.js?v=9';
|
import { refreshByTopic } from './model.js?v=9';
|
||||||
|
import { clearAuthTokens } from './api.js?v=12';
|
||||||
|
|
||||||
let _wsConn = null;
|
let _wsConn = null;
|
||||||
let _wsReconnectMs = 0;
|
let _wsReconnectMs = 0;
|
||||||
@@ -16,8 +17,8 @@ let _wsFailCount = 0;
|
|||||||
const _directHandlers = [];
|
const _directHandlers = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh the access token. Does NOT redirect on failure — the caller
|
* Refresh the access token. On failure, clears all tokens to prevent
|
||||||
* decides what to do when refresh fails.
|
* an infinite reconnection loop with a stale token.
|
||||||
*
|
*
|
||||||
* @returns {Promise<boolean>} true if token was refreshed
|
* @returns {Promise<boolean>} true if token was refreshed
|
||||||
*/
|
*/
|
||||||
@@ -31,9 +32,15 @@ async function _tryRefreshToken() {
|
|||||||
body: JSON.stringify({ refresh_token: refresh }),
|
body: JSON.stringify({ refresh_token: refresh }),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
});
|
});
|
||||||
if (res.status !== 200) return false;
|
if (res.status !== 200) {
|
||||||
|
clearAuthTokens();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!json.ok || !json.data?.tokens) return false;
|
if (!json.ok || !json.data?.tokens) {
|
||||||
|
clearAuthTokens();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const tokens = json.data.tokens;
|
const tokens = json.data.tokens;
|
||||||
window.__auth_token__ = tokens.access_token;
|
window.__auth_token__ = tokens.access_token;
|
||||||
sessionStorage.setItem('vw:refresh', tokens.refresh_token);
|
sessionStorage.setItem('vw:refresh', tokens.refresh_token);
|
||||||
@@ -47,6 +54,7 @@ async function _tryRefreshToken() {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
|
clearAuthTokens();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,13 @@ function LoginPage() {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _loginFormBound = false;
|
||||||
|
let _passkeyBound = false;
|
||||||
|
|
||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
|
if (_loginFormBound) return;
|
||||||
|
_loginFormBound = true;
|
||||||
|
|
||||||
const form = document.getElementById('loginForm');
|
const form = document.getElementById('loginForm');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
|
|
||||||
@@ -90,6 +96,9 @@ async function doPasswordLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupPasskeyButton() {
|
function setupPasskeyButton() {
|
||||||
|
if (_passkeyBound) return;
|
||||||
|
_passkeyBound = true;
|
||||||
|
|
||||||
const passkeyBtn = document.getElementById('passkeyBtn');
|
const passkeyBtn = document.getElementById('passkeyBtn');
|
||||||
if (!passkeyBtn) return;
|
if (!passkeyBtn) return;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Requires auth: rw permission.
|
* Requires auth: rw permission.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { h, definePage, reactive, requestUpdate } from '/static/hoover/index.js?v=11';
|
import { h, definePage, reactive } from '/static/hoover/index.js?v=11';
|
||||||
import { html, PageHeader, Table, Badge, ConfirmDelete, Empty, Card, openModal, closeModal, formModal, apiFetch, toast, esc } from '/static/hoover/index.js?v=11';
|
import { html, PageHeader, Table, Badge, ConfirmDelete, Empty, Card, openModal, closeModal, formModal, apiFetch, toast, esc } from '/static/hoover/index.js?v=11';
|
||||||
|
|
||||||
const SUBSYSTEMS = [
|
const SUBSYSTEMS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user