Spaces:
Running
Running
assafvayner HF Staff
fix(app): 503 on storage failures, validated currency, lock release on shutdown, per-IP login limit, unit-only npm test
e830ad4 | 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; | |
| }; | |