Replace the web UI's sign-in form with auth-gate states - #23
Merged
Conversation
web-ui never authenticates anyone — the portal does, over OIDC — yet the SPA rendered a credential form for every 401. In production that form could not sign anyone in: resolveIdentity ignores the webuiuser cookie once CORE_SIGNING_SECRET is set, so Continue POSTed to the portal, took a 401, and api() surfaced the server's raw string as the error. The user saw the words "sign in" in red with no way forward. The surface now renders a state with one action instead of a form, and the server says which state applies rather than the client inferring it: - The 401 and /me bodies carry mode: "portal" | "dev", derived from the single COOKIE_AUTH const that resolveIdentity itself now reads. Prod can no longer render the dev form. - Portal mode: "Your session ended" with one button to /auth/login, carrying returnTo so the user lands back where they were. No field. - Unreachable: "We couldn't reach the assistant" with Try again. This state did not exist — main.ts ended in a bare void boot() and boot() only special-cased exactly 401, so a 502 or a dropped connection was a blank page. - Dev mode: keeps the field, but says why it exists and names the env var that turns it off. A DEV chip and a persistent ochre top banner mark the instance as unauthenticated for as long as the session lasts. POST /signin now 404s outside cookie-auth mode, and a rejected principal gets a message naming WEB_UI_PRINCIPALS instead of "not allowed". Sign out in portal mode ends the portal session rather than clearing a cookie the server ignores. The impersonation banner generalises to .top-banner / .layout.bannered, since the dev banner is the second caller. Form mechanics: a real label instead of placeholder-as-label, type=email with autocomplete=username so password managers stop offering a password fill, role=alert on the error, and a pending guard against double submits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
web-ui never authenticates anyone — the portal does, over OIDC — yet the SPA rendered a credential form for every 401.
In production that form could not sign anyone in.
resolveIdentityignores thewebuiusercookie onceCORE_SIGNING_SECRETis set, so Continue POSTed to the portal, took a 401, andapi()surfaced the server's raw string as the error text. The user saw the words "sign in" in red, with no way forward. The actual remedy was to reload, which nothing told them.What changes
A surface that cannot authenticate anyone should render a state with one action, not a form — which is what the admin surface already does (
plugins/admin/public/index.html:3776). And the server, not the client, decides which state applies.mode: "portal" | "dev"rides on the 401 and/mebodies, derived from oneCOOKIE_AUTHconst thatresolveIdentityitself now reads. Prod can no longer render the dev form./auth/logincarryingreturnToso the user lands back where they were. No field.main.tsended in a barevoid boot()andboot()only special-cased exactly 401, so a 502 or a dropped connection was a blank page with a console error.Net effect is less code: the form, its submit handler, and the
POST /signinround-trip survive only on the dev path.Adjacent fixes
Each of these would have left the change incoherent if skipped:
/auth/logout. It was clearing a cookie the server ignores — a no-op in production, which would have looked especially broken next to a new "Your session ended" screen.POST /signin404s outside cookie-auth mode instead of setting a cookie that gets ignored.WEB_UI_PRINCIPALSinstead ofnot allowed.WEB_UI_PRINCIPALSno longer fires in portal mode, where no principal can cookie-sign-in at all./signoutadded to vite's proxy list — a pre-existing gap found while testing; sign-out was silently broken on the standalone-vite dev path..impersonation-bannergeneralises to.top-banner/.layout.bannered, now that the dev banner is a second caller.Form mechanics, while in here: a real
<label>instead of placeholder-as-label,type="email"withautocomplete="username"so password managers stop offering a password fill,role="alert"on the error, and a pending guard against double submits.Screenshots
All five states captured against a running instance, with the env flipped to produce each one:
https://claude.ai/code/artifact/73ed970f-5f4c-4813-90c3-919eeb6b530c
Verification
Exercised live on
localhost:8096(real server, embedded vite): sign in → banner → sign out → gate; the 403 rejected-principal error; mode flipping toportalunderCORE_SIGNING_SECRET;/signin404ing outside dev; and Try again recovering once the server came back.430 web-ui tests pass. Repo typecheck, both web-ui tsconfigs, and eslint clean.
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.