fix(web): exempt /register from auth guard so bootstrap admin can self-register
The +layout.svelte auth guard only treated /login as public, so any unauthenticated visit to /register bounced to /login?returnTo=%2Fregister. The "Register" link on the login page therefore looped back to itself, making the first-user-becomes-admin bootstrap path (handleRegister + CreateUserFirstAdminRace) unreachable in production. Extracted isPublicRoute() into web/src/lib/auth/publicRoutes.ts so the public-route set has one source of truth and is unit-testable. Test file explicitly comment-tags /register as a #376 regression guard. Closes the last gap in user-mgmt umbrella #376. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { isPublicRoute } from './publicRoutes';
|
||||
|
||||
describe('isPublicRoute', () => {
|
||||
test('/login is public — login form must render to unauthenticated visitors', () => {
|
||||
expect(isPublicRoute('/login')).toBe(true);
|
||||
});
|
||||
|
||||
// Regression guard for #376: /register was previously bounced to /login
|
||||
// by the layout guard, breaking bootstrap-admin self-registration on
|
||||
// fresh deployments. Do NOT remove /register from the public set unless
|
||||
// you have a deliberate replacement (e.g. invite-only landing page).
|
||||
test('/register is public — bootstrap admin must reach the form', () => {
|
||||
expect(isPublicRoute('/register')).toBe(true);
|
||||
});
|
||||
|
||||
test('/ is gated', () => {
|
||||
expect(isPublicRoute('/')).toBe(false);
|
||||
});
|
||||
|
||||
test('/admin/users is gated', () => {
|
||||
expect(isPublicRoute('/admin/users')).toBe(false);
|
||||
});
|
||||
|
||||
test('/settings is gated', () => {
|
||||
expect(isPublicRoute('/settings')).toBe(false);
|
||||
});
|
||||
|
||||
test('paths that merely start with /login are not public', () => {
|
||||
expect(isPublicRoute('/login/extra')).toBe(false);
|
||||
expect(isPublicRoute('/loginx')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
// Routes that the +layout.svelte auth guard must NOT redirect away from
|
||||
// when the visitor is unauthenticated. Bootstrap-admin self-registration
|
||||
// (#376) requires /register to be reachable without a session — without
|
||||
// /register here, the login page's "Register" link bounces back to /login.
|
||||
const PUBLIC_ROUTES = new Set(['/login', '/register']);
|
||||
|
||||
export function isPublicRoute(pathname: string): boolean {
|
||||
return PUBLIC_ROUTES.has(pathname);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
import { QueryClientProvider } from '@tanstack/svelte-query';
|
||||
import { queryClient } from '$lib/query/client';
|
||||
import { user } from '$lib/auth/store.svelte';
|
||||
import { isPublicRoute } from '$lib/auth/publicRoutes';
|
||||
import Shell from '$lib/components/Shell.svelte';
|
||||
import QueueDrawer from '$lib/components/QueueDrawer.svelte';
|
||||
import ToastHost from '$lib/components/ToastHost.svelte';
|
||||
@@ -27,11 +28,11 @@
|
||||
// Reactive guard: runs every time page.url or user.value changes.
|
||||
$effect(() => {
|
||||
const path = page.url.pathname;
|
||||
const isLogin = path === '/login';
|
||||
if (user.value === null && !isLogin) {
|
||||
const isPublic = isPublicRoute(path);
|
||||
if (user.value === null && !isPublic) {
|
||||
const returnTo = path + page.url.search;
|
||||
goto('/login?returnTo=' + encodeURIComponent(returnTo), { replaceState: true });
|
||||
} else if (user.value !== null && isLogin) {
|
||||
} else if (user.value !== null && isPublic) {
|
||||
goto('/', { replaceState: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user