splitwise / src /hooks.server.ts
assafvayner's picture
assafvayner HF Staff
fix(app): 503 on storage failures, validated currency, lock release on shutdown, per-IP login limit, unit-only npm test
e830ad4
Raw
History Blame Contribute Delete
2.33 kB
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;
};