hooks.server.ts + app.d.ts: handle hook for cookie auth + admin role gate (consumes #14) #15
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Type
Feature
Lineage
Decomposed from
forgejo_admin/westside-admin#2(Keycloak cookie SSR auth + admin role gate). Sub-task 2 of 4. Depends onforgejo_admin/westside-admin#14(keycloak.ts lib) — must land first.Repo
forgejo_admin/westside-adminUser Story
story-westside-admin-admin-row-crud. The handle hook is the per-request enforcement point: every authenticated request reaches the handle hook before any data-bearing route runs. This is where the funnel-auth gate is enforced.Context
The handle hook reads the encrypted session cookie, decrypts it via #14's
decryptCookiePayload, validates the embedded JWT via #14'sverifyKeycloakJwt, checksrealm_access.rolesincludesadmin, and populatesevent.locals.user. Anonymous requests (no cookie) get redirected to/auth/login(which #16 builds). Authenticated-but-not-admin users get rendered the(unauthorized)403 page (which #17 builds) without redirect.The hook also performs lazy token refresh: if the embedded
expis within 30s of now, it calls #14'srefreshTokensIfNeededand re-encrypts the new tokens into the cookie before continuing the request.File Targets
Create:
src/hooks.server.ts— thehandleexport (only —handleErroris out of scope here).Update:
src/app.d.ts— extendApp.Localswithuser: { sub: string; email: string; name: string; roles: string[] }.Do NOT touch:
src/lib/server/keycloak.ts(sub-task 1, frozen at this point).src/routes/auth/*(sub-task 3 — the hook redirects to these endpoints but doesn't define them).src/routes/(unauthorized)/*(sub-task 4 — the hook renders this route group on missing-admin, doesn't define the page).Acceptance Criteria
/auth/*,/(unauthorized), and/health302s to/auth/login?redirect={original_url}./healthrequests pass through unauthenticated and reach the existingsrc/routes/health/handler — k8s liveness/readiness probes cannot authenticate, so blocking them would break the deployment's probe gating.refreshTokensIfNeededthrowsTokenRefreshErrormid-request (refresh token expired, Keycloak refused), treat the request as anonymous: clear the session cookie viaevent.cookies.delete('westside_admin_session', { path: '/' })and 302 to/auth/login?redirect={original_url}. Do NOT 5xx — the user just needs to log in again.event.locals.user.(unauthorized)route group (does NOT redirect — user stays at the URL they hit, sees 403).exp - now < 30s, hook callsrefreshTokensIfNeeded, re-encrypts cookie, sets new Set-Cookie before continuing.HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=<refresh_expiry_seconds>.event.locals.usertyped end-to-end (noanycasts).Test Expectations
/with no cookie → expect 302 to/auth/login?redirect=%2F./with a tampered cookie → expect 302 to/auth/login(treated as anonymous).(unauthorized)page.npm run dev+ manual cookie injection viacurl -b cookie.txt. (No vitest target — wire this up only when sub-task 4 lands and we have an end-to-end fixture.)Constraints
event.cookies.get('westside_admin_session')andevent.cookies.set(...)— do NOT manually parseCookie:headers.event.localsfor user object — do NOT stash onglobalThisor module-level state.westside_admin_session(matches #14's expectation and the spec in #2).event.url.pathname = ...rewrite orawait resolve(event, { transformPageChunk: ... })— NOT a 302 redirect (per AC)./auth/*,/(unauthorized), and/health. The/healthexclusion is mandatory — k8s probes (defined inpal-e-deployments/overlays/westside-admin/prod/deployment-patch.yaml) hit/healthwithout auth.import { ... } from '$lib/server/keycloak';.Checklist
feedback_funnel_requires_authRelated
project-westside-adminforgejo_admin/westside-admin#2— parentforgejo_admin/westside-admin#14— keycloak.ts lib (DEPENDS ON, must merge first)arch-dataflow-westside-admin— Flow 1feedback_funnel_requires_authScope Review: APPROVED
Review note:
review-1135-2026-05-03Scope is tight, file targets are accurate against post-#14 ground truth (PR #18 merged today), and all three claimed lib exports (
verifyKeycloakJwt,decryptCookiePayload,refreshTokensIfNeeded) verified to exist insrc/lib/server/keycloak.tsonmain(HEAD982df5b). Env vars merged viapal-e-deployments#147. No decomposition needed (1 create + 1 update, ~60-100 LoC, 5-8 min agent run).Three small
[BODY]items — none block dispatch:[BODY](should-fix) Resolve the/healthroute question —src/routes/healthexists; the exclusion list (/auth/*,/(unauthorized)) does not name it. Recommend adding/healthas a third exclusion, OR Ava routes the answer in the dev-agent prompt directly (30-sec clarification).[BODY](optional) MakeTokenRefreshErrormid-request behavior explicit (treat-as-anonymous; currently inferable from AC #4).[BODY](optional, cosmetic) Tighten the cookie-name attribution — the lib is cookie-name-agnostic by design, so "matches #14's expectation" is slightly off; the hook is where the constant lives.[SCOPE]carry-over fromreview-1134-2026-05-03:arch-keycloaknote still doesn't exist;arch-dataflow-westside-adminFlow 1 covers the surface this ticket touches. Ava's call whether to author now or defer.Sequencing: #15 unblocked NOW. #16/#17 can run in parallel — do-not-touch fences between siblings are clean; full E2E manual ACs need all four landed.