Add SvelteKit route /jersey-public — System B frontend #243

Closed
opened 2026-04-10 21:51:31 +00:00 by forgejo_admin · 1 comment
Contributor

Type

Feature

Lineage

Standalone — part of System B (public jersey intake) production rollout. Playground prototype at forgejo_admin/westside-playground#57. Architecture in arch-jersey-intake. Revised 2026-04-10 per feedback_funnel_requires_auth.md — all new funnel-exposed PII surfaces must have documented auth. Reuses existing westside-landing Keycloak flow.

Repo

forgejo_admin/westside-landing

User Story

As Marcus
I want a Keycloak-gated /jersey-public page on the westside-landing site
So that any player Marcus shares the link with can sign in, fill the form (with name and email prefilled from their Keycloak identity), and submit a jersey order

Context

westside-landing already has full Keycloak auth wired in production:

  • Realm: westside-basketball, client: westside-spa (PKCE public client)
  • src/lib/keycloak.js — existing initKeycloak(), isAuthenticated(), getUserName(), login(), etc.
  • src/routes/(app)/+layout.svelte — runs initKeycloak() on mount, has reactive $effect guard: if (!authenticated && !isPublic) goto('/signin')
  • PUBLIC_APP_ROUTES allowlist inside (app)/+layout.svelte — routes that bypass the auth guard (currently: /register, /signin, /jersey, /jersey/success, /jersey/cancel, /checkout, /checkout/success, /checkout/cancel, /forgot-password, /reset-password)

This ticket puts /jersey-public inside (app)/ and DOES NOT add it to PUBLIC_APP_ROUTES. Result: unauthenticated visitors auto-redirect to /signin, complete Keycloak login (including self-registration if enabled on the realm), then bounce back to /jersey-public with a valid session. No new auth layer, no oauth2-proxy, no ingress changes.

The playground page is at /home/ldraney/westside-playground/jersey-public.html (438 lines) and has been visually approved by Lucas on 2026-04-10. Promotion follows feedback_svelte_is_html.md — literal copy-paste plus Svelte 5 $state runes.

File Targets

Files to create:

  • src/routes/(app)/jersey-public/+page.svelte — the new route. Copy form HTML structure from playground. Convert inline <script> to Svelte 5 $state runes for form state. Bind on:submit|preventDefault. POST to /api/jersey-public-orders with JWT in Authorization: Bearer <token> header.

