Auth UI: register, login, logout pages #67

Closed
opened 2026-06-15 13:50:16 +00:00 by james · 0 comments
Owner

The backend for register / login / logout / sessions / rate-limit / first-user-is-admin all landed under #11, but there is no UI for any of it — only API routes and app/page.tsx documenting the endpoints. A self-hoster who isn't comfortable driving curl against /api/auth/register has no way to create their account.

This ticket covers the minimum UI to make the existing backend usable by a human.

Scope

  • /register page (app/register/page.tsx) — email + password + confirm form. Submits to the existing POST /api/auth/register. Surfaces the documented error codes as human copy: invalid_email, invalid_password, email_taken, and the 503 policy_not_implemented case (closed registration).
  • /login page (app/login/page.tsx) — email + password form. Submits to POST /api/auth/login. Surfaces wrong-credentials and rate-limited responses as actionable copy, not raw JSON.
  • Logout — a button or link in the authenticated shell that POSTs to /api/auth/logout and redirects the browser to /login.
  • Routing glue:
    • Add /login and /register to lib/auth/public-routes.ts with PR-justified entries (matches the existing convention from #10 / ADR-0004).
    • proxy.ts redirects unauthenticated browser navigation to a protected route → /login?next=<original>. Unauthenticated API requests keep returning 401 (the policy from #10 is unchanged for non-HTML).
    • After successful login or registration, redirect to ?next= if it's set and same-origin; otherwise to a default landing page.
    • An already-authenticated user hitting /login or /register is redirected to the default landing, not shown the form again.
  • Landing page (app/page.tsx) — when there is no session, link to /login and /register. When there is a session, link to the authenticated app shell.

Acceptance criteria

  • A first-time user on a fresh DB can register through /register, ends up signed in (the existing helper sets is_admin=1 on the first user — no separate UI for that today), and lands on the post-registration destination.
  • A second user can register on the same instance; their session is theirs, isolated from User A.
  • An existing user can sign in through /login and land on the post-login destination.
  • Logout from the authenticated shell ends the session and redirects to /login. Hitting back on the browser does not re-authenticate.
  • Unauthenticated browser navigation to a protected route redirects to /login?next=.... The same path requested as a JSON API still returns 401 with no body redirect.
  • After successful login, a same-origin ?next= is honoured; cross-origin or absolute external URLs are rejected.
  • Wrong-credentials, rate-limited, email_taken, invalid_password, and policy_not_implemented all render as human copy (not raw API JSON), and the form keeps the typed email so the user doesn't retype it.
  • A new entry in lib/auth/public-routes.ts for each new public page, justified in the PR description.

Out of scope

  • OAuth provider buttons / sign-in-with flow. Belongs with whichever ticket wires the OAuth providers up (epic #1's exit criterion mentions OAuth2 but the UI button surface is downstream of that wiring).
  • Password reset / forgotten password.
  • Email verification.
  • Magic-link / passwordless sign-in.
  • Admin-only UI surfaces. The first user does get is_admin=1, but surfacing admin actions in the UI is a separate ticket once the admin role grants meaningful power.
  • Server-rendered no-JS form fallback. The PWA target implies a JS-driven app; degrade gracefully but don't aim for full no-JS parity in v0.

Composes with

  • #11 (closed) — backend register / login / logout / sessions / rate-limit. This ticket is the UI half; the existing API contract is the integration surface.
  • #10 (closed) — authorization middleware. The browser-redirect-vs-API-401 split extends that policy without weakening it; the proxy still defaults to 401 for everything that isn't an HTML navigation.
  • #18 (closed) — PWA shell. The login / register pages need to render reasonably inside the installed PWA, not only on a fresh tab.

Part of epic #1.

The backend for register / login / logout / sessions / rate-limit / first-user-is-admin all landed under #11, but there is no UI for any of it — only API routes and `app/page.tsx` documenting the endpoints. A self-hoster who isn't comfortable driving `curl` against `/api/auth/register` has no way to create their account. This ticket covers the minimum UI to make the existing backend usable by a human. ## Scope - `/register` page (`app/register/page.tsx`) — email + password + confirm form. Submits to the existing `POST /api/auth/register`. Surfaces the documented error codes as human copy: `invalid_email`, `invalid_password`, `email_taken`, and the 503 `policy_not_implemented` case (closed registration). - `/login` page (`app/login/page.tsx`) — email + password form. Submits to `POST /api/auth/login`. Surfaces wrong-credentials and rate-limited responses as actionable copy, not raw JSON. - Logout — a button or link in the authenticated shell that `POST`s to `/api/auth/logout` and redirects the browser to `/login`. - **Routing glue:** - Add `/login` and `/register` to `lib/auth/public-routes.ts` with PR-justified entries (matches the existing convention from #10 / ADR-0004). - `proxy.ts` redirects unauthenticated **browser navigation** to a protected route → `/login?next=<original>`. Unauthenticated **API** requests keep returning 401 (the policy from #10 is unchanged for non-HTML). - After successful login or registration, redirect to `?next=` if it's set and same-origin; otherwise to a default landing page. - An already-authenticated user hitting `/login` or `/register` is redirected to the default landing, not shown the form again. - Landing page (`app/page.tsx`) — when there is no session, link to `/login` and `/register`. When there is a session, link to the authenticated app shell. ## Acceptance criteria - [ ] A first-time user on a fresh DB can register through `/register`, ends up signed in (the existing helper sets `is_admin=1` on the first user — no separate UI for that today), and lands on the post-registration destination. - [ ] A second user can register on the same instance; their session is theirs, isolated from User A. - [ ] An existing user can sign in through `/login` and land on the post-login destination. - [ ] Logout from the authenticated shell ends the session and redirects to `/login`. Hitting back on the browser does not re-authenticate. - [ ] Unauthenticated browser navigation to a protected route redirects to `/login?next=...`. The same path requested as a JSON API still returns 401 with no body redirect. - [ ] After successful login, a same-origin `?next=` is honoured; cross-origin or absolute external URLs are rejected. - [ ] Wrong-credentials, rate-limited, `email_taken`, `invalid_password`, and `policy_not_implemented` all render as human copy (not raw API JSON), and the form keeps the typed email so the user doesn't retype it. - [ ] A new entry in `lib/auth/public-routes.ts` for each new public page, justified in the PR description. ## Out of scope - OAuth provider buttons / sign-in-with flow. Belongs with whichever ticket wires the OAuth providers up (epic #1's exit criterion mentions OAuth2 but the UI button surface is downstream of that wiring). - Password reset / forgotten password. - Email verification. - Magic-link / passwordless sign-in. - Admin-only UI surfaces. The first user does get `is_admin=1`, but surfacing admin actions in the UI is a separate ticket once the admin role grants meaningful power. - Server-rendered no-JS form fallback. The PWA target implies a JS-driven app; degrade gracefully but don't aim for full no-JS parity in v0. ## Composes with - #11 (closed) — backend `register` / `login` / `logout` / sessions / rate-limit. This ticket is the UI half; the existing API contract is the integration surface. - #10 (closed) — authorization middleware. The browser-redirect-vs-API-401 split extends that policy without weakening it; the proxy still defaults to 401 for everything that isn't an HTML navigation. - #18 (closed) — PWA shell. The login / register pages need to render reasonably inside the installed PWA, not only on a fresh tab. Part of epic #1.
james closed this issue 2026-06-16 13:17:47 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#67
No description provided.