fix(auth): treat expired JWTs as no identity, improve logout and token verification flow, and bump deps

This commit is contained in:
2026-03-04 01:11:19 +00:00
parent 89b9d01628
commit 34d40f7370
11 changed files with 1232 additions and 1581 deletions

View File

@@ -0,0 +1,15 @@
[ 916ms] [ERROR] method: >>getCombinedMetrics<< got an ERROR: "Valid identity required" with data {} @ http://localhost:3000/bundle.js:15
[ 972ms] [ERROR] method: >>getConfiguration<< got an ERROR: "Valid identity required" with data {} @ http://localhost:3000/bundle.js:15
[ 973ms] [ERROR] method: >>getRecentLogs<< got an ERROR: "Valid identity required" with data {} @ http://localhost:3000/bundle.js:15
[ 990ms] K2
[ 1024ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
[ 37030ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: @ http://localhost:3000/typedserver/devtools:16227
[ 37031ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 37923ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 37923ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 39699ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 39699ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 44287ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 44288ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 53685ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 53685ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251

View File

@@ -0,0 +1,90 @@
[ 1146ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
[ 26151ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
[ 257684ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: @ http://localhost:3000/bundle.js:38066
[ 257684ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: @ http://localhost:3000/typedserver/devtools:16227
[ 257684ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/bundle.js:38066
[ 257685ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 258151ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 258500ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 258500ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 258568ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/bundle.js:38066
[ 258568ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/bundle.js:38066
[ 259149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 260149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 260245ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/bundle.js:38066
[ 260245ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/bundle.js:38066
[ 260324ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 260324ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 261149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 262149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 263149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 263917ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 263917ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 264149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 264781ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/bundle.js:38066
[ 264781ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/bundle.js:38066
[ 265169ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 266149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 267149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 268149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 269149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 270149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 271149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 272149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 272565ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 272565ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 273149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 273647ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/bundle.js:38066
[ 273647ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/bundle.js:38066
[ 274149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 275149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 276149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 277149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 278149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 279149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 280149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 281149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 282149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 283149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 284149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 285149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 286149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 287149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 288150ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 289149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 290149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 290179ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/bundle.js:38066
[ 290179ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/bundle.js:38066
[ 291147ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 291147ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 291149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 292149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 293149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 294149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 295149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 296149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 297149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 298149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 299149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 300149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 301149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 302149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 303149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 304149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 305149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 306149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 307149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 308149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 309149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 310149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 311149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 312150ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 313149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 314149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 315149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 316149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 317149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 318150ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 319149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 320149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0
[ 321149ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedrequest:0

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -1,5 +1,15 @@
# Changelog # Changelog
## 2026-03-04 - 11.0.1 - fix(auth)
treat expired JWTs as no identity, improve logout and token verification flow, and bump deps
- App: getActionContext now treats expired JWTs as null to avoid using stale identities for requests.
- Logout action always clears local login state; server-side adminLogout is attempted only when a valid identity exists.
- Dashboard: verify persisted JWT with server (verifyIdentity) on startup; if verification fails, clear state and show login.
- Auto-refresh: on combined refresh failure, detect auth-related errors (invalid/unauthorized/401), dispatch logout and reload to force re-login.
- Deps: bumped devDependencies @git.zone/tstest (^3.2.0) and @git.zone/tswatch (^3.2.5); added runtime dependency @push.rocks/lik (^6.2.2).
- Tests/artifacts: added Playwright console logs and page screenshots (test artifacts) to the commit.
## 2026-03-03 - 11.0.0 - BREAKING CHANGE(opsserver) ## 2026-03-03 - 11.0.0 - BREAKING CHANGE(opsserver)
Require authentication for OpsServer endpoints, split handlers into authenticated view/admin routers, and make identity required on many TypedRequest interfaces Require authentication for OpsServer endpoints, split handlers into authenticated view/admin routers, and make identity required on many TypedRequest interfaces

View File

@@ -22,8 +22,8 @@
"@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.9.0", "@git.zone/tsbundle": "^2.9.0",
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.8", "@git.zone/tstest": "^3.2.0",
"@git.zone/tswatch": "^3.2.0", "@git.zone/tswatch": "^3.2.5",
"@types/node": "^25.3.3" "@types/node": "^25.3.3"
}, },
"dependencies": { "dependencies": {
@@ -34,6 +34,7 @@
"@apiclient.xyz/cloudflare": "^7.1.0", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.43.3", "@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.1.6",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.1.3", "@push.rocks/smartacme": "^9.1.3",

2622
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '11.0.0', version: '11.0.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '11.0.0', version: '11.0.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -238,9 +238,12 @@ interface IActionContext {
} }
const getActionContext = (): IActionContext => { const getActionContext = (): IActionContext => {
return { const identity = loginStatePart.getState().identity;
identity: loginStatePart.getState().identity, // Treat expired JWTs as no identity — prevents stale persisted sessions from firing requests
}; if (identity && identity.expiresAt && identity.expiresAt < Date.now()) {
return { identity: null };
}
return { identity };
}; };
// Login Action // Login Action
@@ -271,24 +274,23 @@ export const loginAction = loginStatePart.createAction<{
} }
}); });
// Logout Action // Logout Action — always clears state, even if identity is expired/missing
export const logoutAction = loginStatePart.createAction(async (statePartArg) => { export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
if (!context.identity) return statePartArg.getState();
// Try to notify server, but don't block logout if identity is missing/expired
if (context.identity) {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_AdminLogout interfaces.requests.IReq_AdminLogout
>('/typedrequest', 'adminLogout'); >('/typedrequest', 'adminLogout');
try { try {
await typedRequest.fire({ await typedRequest.fire({ identity: context.identity });
identity: context.identity,
});
} catch (error) { } catch (error) {
console.error('Logout error:', error); console.error('Logout error:', error);
} }
}
// Clear login state regardless // Always clear login state
return { return {
identity: null, identity: null,
isLoggedIn: false, isLoggedIn: false,
@@ -1338,6 +1340,12 @@ async function dispatchCombinedRefreshAction() {
} }
} catch (error) { } catch (error) {
console.error('Combined refresh failed:', error); console.error('Combined refresh failed:', error);
// If the error looks like an auth failure (invalid JWT), force re-login
const errMsg = String(error);
if (errMsg.includes('invalid') || errMsg.includes('unauthorized') || errMsg.includes('401')) {
await loginStatePart.dispatchAction(logoutAction, null);
window.location.reload();
}
} }
} }

View File

@@ -1,5 +1,6 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js'; import * as appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { appRouter } from '../router.js'; import { appRouter } from '../router.js';
import { import {
@@ -218,13 +219,27 @@ export class OpsDashboard extends DeesElement {
// Handle initial state - check if we have a stored session that's still valid // Handle initial state - check if we have a stored session that's still valid
const loginState = appstate.loginStatePart.getState(); const loginState = appstate.loginStatePart.getState();
if (loginState.identity?.jwt) { if (loginState.identity?.jwt) {
// Verify JWT hasn't expired
if (loginState.identity.expiresAt > Date.now()) { if (loginState.identity.expiresAt > Date.now()) {
// JWT still valid, restore logged-in state // Client-side expiry looks valid — verify with server (keypair may have changed)
try {
const verifyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_VerifyIdentity
>('/typedrequest', 'verifyIdentity');
const response = await verifyRequest.fire({ identity: loginState.identity });
if (response.valid) {
// JWT confirmed valid by server
this.loginState = loginState; this.loginState = loginState;
await simpleLogin.switchToSlottedContent(); await simpleLogin.switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null); await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null); await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
} else {
// Server rejected the JWT — clear state, show login
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
} catch {
// Server unreachable or error — clear state, show login
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
} else { } else {
// JWT expired, clear the stored state // JWT expired, clear the stored state
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);