Files to verify/reference (do NOT modify unless strictly required):

  • src/routes/(app)/+layout.svelte — auth guard lives here. Do NOT add /jersey-public to PUBLIC_APP_ROUTES. Read it to understand the pattern.
  • src/lib/keycloak.js — import getToken(), getUserName(), getEmail() (add the getter if it doesn't exist, matching existing style)

Files the agent should NOT touch:

  • src/routes/(public)/* — any public (unauth) route
  • src/routes/(app)/checkout/*, src/routes/(app)/jersey/* — Systems A and C, hands off
  • Any other route

Prefill from JWT

On mount, the form must:

  1. Await ready from $lib/keycloak.js
  2. Read JWT claims via existing helpers (getUserName() for player_name, getEmail() for email — add getEmail() helper to $lib/keycloak.js if missing; it's a 3-line getter that reads keycloak.tokenParsed?.email)
  3. Prefill the playerName and email $state fields
  4. Leave these fields editable in case the user wants to submit on behalf of someone else (e.g., parent filling for child)

Acceptance Criteria

  • When I visit /jersey-public unauthenticated, I am redirected to /signin
  • After signing in, I land back on /jersey-public with my player name and email prefilled from the JWT
  • When I toggle Kings/Queens, both card images swap to the correct MinIO URLs
  • When I submit with any required field missing, the form blocks and shows inline errors
  • When I submit a valid form, the request goes to POST /api/jersey-public-orders with Authorization: Bearer <JWT> header (verify in DevTools)
  • Success/error UI states match the playground's behavior
  • Mobile layout matches playground on iPhone
  • /jersey-public is NOT in PUBLIC_APP_ROUTES — verify by grep

Test Expectations

  • Unit test: form validation blocks required-field submission
  • Unit test: K/Q toggle swaps both image src attributes
  • Unit test: prefills name + email from mocked JWT claims on mount
  • Component test (if framework supports): unauthenticated render triggers goto('/signin') via the layout guard
  • Run command: npm run test (or westside-landing's test runner)

Constraints

  • Reuse existing $lib/keycloak.js helpers — do NOT roll new auth code
  • Route MUST live under (app)/ — do NOT add to PUBLIC_APP_ROUTES
  • Use Svelte 5 $state runes
  • No Tailwind (feedback_no_tailwind.md)
  • Mobile-first
  • Do not modify System A or System C routes
  • POST body is JSON; Authorization: Bearer <token> header required

Checklist

  • PR opened against westside-landing main
  • Tailscale preview verified locally via npm run dev
  • Auth chain documented in PR body (per feedback_funnel_requires_auth.md)
  • /jersey-public confirmed absent from PUBLIC_APP_ROUTES
  • Tests pass
  • westside-basketball — project
  • story:WS-S31 — admin public jersey intake link
  • arch-jersey-intake — architecture doc
  • forgejo_admin/westside-playground#57 — playground prototype
  • feedback_svelte_is_html.md — promotion philosophy
  • feedback_funnel_requires_auth.md — why this is Keycloak-gated
  • forgejo_admin/westside-playground#59 — Keycloak-gated forms spike (answered "yes" by this ticket)
### Type Feature ### Lineage Standalone — part of System B (public jersey intake) production rollout. Playground prototype at `forgejo_admin/westside-playground#57`. Architecture in `arch-jersey-intake`. **Revised 2026-04-10** per `feedback_funnel_requires_auth.md` — all new funnel-exposed PII surfaces must have documented auth. Reuses existing westside-landing Keycloak flow. ### Repo `forgejo_admin/westside-landing` ### User Story As Marcus I want a Keycloak-gated `/jersey-public` page on the westside-landing site So that any player Marcus shares the link with can sign in, fill the form (with name and email prefilled from their Keycloak identity), and submit a jersey order ### Context westside-landing already has full Keycloak auth wired in production: - Realm: `westside-basketball`, client: `westside-spa` (PKCE public client) - `src/lib/keycloak.js` — existing `initKeycloak()`, `isAuthenticated()`, `getUserName()`, `login()`, etc. - `src/routes/(app)/+layout.svelte` — runs `initKeycloak()` on mount, has reactive `$effect` guard: `if (!authenticated && !isPublic) goto('/signin')` - `PUBLIC_APP_ROUTES` allowlist inside `(app)/+layout.svelte` — routes that bypass the auth guard (currently: `/register`, `/signin`, `/jersey`, `/jersey/success`, `/jersey/cancel`, `/checkout`, `/checkout/success`, `/checkout/cancel`, `/forgot-password`, `/reset-password`) This ticket puts `/jersey-public` **inside** `(app)/` and **DOES NOT** add it to `PUBLIC_APP_ROUTES`. Result: unauthenticated visitors auto-redirect to `/signin`, complete Keycloak login (including self-registration if enabled on the realm), then bounce back to `/jersey-public` with a valid session. No new auth layer, no oauth2-proxy, no ingress changes. The playground page is at `/home/ldraney/westside-playground/jersey-public.html` (438 lines) and has been visually approved by Lucas on 2026-04-10. Promotion follows `feedback_svelte_is_html.md` — literal copy-paste plus Svelte 5 `$state` runes. ### File Targets Files to create: - `src/routes/(app)/jersey-public/+page.svelte` — the new route. Copy form HTML structure from playground. Convert inline `<script>` to Svelte 5 `$state` runes for form state. Bind `on:submit|preventDefault`. POST to `/api/jersey-public-orders` with JWT in `Authorization: Bearer <token>` header. Files to verify/reference (do NOT modify unless strictly required): - `src/routes/(app)/+layout.svelte` — auth guard lives here. Do NOT add `/jersey-public` to `PUBLIC_APP_ROUTES`. Read it to understand the pattern. - `src/lib/keycloak.js` — import `getToken()`, `getUserName()`, `getEmail()` (add the getter if it doesn't exist, matching existing style) Files the agent should NOT touch: - `src/routes/(public)/*` — any public (unauth) route - `src/routes/(app)/checkout/*`, `src/routes/(app)/jersey/*` — Systems A and C, hands off - Any other route ### Prefill from JWT On mount, the form must: 1. Await `ready` from `$lib/keycloak.js` 2. Read JWT claims via existing helpers (`getUserName()` for player_name, `getEmail()` for email — add `getEmail()` helper to `$lib/keycloak.js` if missing; it's a 3-line getter that reads `keycloak.tokenParsed?.email`) 3. Prefill the `playerName` and `email` `$state` fields 4. Leave these fields editable in case the user wants to submit on behalf of someone else (e.g., parent filling for child) ### Acceptance Criteria - [ ] When I visit `/jersey-public` unauthenticated, I am redirected to `/signin` - [ ] After signing in, I land back on `/jersey-public` with my player name and email prefilled from the JWT - [ ] When I toggle Kings/Queens, both card images swap to the correct MinIO URLs - [ ] When I submit with any required field missing, the form blocks and shows inline errors - [ ] When I submit a valid form, the request goes to `POST /api/jersey-public-orders` with `Authorization: Bearer <JWT>` header (verify in DevTools) - [ ] Success/error UI states match the playground's behavior - [ ] Mobile layout matches playground on iPhone - [ ] `/jersey-public` is NOT in `PUBLIC_APP_ROUTES` — verify by grep ### Test Expectations - [ ] Unit test: form validation blocks required-field submission - [ ] Unit test: K/Q toggle swaps both image src attributes - [ ] Unit test: prefills name + email from mocked JWT claims on mount - [ ] Component test (if framework supports): unauthenticated render triggers goto('/signin') via the layout guard - [ ] Run command: `npm run test` (or westside-landing's test runner) ### Constraints - Reuse existing `$lib/keycloak.js` helpers — do NOT roll new auth code - Route MUST live under `(app)/` — do NOT add to `PUBLIC_APP_ROUTES` - Use Svelte 5 `$state` runes - No Tailwind (`feedback_no_tailwind.md`) - Mobile-first - Do not modify System A or System C routes - POST body is JSON; `Authorization: Bearer <token>` header required ### Checklist - [ ] PR opened against `westside-landing` main - [ ] Tailscale preview verified locally via `npm run dev` - [ ] Auth chain documented in PR body (per `feedback_funnel_requires_auth.md`) - [ ] `/jersey-public` confirmed absent from `PUBLIC_APP_ROUTES` - [ ] Tests pass ### Related - `westside-basketball` — project - `story:WS-S31` — admin public jersey intake link - `arch-jersey-intake` — architecture doc - `forgejo_admin/westside-playground#57` — playground prototype - `feedback_svelte_is_html.md` — promotion philosophy - `feedback_funnel_requires_auth.md` — why this is Keycloak-gated - `forgejo_admin/westside-playground#59` — Keycloak-gated forms spike (answered "yes" by this ticket)
Author
Contributor

Scope Review: APPROVED

Review note: review-946-2026-04-10

All template sections present, traceability triangle intact (story:WS-S31 verified on project-westside-basketball user-stories, arch-jersey-intake note exists and names westside-landing/src/routes/jersey-public as the exact System B frontend target, Forgejo issue #243 open). File targets verified against the live repo via Forgejo API: src/routes/(app)/ exists, jersey-public does NOT exist, src/lib/keycloak.js exports match (initKeycloak/getToken/getUserName present, tokenParsed referenced, getEmail correctly flagged as absent), (app)/+layout.svelte has the $effect guard and PUBLIC_APP_ROUTES allowlist matches the ticket body verbatim. Playground prototype verified at 438 lines.

No decomposition needed (1 new file + 3-line helper, 1 repo, single cohesive feature). No scope leak into System A or System C — both explicitly marked do-not-touch. Auth chain cleanly reuses existing Keycloak infrastructure with no new layer.

Soft deps: #947 (migration, wave:1, parallel) and #948 (POST endpoint, wave:2). Neither blocks frontend development — endpoint can be stubbed for local dev.

Discovered scope (not blocking, create as new ticket)

  • [SCOPE] Update arch-jersey-intake System B section to reflect Keycloak-gated identity model per feedback_funnel_requires_auth.md (arch doc currently describes System B as self-declared-identity; the ticket revision moves it to JWT claims).

Verdict: APPROVED — ready to move backlog → todo.

## Scope Review: APPROVED Review note: `review-946-2026-04-10` All template sections present, traceability triangle intact (story:WS-S31 verified on project-westside-basketball user-stories, arch-jersey-intake note exists and names `westside-landing/src/routes/jersey-public` as the exact System B frontend target, Forgejo issue #243 open). File targets verified against the live repo via Forgejo API: `src/routes/(app)/` exists, `jersey-public` does NOT exist, `src/lib/keycloak.js` exports match (initKeycloak/getToken/getUserName present, tokenParsed referenced, getEmail correctly flagged as absent), `(app)/+layout.svelte` has the $effect guard and PUBLIC_APP_ROUTES allowlist matches the ticket body verbatim. Playground prototype verified at 438 lines. No decomposition needed (1 new file + 3-line helper, 1 repo, single cohesive feature). No scope leak into System A or System C — both explicitly marked do-not-touch. Auth chain cleanly reuses existing Keycloak infrastructure with no new layer. Soft deps: #947 (migration, wave:1, parallel) and #948 (POST endpoint, wave:2). Neither blocks frontend development — endpoint can be stubbed for local dev. ### Discovered scope (not blocking, create as new ticket) - `[SCOPE]` Update `arch-jersey-intake` System B section to reflect Keycloak-gated identity model per `feedback_funnel_requires_auth.md` (arch doc currently describes System B as self-declared-identity; the ticket revision moves it to JWT claims). **Verdict: APPROVED — ready to move backlog → todo.**
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
ldraney/westside-app#243
No description provided.