feat: jersey-public SvelteKit route with Keycloak gate (#243) #246
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ldraney/westside-app!246
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "243-jersey-public-svelte-route"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds the System B public jersey intake route at
/jersey-publicinside the(app)/route group. The route inherits the existing Keycloak auth guard from(app)/+layout.svelte— no new auth code, no oauth2-proxy, no ingress changes.Auth Chain (per
feedback_funnel_requires_auth.md)/jersey-public(app)/+layout.svelterunsinitKeycloak()on mount (check-sso, PKCE)$effectguard checksPUBLIC_APP_ROUTES—/jersey-publicis intentionally NOT in that listgoto('/signin')which triggers Keycloak login (realmwestside-basketball, clientwestside-spa)/jersey-publicwith a valid sessionpreferred_username+emailclaims viagetUserName()/getEmail()and prefillsplayerName+email(still editable so parents can submit for kids)POST /api/jersey-public-orderswithAuthorization: Bearer <token>fromgetToken()Changes
src/routes/(app)/jersey-public/+page.svelte(new, ~450 lines) — Svelte 5 runes, mobile-first, pure CSS. Ports the approved playground prototype (forgejo_admin/westside-playground#57) to SvelteKit. K/Q toggle swaps both jersey card images via$derivedURLs. Tier radio wraps the whole.jersey-optioncard. Inline validation withjp-errorbanners. Status banner for submit result. Handles 404 gracefully — shows a "backend not yet deployed, text Marcus" message rather than a raw error since the API endpoint lands in a separate PR.src/lib/keycloak.js— adds 3-linegetEmail()helper matching the style of the existinggetUserId()/getUserName()getters.Acceptance Criteria
/jersey-publicunauthenticated -> redirected to/signin(inherited from(app)/+layout.svelteguard)/api/jersey-public-orderswithAuthorization: Bearer <JWT>/jersey-publicis NOT inPUBLIC_APP_ROUTES— verified by grep$staterunes, no Tailwind, reuses existing.jersey-*classes fromsrc/app.cssVerification Greps
Test Plan
npm run check-> 0 errors in new files (existing a11y warnings in admin/teams and checkout are pre-existing and unrelated)npm run build-> builds cleanly,.svelte-kit/output/server/entries/pages/(app)/jersey-public/_page.svelte.jsgenerated (11.62 kB)npm run dev, visit/jersey-publicunauthenticated -> redirects to/signin/jersey-publicwith name + email prefilled/api/jersey-public-orderswithAuthorization: Bearerheader (DevTools Network tab); 404 shows friendly messageReview Checklist
(app)/and is NOT added toPUBLIC_APP_ROUTES$state/$derivedrunes$lib/keycloak.jshelpers (ready,getUserName,getEmail,getToken) — no new auth codegetEmail()helper added to$lib/keycloak.jsnpm run checkandnpm run buildboth green(public)/jersey) and System C ((app)/checkout) per ticket scopeFollow-up Scope (not in this PR)
POST /api/jersey-public-orderslands in a separate PR (#430 per the ticket)marcus-kings/marcus-queens/otherwith aTODOcomment — switch to/api/teamswhen backend wires uppackage.jsonscripts are onlydev,build,preview,check). Adding Vitest is out of scope for this ticket and likely belongs in a separate infra ticket.Related
Closes #243
Related Notes
story:WS-S31— admin public jersey intake linkarch-jersey-intake— architecture docforgejo_admin/westside-playground#57(approved prototype)feedback_funnel_requires_auth.md,feedback_svelte_is_html.md,feedback_no_tailwind.mdPR #246 Review
DOMAIN REVIEW
Stack: SvelteKit 5 (runes:
$state,$derived), pure CSS, Keycloak JS adapter. Single new route(app)/jersey-public/+page.svelte(539 lines) + 8-linegetEmail()helper insrc/lib/keycloak.js.Auth chain verified:
src/routes/(app)/+layout.svelte:26—PUBLIC_APP_ROUTESarray checked against my local main. Contains/register,/signin,/jersey,/jersey/success,/jersey/cancel,/checkout,/checkout/success,/checkout/cancel,/forgot-password,/reset-password. Does NOT contain/jersey-public— confirmed. The reactive$effectguard at(app)/+layout.svelte:46-62willgoto('/signin')for unauthenticated visitors. Auth chain is sound and matchesfeedback_funnel_requires_auth.md.getEmail()helper:Functionally correct. Minor style drift from the existing
getUserName()/getUserId()pattern which uses an earlyif (!keycloak?.tokenParsed) return '';guard before destructuring — but the optional-chaining one-liner is equivalent and arguably cleaner. Non-blocking nit.getToken()usage:const token = await getToken();then spreadsAuthorization: Bearer ${token}conditionally. Matches thePromise<string|null>signature atsrc/lib/keycloak.js:104. Token refresh (updateToken(30)) is handled insidegetToken(). Correct.System A / System C scope isolation: PR metadata shows
changed_files: 2. Onlysrc/lib/keycloak.jsand the new(app)/jersey-public/+page.svelte. No modifications to(public)/jersey,(app)/jersey,(app)/checkout, or(app)/+layout.svelte. Clean scope.Form validation (+page.svelte:58-78):
playerName,email(w/ regex),team,kq,topSize,shortSize,tiernum1,num2,num3— validated against/^(0|00|[1-9][0-9]?)$/only when non-emptyerrors.numbersflag for all three number fieldsEMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/— permissive but sufficienttrim()applied on compare and on payloadMatches spec.
K/Q image swap (+page.svelte:14-23 + 43-44): 4 MinIO URLs (kings-home, kings-away-new, queens-home, queens-away-new) in
JERSEY_IMAGESconstant.$derivedswaps both reversible and shooter images based onkq. Fallback?? JERSEY_IMAGES.kings.reversible. Verified 4 URLs matchminio-api.tail5b443a.ts.net/assets/westside/jerseys/.Submit (+page.svelte:96-157):
(385) 450-9963(number is used consistently across 7 other files in the repo)body.message || body.detail, falls back toSubmission failed (${status}).console.errorfor dev visibilityAccessibility:
for/idlabel pairsrole="radiogroup"+aria-label="Kings or Queens"(+page.svelte:218)<label class="jp-tier-click">— full card is the click targetrole="status"+aria-live="polite"(+page.svelte:406)autocomplete="name",autocomplete="email",inputmode="email",inputmode="numeric"all setgrid-template-columns: 1frat base,1fr 1fr@ 640px+)Security:
getToken()at submit time onlystatusMessage{@html}anywhereconsole.erroron catch is the only logging — acceptableBLOCKERS
None.
NITS
.jersey-*class dependency unverified — The<style>block references:global(.jersey-option input[name='tier'])and:global(.jersey-option.is-selected). The template uses.jersey-option,.jersey-options,.jersey-form-group,.jersey-form-label,.jersey-select,.jersey-option-featured,.jersey-option-badge,.jersey-option-img,.jersey-option-body,.jersey-option-header,.jersey-option-name,.jersey-option-price,.jersey-option-desc, plus.btn,.btn-primary,.btn-block,.container. PR body claims "reuses existing.jersey-option/.jersey-form-groupclasses fromsrc/app.css," but on my localmaincheckout of the sibling repo, onlyjersey-number-input,jersey-number-input-wrap,jersey-number-input-error, andjersey-number-hintappear (all used inside(app)/jersey/+page.svelte, not defined globally).npm run buildpassing does NOT prove these classes exist — unused global selectors render unstyled silently. Manual viewport QA is mandatory (already in the Test Plan checklist, just flagging it's load-bearing, not cosmetic).src/routes/(app)/jersey-public/+page.svelte:188—<option value="">Select a team…</option>is notdisabled, while the size selects (lines 276, 294) use<option value="" disabled>. Inconsistent UX. Non-blocking since form validation catches empty string.src/lib/keycloak.js:143-149(newgetEmail) — uses one-liner optional chaining instead of the early-return guard pattern ofgetUserId()/getUserName(). Functionally equivalent. Consider matching style in a future touch-up.+page.svelte:135—console.error('jersey-public submit failed:', err)— fine for now but consider a telemetry hook once observability wires up.+page.svelte:192-195with<!-- TODO: populate from /api/teams ... -->. Already tracked as follow-up per PR body. Not a blocker.SOP COMPLIANCE
243-jersey-public-svelte-route)Closes #243linking presentWS-S31), arch (arch-jersey-intake), board item#946, playground prototype, and relevant conventions (feedback_funnel_requires_auth.md,feedback_svelte_is_html.md,feedback_no_tailwind.md)feedback_funnel_requires_auth.md@apply,bg-*,text-*,flex-*not present — pure CSS vars + explicit selectors)$state,$derived,$propsnot needed here)#430called out as deferredPROCESS OBSERVATIONS
feedback_funnel_requires_auth.mdasks for. Future public-route PRs should be held to this standard..jersey-*global classes render on the deployed route. If they don't, this page will look broken even though build and check pass.VERDICT: APPROVED