Update AppImage

This commit is contained in:
MacRimi
2026-05-09 23:22:45 +02:00
parent 748334eed6
commit 0288c14a29
6 changed files with 84 additions and 13 deletions
Binary file not shown.
@@ -1 +1 @@
774332ee537b6c24caadc0a2f8ad5c8952f4a07d7c875ec04e6a310383371c09 /tmp/ProxMenux-1.2.1.1-beta.AppImage
e1a6ae02b7a8dc65cf31c6b3fecfd3719531377adc0978a12f949edbf3cac8e3 /tmp/ProxMenux-1.2.1.1-beta.AppImage
+24 -3
View File
@@ -29,17 +29,38 @@ export default function Home() {
const response = await fetch(getApiUrl("/api/auth/status"), {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
// 401 here means the token is present but invalid — typically signed
// under a previous jwt_secret (rotated on AppImage upgrade or fresh
// install). If we let this fall into the catch below, the dashboard
// would render and every authenticated component would fire its own
// 401 in parallel, flooding the backend logs and looping reloads.
// Drop the dead token and force the Login screen instead.
if (response.status === 401) {
try {
localStorage.removeItem("proxmenux-auth-token")
} catch {
// private browsing — best-effort
}
setAuthStatus({
loading: false,
authEnabled: true,
authConfigured: true,
authenticated: false,
})
return
}
// Check if response is valid JSON before parsing
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const contentType = response.headers.get("content-type")
if (!contentType || !contentType.includes("application/json")) {
throw new Error("Response is not JSON")
}
const data = await response.json()
const authenticated = data.auth_enabled ? data.authenticated : true
+5
View File
@@ -76,6 +76,11 @@ export function Login({ onLogin }: LoginProps) {
}
localStorage.setItem("proxmenux-auth-token", data.token)
try {
sessionStorage.removeItem("proxmenux-auth-401-handled")
} catch {
// ignore
}
if (rememberMe) {
localStorage.setItem("proxmenux-saved-username", username)
+16 -7
View File
@@ -92,19 +92,28 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
if (!response.ok) {
if (response.status === 401) {
// Token is missing, expired, or signed under a previous JWT_SECRET
// (audit Tier 4 #22 rotates per-install). Drop the stale token and
// bounce the user to login — the previous behavior just threw and
// left the dashboard stuck on a blank state. Audit Tier 2 residual.
// (rotated per-install). Drop the stale token and force a single
// reload so the page-level auth gate (`app/page.tsx`) can render
// <Login> instead of cascading 401s from every authenticated
// component on mount. The sessionStorage flag is essential: a
// page like Hardware/Storage fires 10-20 SWR fetches in parallel,
// and without dedup each of them would race to reload the tab —
// observed in the wild as ~180 "Invalid token" log lines per
// second from a single browser running an upgraded Monitor.
if (typeof window !== "undefined") {
try {
localStorage.removeItem("proxmenux-auth-token")
} catch {
// localStorage might be unavailable in private browsing — ignore.
}
// Avoid redirect loops if we're already on the auth page.
const path = window.location.pathname
if (!path.startsWith("/auth") && !path.startsWith("/login")) {
window.location.assign("/")
try {
if (!sessionStorage.getItem("proxmenux-auth-401-handled")) {
sessionStorage.setItem("proxmenux-auth-401-handled", "1")
window.location.reload()
}
} catch {
// sessionStorage unavailable — fall back to a plain reload.
window.location.reload()
}
}
throw new Error(`Unauthorized: ${endpoint}`)
+38 -2
View File
@@ -14,6 +14,8 @@ import hashlib
import hmac
import secrets
import base64
import threading
import time
from datetime import datetime, timedelta
from pathlib import Path
@@ -341,6 +343,40 @@ def verify_token_full(token):
return None, None
_AUTH_LOG_RATE = {'last_ts': 0.0, 'suppressed': 0, 'last_msg': ''}
_AUTH_LOG_LOCK = threading.Lock()
def _log_auth_failure_throttled(msg):
"""Log a JWT verification failure at most once every 30 seconds.
A browser whose token was invalidated by a jwt_secret rotation can
fire dozens of authenticated requests per page load (SWR fetches +
WebSocket reconnects); without throttling this floods the journal
with hundreds of identical 'Invalid token: Signature verification
failed' lines per second and stalls journald. We keep the first
occurrence verbatim and emit one summary line every 30s with the
suppressed count, so the operator still has visibility of the
issue without the cascade.
"""
now = time.time()
with _AUTH_LOG_LOCK:
elapsed = now - _AUTH_LOG_RATE['last_ts']
if elapsed >= 30:
if _AUTH_LOG_RATE['suppressed']:
print(f"[auth] {_AUTH_LOG_RATE['last_msg']} "
f"(+{_AUTH_LOG_RATE['suppressed']} more in last "
f"{int(elapsed)}s)")
else:
print(f"[auth] {msg}")
_AUTH_LOG_RATE['last_ts'] = now
_AUTH_LOG_RATE['suppressed'] = 0
_AUTH_LOG_RATE['last_msg'] = msg
else:
_AUTH_LOG_RATE['suppressed'] += 1
_AUTH_LOG_RATE['last_msg'] = msg
def verify_token(token):
"""
Verify a JWT token
@@ -376,10 +412,10 @@ def verify_token(token):
payload = jwt.decode(token, _get_jwt_secret(), algorithms=[JWT_ALGORITHM])
return payload.get('username')
except jwt.ExpiredSignatureError:
print("Token has expired")
_log_auth_failure_throttled("Token has expired")
return None
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
_log_auth_failure_throttled(f"Invalid token: {e}")
return None