mirror of
https://github.com/zvx-echo6/central.git
synced 2026-05-22 02:24:38 +02:00
fix(gui): dashboard polls card + CSRF exception handler
Fix A - /dashboard/polls:
- Use get_last_msg instead of pull_subscribe (no durable consumers)
- Fix subject filter: central.meta.adapter.{name}.status
- Parse correct fields: ts and ok from status message
- Handle NotFoundError gracefully when no status exists
Fix B - CSRF exception handler:
- Add global CsrfProtectError handler in __init__.py
- Return friendly "session expired" message instead of 500
- Re-render forms with error or redirect to /login
- Update templates to display error messages
Tests:
- Add get_last_msg mocking tests for polls
- Add regression test verifying no pull_subscribe
- Add CSRF handler tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4368c83613
commit
9396e5dbe8
7 changed files with 283 additions and 74 deletions
|
|
@ -136,6 +136,50 @@ def _create_app() -> FastAPI:
|
|||
# Include routes
|
||||
app.include_router(router)
|
||||
|
||||
# CSRF exception handler - return friendly error instead of 500
|
||||
from fastapi_csrf_protect.exceptions import CsrfProtectError
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
@app.exception_handler(CsrfProtectError)
|
||||
async def csrf_exception_handler(request, exc: CsrfProtectError):
|
||||
from fastapi_csrf_protect import CsrfProtect
|
||||
|
||||
csrf_protect = CsrfProtect()
|
||||
csrf_token, signed_token = csrf_protect.generate_csrf_tokens()
|
||||
|
||||
if request.url.path == "/login":
|
||||
response = templates.TemplateResponse(
|
||||
request=request,
|
||||
name="login.html",
|
||||
context={"csrf_token": csrf_token, "error": "Your session expired. Please try again."},
|
||||
)
|
||||
csrf_protect.set_csrf_cookie(signed_token, response)
|
||||
return response
|
||||
elif request.url.path == "/setup":
|
||||
response = templates.TemplateResponse(
|
||||
request=request,
|
||||
name="setup.html",
|
||||
context={"csrf_token": csrf_token, "error": "Your session expired. Please try again."},
|
||||
)
|
||||
csrf_protect.set_csrf_cookie(signed_token, response)
|
||||
return response
|
||||
elif request.url.path == "/logout":
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
elif request.url.path == "/change-password":
|
||||
response = templates.TemplateResponse(
|
||||
request=request,
|
||||
name="change_password.html",
|
||||
context={"csrf_token": csrf_token, "error": "Your session expired. Please try again."},
|
||||
)
|
||||
csrf_protect.set_csrf_cookie(signed_token, response)
|
||||
return response
|
||||
elif request.url.path.startswith("/adapters/"):
|
||||
# Redirect back to adapters list
|
||||
return RedirectResponse("/adapters", status_code=302)
|
||||
else:
|
||||
# Fallback: redirect to login
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ async def dashboard_streams(request: Request) -> HTMLResponse:
|
|||
async def dashboard_polls(request: Request) -> HTMLResponse:
|
||||
"""Get last poll times for each adapter."""
|
||||
from central.gui.nats import get_js
|
||||
from nats.js.errors import NotFoundError
|
||||
|
||||
templates = _get_templates()
|
||||
pool = get_pool()
|
||||
|
|
@ -210,43 +211,31 @@ async def dashboard_polls(request: Request) -> HTMLResponse:
|
|||
else:
|
||||
for name in adapter_names:
|
||||
try:
|
||||
# Get last message from CENTRAL_META for this adapter
|
||||
sub = await js.pull_subscribe(
|
||||
f"central.meta.{name}.status",
|
||||
durable=f"dashboard-poll-{name}",
|
||||
stream="CENTRAL_META",
|
||||
msg = await js.get_last_msg(
|
||||
"CENTRAL_META",
|
||||
f"central.meta.adapter.{name}.status",
|
||||
)
|
||||
try:
|
||||
msgs = await sub.fetch(1, timeout=1.0)
|
||||
if msgs:
|
||||
data = json.loads(msgs[0].data.decode())
|
||||
last_poll = data.get("data", {}).get("time", "—")
|
||||
adapters.append({
|
||||
"name": name,
|
||||
"last_poll": last_poll,
|
||||
"status": "✓",
|
||||
"error": None,
|
||||
})
|
||||
else:
|
||||
adapters.append({
|
||||
"name": name,
|
||||
"last_poll": None,
|
||||
"status": None,
|
||||
"error": None,
|
||||
})
|
||||
except Exception:
|
||||
adapters.append({
|
||||
"name": name,
|
||||
"last_poll": None,
|
||||
"status": None,
|
||||
"error": None,
|
||||
})
|
||||
except Exception:
|
||||
data = json.loads(msg.data.decode())
|
||||
adapters.append({
|
||||
"name": name,
|
||||
"last_poll": data.get("ts"),
|
||||
"status": "✓" if data.get("ok") else "✗",
|
||||
"error": data.get("error") if not data.get("ok") else None,
|
||||
})
|
||||
except NotFoundError:
|
||||
# No status message for this adapter yet
|
||||
adapters.append({
|
||||
"name": name,
|
||||
"last_poll": None,
|
||||
"status": None,
|
||||
"error": "unavailable",
|
||||
"error": None,
|
||||
})
|
||||
except Exception as e:
|
||||
adapters.append({
|
||||
"name": name,
|
||||
"last_poll": None,
|
||||
"status": "?",
|
||||
"error": str(e),
|
||||
})
|
||||
|
||||
return templates.TemplateResponse(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
<h1>Change Password</h1>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="/change-password" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
<h1>Login</h1>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="/login" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
<p>Create the initial operator account to get started.</p>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="/setup" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue