Stop the dashboard reloading itself when it cannot authenticate - #378
Conversation
This comment has been minimized.
This comment has been minimized.
The API client answered every 401 by assigning window.location.href = "/login". On the login page that is a same-URL assignment, which reloads; the reload remounts AuthProvider, which calls /api/auth/me, which 401s, which assigns it again. Where nothing can mint a session the loop does not terminate, and the login card is only visible between reloads. Nothing has hit this yet because no deployed environment has ever reported the enforced auth mode: AUTH_BYPASS is only true when no OAuth client is configured, and while it was set /api/auth/me answered 200 to everyone. Closing the anonymous-admin bypass is what makes dash.retina.fm reach this path, and that vhost has no Cloudflare Access application in front of it by design. So: navigate only when the caller is somewhere else in the app. The comparison trims trailing slashes, because the router matches /login/ to the same route and a raw string compare would let that spelling through for one more reload. A 401 also stops being a bare Error, because AuthProvider could not tell it from the network and timeout failures its four-attempt backoff exists for, and spending those on a settled answer held the login card behind a loading state for around nine seconds. Every guard is mutation-checked one at a time: removing the path check fails the already-on-login test, removing the slash trim fails its /login/ case, and removing the break fails the settles-without-retrying test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1af189b to
cfbf601
Compare
|
Claude finished @Babissimo's task in 1m 54s —— View job Review: fix/dashboard-401-redirect-loop (PR #378)
This is an update to the PR since the last review — it addresses both concrete findings from that pass. Previously flagged, now fixed:
Still open, as noted last time (not blocking, out of the PR's stated scope):
Correctness check on the new logic: Not run: Nothing here blocks merge — #3 and #4 are the same pre-existing, low-impact, explicitly-out-of-scope items from the prior pass, now just narrower in scope since #1 and #2 are resolved. |
Prerequisite for #376. Found by auditing what depends on the anonymous-admin
bypass, not by the diff review on that PR, because nothing in the diff is wrong:
the bug is in code #376 never touches and has simply never been reached.
The loop
dashboard/src/api/client.tsanswered every 401 withwindow.location.href = "/login".AuthProviderwraps the whole router,including the
/loginroute (dashboard/src/main.tsx:13), so on the login pagethe sequence is: mount,
GET /api/auth/me, 401, assign/loginwhile already on/login, which reloads, which remounts, which calls again. Where nothing canmint a session it does not terminate, and the card is visible only between
reloads.
Why it has never happened
AUTH_BYPASSis true only when no OAuth client is configured, so the bypassbeing set in every deployed environment is also proof that none of them can issue
a session. While it was set,
/api/auth/meanswered 200 to anyone and this pathwas unreachable. #376 removes the flag, and
dash.retina.fmhas no CloudflareAccess application in front of it by design, so that vhost lands here on every
load.
It is also why no test caught it: the
oauthbranch offrontend/e2e/dashboard.spec.tshas never once executed against a deployedenvironment, every one of them having reported
bypass.The change
Navigate only when the caller is elsewhere in the app. A 401 also stops being a
bare
Error:AuthProvidercould not distinguish it from the network andtimeout failures its four-attempt backoff exists for, so a settled answer sat
behind a loading state for about nine seconds before the card appeared.
Scope is deliberately just the loop. Whether there should be a user-facing auth
story at all is open, and this does not presume one — it only makes the
unauthenticated state settle instead of thrash.
Verification
individually: removing the path check fails the already-on-login test, removing
the break fails the settles-without-retrying test, one each.
npm run typecheckclean,npm run buildclean,
pre-commit run --all-filesclean.dashboard/srckeyed off the old error message.Touches only
dashboard/, so it is disjoint from #376 and the two can land ineither order.
🤖 Generated with Claude Code