Files
website/src/hooks.server.ts

69 lines
3.5 KiB
TypeScript

import type { Handle, HandleServerError, ServerInit } from '@sveltejs/kit/hooks';
import { startMusicPoller } from '#lib/server/music/poller.js';
import { startGamesPoller } from '#lib/server/games/poller.js';
import { building } from '$app/env';
import { auth } from '#lib/server/auth.js';
import { isOwner, isOwnerOnlyPath, isOwnerWritePath } from '#lib/server/owner.js';
import { error } from '@sveltejs/kit';
const analytics = /<!-- analytics -->[\s\S]*?<!-- \/analytics -->/;
// Background jobs that live in the app process: the Spotify scrobbler (idle until connected)
// and the CS2 / PUBG / R6 match history poller.
export const init: ServerInit = () => {
if (!building) {
startMusicPoller();
startGamesPoller();
}
};
export const handle: Handle = async ({ event, resolve }) => {
// Match the endpoint path here: the dev proxy may terminate HTTPS before Vite.
// Better Auth validates request origins against its configured public URL.
if (!building && event.url.pathname.startsWith('/api/auth/')) return auth.handler(event.request);
const session = building ? null : await auth.api.getSession({ headers: event.request.headers });
event.locals.user = session?.user ?? null;
event.locals.session = session?.session ?? null;
// Owner-only areas (health tracker, book files, reader and book APIs). Page loads,
// __data.json and endpoints 404 for non-owners in the route itself (layout load /
// endpoint), which renders the same 404 page as an unknown URL. As a backstop, any other
// method (form actions, which skip layout loads) is refused here before a route runs,
// including writes to the otherwise public /trackers/books pages.
const ownerOnly = isOwnerOnlyPath(event.url.pathname);
const owner = isOwner(event);
const read = event.request.method === 'GET' || event.request.method === 'HEAD';
if (!owner && ((ownerOnly && !read) || (!read && isOwnerWritePath(event.url.pathname))))
error(404, 'Not Found');
// Private pages (the /start new-tab dashboard, trackers) skip Google Analytics: less to
// load, and nothing private gets reported.
const response = await resolve(
event,
ownerOnly ? { transformPageChunk: ({ html }) => html.replace(analytics, '') } : undefined
);
if (ownerOnly) {
response.headers.set('x-robots-tag', 'noindex, nofollow, noarchive');
// Keep a stricter value a route set itself (the /start/live stream adds no-transform).
if (!response.headers.get('cache-control')?.includes('no-store'))
response.headers.set('cache-control', 'private, no-store');
}
return response;
};
// Runs for every error (SvelteKit 3 passes a `kind`). Unknown errors (bugs, failing services)
// get a short id that the error page shows and the log line starts with ("[error ab12cd34]"),
// so a screenshot or report can be matched to the stack trace; the client only gets the
// generic "Internal Error", never the stack. 404s get one compact line (Caddy keeps no access
// log), other error() calls are expected and stay quiet. Query strings are never logged.
export const handleError: HandleServerError = (input) => {
const { event } = input;
const where = `${event.request.method} ${event.url.pathname} route=${event.route.id ?? '(none)'}`;
if (input.kind === 'unknown') {
const id = crypto.randomUUID().slice(0, 8);
console.error(`[error ${id}] 500 ${where}`, input.error);
return { id };
}
if (input.error.status === 404) console.warn(`[404] ${where}`);
else if (input.error.status >= 500)
console.error(`[${input.error.status}] ${where} ${input.error.message}`);
};