Production deploy hit a chicken-and-egg bootstrap blocker: the +layout.svelte auth guard only treated /login as a public route, so any unauthenticated visit to /register got bounced to /login?returnTo=%2Fregister and the "Register" link on the login page looped back to itself. The first-user-becomes-admin path (handleRegister + CreateUserFirstAdminRace) was therefore unreachable without direct DB surgery.
This PR adds /register to the public-route allowlist and closes the last gap on user-mgmt umbrella #376.
Changes
Newweb/src/lib/auth/publicRoutes.ts — isPublicRoute() helper with /login + /register as the public set. Single source of truth so future auth-guard changes don't require touching +layout.svelte.
Updatedweb/src/routes/+layout.svelte — guard now calls isPublicRoute(path) instead of an inline path === '/login' check.
Newweb/src/lib/auth/publicRoutes.test.ts — six unit tests, including an explicit comment-tagged regression guard for the /register case so this can't silently regress.
Test plan
CI runs vitest on publicRoutes.test.ts
Operator: pull the resulting image, browse to https://minstrel.fabledsword.com/register while logged out, register a fresh user, confirm is_admin = true on first user
Operator: with a session, browse to /register and confirm the redirect to / still fires (authenticated user shouldn't see the form)
Notes
Production also needs HTTPS (or localhost) for the SPA to load at all — crypto.randomUUID is gated to secure contexts and SvelteKit's client runtime calls it. Operator already moved minstrel.fabledsword.com behind TLS while diagnosing this; documenting here for the next operator.
Fable #376 marked done with closeout notes covering shipped surface, the SMTP-blocked forgot-password carve-out, and this regression.
## Summary
Production deploy hit a chicken-and-egg bootstrap blocker: the `+layout.svelte` auth guard only treated `/login` as a public route, so any unauthenticated visit to `/register` got bounced to `/login?returnTo=%2Fregister` and the "Register" link on the login page looped back to itself. The first-user-becomes-admin path (`handleRegister` + `CreateUserFirstAdminRace`) was therefore unreachable without direct DB surgery.
This PR adds `/register` to the public-route allowlist and closes the last gap on user-mgmt umbrella **#376**.
## Changes
- **New** `web/src/lib/auth/publicRoutes.ts` — `isPublicRoute()` helper with `/login` + `/register` as the public set. Single source of truth so future auth-guard changes don't require touching `+layout.svelte`.
- **Updated** `web/src/routes/+layout.svelte` — guard now calls `isPublicRoute(path)` instead of an inline `path === '/login'` check.
- **New** `web/src/lib/auth/publicRoutes.test.ts` — six unit tests, including an explicit comment-tagged regression guard for the `/register` case so this can't silently regress.
## Test plan
- [x] CI runs vitest on `publicRoutes.test.ts`
- [ ] Operator: pull the resulting image, browse to `https://minstrel.fabledsword.com/register` while logged out, register a fresh user, confirm `is_admin = true` on first user
- [ ] Operator: with a session, browse to `/register` and confirm the redirect to `/` still fires (authenticated user shouldn't see the form)
## Notes
- Production also needs HTTPS (or `localhost`) for the SPA to load at all — `crypto.randomUUID` is gated to secure contexts and SvelteKit's client runtime calls it. Operator already moved `minstrel.fabledsword.com` behind TLS while diagnosing this; documenting here for the next operator.
- Fable #376 marked done with closeout notes covering shipped surface, the SMTP-blocked forgot-password carve-out, and this regression.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Production deploy hit a chicken-and-egg bootstrap blocker: the
+layout.svelteauth guard only treated/loginas a public route, so any unauthenticated visit to/registergot bounced to/login?returnTo=%2Fregisterand the "Register" link on the login page looped back to itself. The first-user-becomes-admin path (handleRegister+CreateUserFirstAdminRace) was therefore unreachable without direct DB surgery.This PR adds
/registerto the public-route allowlist and closes the last gap on user-mgmt umbrella #376.Changes
web/src/lib/auth/publicRoutes.ts—isPublicRoute()helper with/login+/registeras the public set. Single source of truth so future auth-guard changes don't require touching+layout.svelte.web/src/routes/+layout.svelte— guard now callsisPublicRoute(path)instead of an inlinepath === '/login'check.web/src/lib/auth/publicRoutes.test.ts— six unit tests, including an explicit comment-tagged regression guard for the/registercase so this can't silently regress.Test plan
publicRoutes.test.tshttps://minstrel.fabledsword.com/registerwhile logged out, register a fresh user, confirmis_admin = trueon first user/registerand confirm the redirect to/still fires (authenticated user shouldn't see the form)Notes
localhost) for the SPA to load at all —crypto.randomUUIDis gated to secure contexts and SvelteKit's client runtime calls it. Operator already movedminstrel.fabledsword.combehind TLS while diagnosing this; documenting here for the next operator.