When `PRIVATE_MODE=true`, every API path except a small allowlist
(`/health`, `/auth/{config,login,logout,register}`) requires a valid
session cookie or bearer token — anonymous reads are rejected with
401. Self-registration is force-disabled in private mode regardless
of `ALLOW_SELF_REGISTER`, so a locked-down instance flips with a
single switch (admins still mint accounts via `POST /admin/users`).
The backend gate is a tower middleware that reuses the existing
`CurrentUser` extractor, so the cookie + bearer paths cannot drift
from per-handler auth. `/auth/config` now exposes the flag plus the
effective `self_register_enabled` value so the frontend can render
the navbar correctly on the first paint.
On the frontend, a new universal root `+layout.ts` fetches the
config and redirects anonymous visitors to `/login?next=<path>`
before page-specific loads fire. The redirect is UX only — the
backend middleware is the source of truth, so crafted requests
still 401.
Defaults stay public (`PRIVATE_MODE=false`); existing deployments
need no env change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
42 lines
1.6 KiB
TypeScript
42 lines
1.6 KiB
TypeScript
// Universal root load. Surfaces /auth/config to every page so the
|
|
// navbar + layout can render without an extra round-trip, and — when
|
|
// the backend reports PRIVATE_MODE=true — bounces anonymous visitors
|
|
// to /login before any page-specific load fires. The backend
|
|
// middleware is still the source of truth for the gate; this just
|
|
// matches the UX so users don't see a page full of failed fetches.
|
|
import type { LayoutLoad } from './$types';
|
|
import { redirect } from '@sveltejs/kit';
|
|
import { getAuthConfig, me, type AuthConfig } from '$lib/api/auth';
|
|
|
|
// Paths reachable anonymously even when private_mode is on. /login is
|
|
// the entry point of the auth flow; everything else (including
|
|
// /register, which is force-blocked in private mode) bounces.
|
|
const PRIVATE_MODE_BYPASS = new Set(['/login']);
|
|
|
|
const PUBLIC_DEFAULTS: AuthConfig = {
|
|
self_register_enabled: true,
|
|
private_mode: false
|
|
};
|
|
|
|
export const load: LayoutLoad = async ({ url }) => {
|
|
let authConfig: AuthConfig = PUBLIC_DEFAULTS;
|
|
try {
|
|
authConfig = await getAuthConfig();
|
|
} catch {
|
|
// Fail-soft: keep the optimistic public-mode defaults so a
|
|
// backend hiccup doesn't lock anyone out of the login page.
|
|
// No private data can leak through here — the backend
|
|
// middleware is still authoritative for the gate.
|
|
}
|
|
|
|
if (authConfig.private_mode && !PRIVATE_MODE_BYPASS.has(url.pathname)) {
|
|
const user = await me().catch(() => null);
|
|
if (!user) {
|
|
const next = url.pathname + url.search;
|
|
redirect(302, `/login?next=${encodeURIComponent(next)}`);
|
|
}
|
|
}
|
|
|
|
return { authConfig };
|
|
};
|