Auth UI: register, login, logout (incl. POST /api/register) #52

Closed
opened 2026-06-26 00:03:41 +02:00 by myrmidex · 1 comment
Owner

Context

The cookie-session auth backend exists (#30: json_login on /api/login, firewall logout on /api/logout, /api/me), but there is no frontend for it and no register endpoint yet. User is deliberately off API Platform (#30 decision), so register needs a small dedicated endpoint.

This is a full-stack vertical slice: the register backend + all three auth screens. It sits between #33 (SPA scaffold) and the scenario/bucket content tickets — you can't reach a scenario page without being logged in.

Scope

Backend

  • POST /api/register — create a User (email + password), hash via the configured password hasher, reject duplicate email (validation, not 500). NOT via API Platform (User stays off it) — a small dedicated controller/action, JSON in/out, returns 201 or 422. Pin: happy-path 201, duplicate-email 422, missing/invalid fields 422, weak/short password rejected.

Frontend (depends on #33 scaffold: router + API client + auth context)

  • Register screen → POST /api/register, then auto-login or redirect to login.
  • Login screen → POST /api/login (json_login), store auth state in context, redirect to scenario list.
  • LogoutPOST /api/logout (firewall, returns 204), clear auth context, redirect to login.
  • Auth-guarded routing: unauthenticated users hitting an app route → redirect to login; authenticated users hitting login/register → redirect to app.

Notes / gotchas

  • Auth is cookie-session, NOT JWT — no token storage; the session cookie is the auth. /api/me is the "am I logged in?" probe.
  • Writes require Content-Type: application/ld+json for API Platform resources, but /api/login//api/register are NOT AP resources — confirm their expected content-type (json_login takes application/json).
  • SameSite=Lax + content-type enforcement is the CSRF story (see PLATFORM.md) — no CSRF token UI needed.

Out of scope

  • Password reset / email verification (future).
  • Remember-me, social login.

Testing

  • Backend: TDD per the register endpoint (functional + the validation matrix).
  • Frontend: per the SPA's test setup (established in #33).
## Context The cookie-session auth backend exists (#30: `json_login` on `/api/login`, firewall logout on `/api/logout`, `/api/me`), but there is **no frontend** for it and **no register endpoint** yet. `User` is deliberately off API Platform (#30 decision), so register needs a small dedicated endpoint. This is a **full-stack vertical slice**: the register backend + all three auth screens. It sits between #33 (SPA scaffold) and the scenario/bucket content tickets — you can't reach a scenario page without being logged in. ## Scope ### Backend - `POST /api/register` — create a `User` (email + password), hash via the configured password hasher, reject duplicate email (validation, not 500). NOT via API Platform (User stays off it) — a small dedicated controller/action, JSON in/out, returns 201 or 422. Pin: happy-path 201, duplicate-email 422, missing/invalid fields 422, weak/short password rejected. ### Frontend (depends on #33 scaffold: router + API client + auth context) - **Register** screen → `POST /api/register`, then auto-login or redirect to login. - **Login** screen → `POST /api/login` (json_login), store auth state in context, redirect to scenario list. - **Logout** → `POST /api/logout` (firewall, returns 204), clear auth context, redirect to login. - Auth-guarded routing: unauthenticated users hitting an app route → redirect to login; authenticated users hitting login/register → redirect to app. ## Notes / gotchas - Auth is cookie-session, NOT JWT — no token storage; the session cookie is the auth. `/api/me` is the "am I logged in?" probe. - Writes require `Content-Type: application/ld+json` for API Platform resources, but `/api/login`/`/api/register` are NOT AP resources — confirm their expected content-type (json_login takes `application/json`). - SameSite=Lax + content-type enforcement is the CSRF story (see PLATFORM.md) — no CSRF token UI needed. ## Out of scope - Password reset / email verification (future). - Remember-me, social login. ## Testing - Backend: TDD per the register endpoint (functional + the validation matrix). - Frontend: per the SPA's test setup (established in #33).
myrmidex added this to the v0.3.0 milestone 2026-06-26 00:03:41 +02:00
myrmidex added the
enhancement
label 2026-06-26 00:03:41 +02:00
myrmidex self-assigned this 2026-06-26 00:03:41 +02:00
myrmidex added a new dependency 2026-06-26 00:11:23 +02:00
myrmidex added a new dependency 2026-06-26 00:11:24 +02:00
Author
Owner

Resolved on release/v0.3.0 (15 commits — a full-stack vertical slice). pr-reviewer (full-ticket diff): Request Changes → the one blocker fixed → green.

Backend

POST /api/register — dedicated plain controller (User stays off API Platform) + RegisterRequest DTO: NotBlank/Email/Length + #[UniqueEntity] (duplicate email → 422, not a DB 500). Manual ValidatorInterface{errors:{field:[msgs]}} 422 / 201 {id,email} (never the hash), no session established. Absent-key guard (?? '') keeps a missing field a 422, not a 500. Reachable unauthenticated. 10 functional tests.

Frontend (the auth experience + the patterns #53–#59 build on)

  • Test harness: Vitest + RTL + MSW (the TDD loop), set up here.
  • AuthProvider (split context/provider/hook for Fast Refresh) — probes /api/me, login()/logout() flip/clear auth state; logout clears regardless of network outcome.
  • Route guards (RequireAuth/RequireGuest as layout routes via <Outlet/>) — loading→null no-flash; redirect via state-flip, not imperative nav.
  • Data routercreateBrowserRouter route-objects in router.tsx (real config smoke-tested via createMemoryRouter).
  • Login / Register / Logout screens — login reuses context, register posts directly + navigates to /login (no auto-login), per-field 422 errors. Cross-links between login↔register. Split layouts (bare guest vs authed-with-logout).
  • Reusable primitives: Input, FieldError + parseFieldErrors (shared field-error pattern), api.postJson (application/json — json_login rejects ld+json).

E2E

Containerized Playwright (host-run failed on NixOS → mcr.microsoft.com/playwright image as a profile-gated compose service, targets frontend:5173, gated on a frontend healthcheck). One register→login→logout smoke test, green.

Infra

npm pinned via a single NPM_VERSION in root .env → both dev Dockerfiles. dev-fe-* / dev-e2e / dev-check / dev-check-all shell helpers.

238 backend tests + 33 frontend tests + the e2e all green; PHPStan + oxlint + Prettier clean.

Spawned: #62 (coverage gaps + threshold + enable TS strict), #63 (rename backend dev-*dev-be-*), and notes on #35 (php healthcheck + e2e-in-CI + prod APP_DEBUG).

Resolved on `release/v0.3.0` (15 commits — a full-stack vertical slice). pr-reviewer (full-ticket diff): Request Changes → the one blocker fixed → green. ## Backend `POST /api/register` — dedicated plain controller (`User` stays off API Platform) + `RegisterRequest` DTO: `NotBlank`/`Email`/`Length` + `#[UniqueEntity]` (duplicate email → 422, not a DB 500). Manual `ValidatorInterface` → `{errors:{field:[msgs]}}` 422 / `201 {id,email}` (never the hash), no session established. Absent-key guard (`?? ''`) keeps a missing field a 422, not a 500. Reachable unauthenticated. 10 functional tests. ## Frontend (the auth experience + the patterns #53–#59 build on) - **Test harness:** Vitest + RTL + MSW (the TDD loop), set up here. - **AuthProvider** (split context/provider/hook for Fast Refresh) — probes `/api/me`, `login()`/`logout()` flip/clear auth state; logout clears regardless of network outcome. - **Route guards** (`RequireAuth`/`RequireGuest` as layout routes via `<Outlet/>`) — `loading→null` no-flash; redirect via state-flip, not imperative nav. - **Data router** — `createBrowserRouter` route-objects in `router.tsx` (real config smoke-tested via `createMemoryRouter`). - **Login / Register / Logout** screens — login reuses context, register posts directly + navigates to `/login` (no auto-login), per-field 422 errors. Cross-links between login↔register. Split layouts (bare guest vs authed-with-logout). - **Reusable primitives:** `Input`, `FieldError` + `parseFieldErrors` (shared field-error pattern), `api.postJson` (application/json — json_login rejects ld+json). ## E2E Containerized Playwright (host-run failed on NixOS → `mcr.microsoft.com/playwright` image as a profile-gated compose service, targets `frontend:5173`, gated on a frontend healthcheck). One register→login→logout smoke test, green. ## Infra npm pinned via a single `NPM_VERSION` in root `.env` → both dev Dockerfiles. `dev-fe-*` / `dev-e2e` / `dev-check` / `dev-check-all` shell helpers. 238 backend tests + 33 frontend tests + the e2e all green; PHPStan + oxlint + Prettier clean. Spawned: #62 (coverage gaps + threshold + enable TS strict), #63 (rename backend `dev-*`→`dev-be-*`), and notes on #35 (php healthcheck + e2e-in-CI + prod APP_DEBUG).
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.

Reference: lvl0/buckets#52
No description provided.