import { dev } from '$app/environment'; import { decodePathname } from '$lib/server/auth/decode-path'; import { SAFE_NEXT } from '$lib/server/auth/safe-next'; import { SESSION_COOKIE, verifySessionToken } from '$lib/server/auth/session'; import { getApp } from '$lib/server/app'; import { error, redirect, type Handle } from '@sveltejs/kit'; const PUBLIC_PATHS = new Set(['/login']); const ALLOWED_WHILE_PASSWORD_CHANGE = new Set(['/me/password', '/logout']); export const handle: Handle = async ({ event, resolve }) => { const app = await getApp(); event.locals.app = app; event.locals.user = null; const token = event.cookies.get(SESSION_COOKIE); if (token) { const payload = verifySessionToken(token, app.config.sessionSecret); const user = payload ? app.users.byId(payload.uid) : undefined; if (payload && user && user.active && user.sessionVersion === payload.sv) { event.locals.user = user; } else { event.cookies.delete(SESSION_COOKIE, { path: '/' }); } } // SvelteKit matches routes on the decoded pathname, so every check below must use it too: // gating on the raw pathname lets `/%61dmin` slip past the admin check and still hit /admin. let path: string; try { path = decodePathname(event.url.pathname); } catch { error(400, 'Malformed URL'); } const user = event.locals.user; if (!user) { if (!PUBLIC_PATHS.has(path)) { const fullPath = event.url.pathname + event.url.search; const next = SAFE_NEXT.test(path) ? `?next=${encodeURIComponent(fullPath)}` : ''; redirect(303, `/login${next}`); } } else { if (path === '/login') redirect(303, '/'); if (user.mustChangePassword && !ALLOWED_WHILE_PASSWORD_CHANGE.has(path)) redirect(303, '/me/password'); if ((path === '/admin' || path.startsWith('/admin/')) && user.role !== 'admin') error(403, 'Admins only'); if (event.request.method !== 'GET' && event.request.method !== 'HEAD' && path !== '/logout') { app.syncReadOnly(); if (!app.guard.canWrite) error(503, 'Read-only: another instance of the app is currently active. Try again in a couple of minutes.'); } } const response = await resolve(event); response.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('Referrer-Policy', 'same-origin'); if (!dev) response.headers.set('X-Frame-Options', 'DENY'); return response; };