mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-13 20:45:01 +00:00
Update AppImage
This commit is contained in:
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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user