Update appimage

This commit is contained in:
MacRimi
2026-05-18 17:47:25 +02:00
parent c13601cd2d
commit 4bedeb9fcd
6 changed files with 52 additions and 25 deletions
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
78dee82f335a54bd82a22f778de7e5391272b91c28bc7cfd1bffcd9091c62395 /tmp/ProxMenux-1.2.1.1-beta.AppImage
a1a3e1d1f028a837efc34ef3507a03a8a899e244824381ef3ae3b3e60a5a8396 /tmp/ProxMenux-1.2.1.1-beta.AppImage
+15
View File
@@ -65,6 +65,21 @@ export default function Home() {
const authenticated = data.auth_enabled ? data.authenticated : true
// Clear the 401 cascade-prevention flag when we successfully end
// up in the authenticated state. The flag is meant to dedupe a
// burst of 401s during a single page load; once we've confirmed
// the user is in, a future 401 (token rotation, restart, etc.)
// should be allowed to reload again. Without this, a stale flag
// can prevent the post-2FA dashboard from recovering from any
// transient 401 and leaves the UI blocked.
if (authenticated) {
try {
sessionStorage.removeItem("proxmenux-auth-401-handled")
} catch {
// private browsing — best-effort
}
}
setAuthStatus({
loading: false,
authEnabled: data.auth_enabled,
+15
View File
@@ -26,6 +26,21 @@ export function Login({ onLogin }: LoginProps) {
const [loading, setLoading] = useState(false)
useEffect(() => {
// The Login screen is, by construction, the recovery path from any
// 401 cascade (the api-config wrapper redirects here when an
// expired/invalid JWT is detected). Clear the cascade-prevention
// flag on mount so a successful login can subsequently fire a fresh
// reload if a NEW 401 ever occurs. Without this clear, any 401 set
// earlier in the session sticks around forever and the next 401
// (e.g. mid-2FA, or right after a successful login if the token was
// briefly stale) is silently swallowed by the de-dup — the user
// sees a blank/stuck dashboard.
try {
sessionStorage.removeItem("proxmenux-auth-401-handled")
} catch {
// private browsing — best-effort
}
const savedUsername = localStorage.getItem("proxmenux-saved-username")
const savedPassword = localStorage.getItem("proxmenux-saved-password")
+20 -5
View File
@@ -95,12 +95,27 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
// (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.
// component on mount.
//
// Only react when we actually had a token to invalidate. A 401
// without any token in localStorage means the caller is the
// Login screen itself, or a leftover fetch from a recently
// unmounted Dashboard — reloading there does nothing but waste
// the user's keystrokes and can leave the cascade flag set
// forever, swallowing the very 401 that we'd want to recover
// from after a successful re-login. The fix: bail out early
// if we have no token to invalidate.
if (typeof window !== "undefined") {
let hadToken = false
try {
hadToken = !!localStorage.getItem("proxmenux-auth-token")
} catch {
// private browsing — assume yes so we attempt recovery.
hadToken = true
}
if (!hadToken) {
throw new Error(`Unauthorized: ${endpoint}`)
}
try {
localStorage.removeItem("proxmenux-auth-token")
} catch {
+1 -19
View File
@@ -191,24 +191,6 @@ def _bad_request(msg: str):
return jsonify({'error': msg}), 400
def _is_loopback_addr(value: str) -> bool:
"""Return True for IPv4, IPv6 and IPv4-mapped loopback addresses.
When Flask is bound to ``::`` for dual-stack support, an HTTP request
sent to ``127.0.0.1`` can be reported as ``::ffff:127.0.0.1``. Treat it
as local so the PVE webhook keeps the intended localhost trust path.
"""
try:
import ipaddress
addr = ipaddress.ip_address(value)
if addr.is_loopback:
return True
ipv4_mapped = getattr(addr, 'ipv4_mapped', None)
return bool(ipv4_mapped and ipv4_mapped.is_loopback)
except ValueError:
return value == 'localhost'
def _validate_event_type(value: str) -> bool:
return isinstance(value, str) and bool(_EVENT_TYPE_RE.match(value))
@@ -1243,7 +1225,7 @@ def proxmox_webhook():
_reject = lambda code, error, status: (jsonify({'accepted': False, 'error': error}), status)
client_ip = request.remote_addr or ''
is_localhost = _is_loopback_addr(client_ip)
is_localhost = client_ip in ('127.0.0.1', '::1')
# CSRF defence-in-depth: reject `application/x-www-form-urlencoded`
# bodies. PVE always sends `application/json`; form-encoded bodies