mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-27 10:44:43 +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"), {
|
const response = await fetch(getApiUrl("/api/auth/status"), {
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
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
|
// Check if response is valid JSON before parsing
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}`)
|
throw new Error(`HTTP ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get("content-type")
|
const contentType = response.headers.get("content-type")
|
||||||
if (!contentType || !contentType.includes("application/json")) {
|
if (!contentType || !contentType.includes("application/json")) {
|
||||||
throw new Error("Response is not JSON")
|
throw new Error("Response is not JSON")
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
const authenticated = data.auth_enabled ? data.authenticated : true
|
const authenticated = data.auth_enabled ? data.authenticated : true
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ export function Login({ onLogin }: LoginProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("proxmenux-auth-token", data.token)
|
localStorage.setItem("proxmenux-auth-token", data.token)
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem("proxmenux-auth-401-handled")
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
localStorage.setItem("proxmenux-saved-username", username)
|
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.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// Token is missing, expired, or signed under a previous JWT_SECRET
|
// Token is missing, expired, or signed under a previous JWT_SECRET
|
||||||
// (audit Tier 4 #22 rotates per-install). Drop the stale token and
|
// (rotated per-install). Drop the stale token and force a single
|
||||||
// bounce the user to login — the previous behavior just threw and
|
// reload so the page-level auth gate (`app/page.tsx`) can render
|
||||||
// left the dashboard stuck on a blank state. Audit Tier 2 residual.
|
// <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") {
|
if (typeof window !== "undefined") {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem("proxmenux-auth-token")
|
localStorage.removeItem("proxmenux-auth-token")
|
||||||
} catch {
|
} catch {
|
||||||
// localStorage might be unavailable in private browsing — ignore.
|
// localStorage might be unavailable in private browsing — ignore.
|
||||||
}
|
}
|
||||||
// Avoid redirect loops if we're already on the auth page.
|
try {
|
||||||
const path = window.location.pathname
|
if (!sessionStorage.getItem("proxmenux-auth-401-handled")) {
|
||||||
if (!path.startsWith("/auth") && !path.startsWith("/login")) {
|
sessionStorage.setItem("proxmenux-auth-401-handled", "1")
|
||||||
window.location.assign("/")
|
window.location.reload()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// sessionStorage unavailable — fall back to a plain reload.
|
||||||
|
window.location.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`Unauthorized: ${endpoint}`)
|
throw new Error(`Unauthorized: ${endpoint}`)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import hashlib
|
|||||||
import hmac
|
import hmac
|
||||||
import secrets
|
import secrets
|
||||||
import base64
|
import base64
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -341,6 +343,40 @@ def verify_token_full(token):
|
|||||||
return None, None
|
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):
|
def verify_token(token):
|
||||||
"""
|
"""
|
||||||
Verify a JWT token
|
Verify a JWT token
|
||||||
@@ -376,10 +412,10 @@ def verify_token(token):
|
|||||||
payload = jwt.decode(token, _get_jwt_secret(), algorithms=[JWT_ALGORITHM])
|
payload = jwt.decode(token, _get_jwt_secret(), algorithms=[JWT_ALGORITHM])
|
||||||
return payload.get('username')
|
return payload.get('username')
|
||||||
except jwt.ExpiredSignatureError:
|
except jwt.ExpiredSignatureError:
|
||||||
print("Token has expired")
|
_log_auth_failure_throttled("Token has expired")
|
||||||
return None
|
return None
|
||||||
except jwt.InvalidTokenError as e:
|
except jwt.InvalidTokenError as e:
|
||||||
print(f"Invalid token: {e}")
|
_log_auth_failure_throttled(f"Invalid token: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user