import * as Sentry from '@sentry/sveltekit'; import { redirect, type Handle, type HandleFetch } from '@sveltejs/kit'; import { paraglideMiddleware } from '$lib/paraglide/server'; import { sequence } from '@sveltejs/kit/hooks'; import { env } from 'process'; import { cookieName, cookieMaxAge } from '$lib/paraglide/runtime'; import { detectLocale } from '$lib/shared/server/locale'; // VITE_SENTRY_DSN is a write-only ingest key — it can POST events to GlitchTip // but cannot read them. Safe to include in the client bundle per Sentry security model. Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, environment: import.meta.env.MODE, tracesSampleRate: 0.1, sendDefaultPii: false, enabled: !!import.meta.env.VITE_SENTRY_DSN }); const PUBLIC_PATHS = [ '/login', '/logout', '/forgot-password', '/reset-password', '/register', '/hilfe/transkription' // prerendered help page — must be reachable without an auth cookie ]; const handleLocaleDetection: Handle = ({ event, resolve }) => { if (!event.cookies.get(cookieName)) { const locale = detectLocale(event.request.headers.get('accept-language') ?? ''); if (locale) { event.cookies.set(cookieName, locale, { path: '/', sameSite: 'lax', maxAge: cookieMaxAge, httpOnly: false }); } } return resolve(event); }; const handleAuth: Handle = async ({ event, resolve }) => { const isPublic = PUBLIC_PATHS.some((p) => event.url.pathname.startsWith(p)); if (!isPublic && !event.locals.user) { throw redirect(302, '/login'); } return resolve(event); }; const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => { event.request = request; return resolve(event, { transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale) }); }); const userGroup: Handle = async ({ event, resolve }) => { // One-off cleanup of the legacy Basic-credentials cookie from before the Spring Session migration (#523). if (event.cookies.get('auth_token')) { event.cookies.delete('auth_token', { path: '/' }); } const sessionId = event.cookies.get('fa_session'); if (!sessionId) { return resolve(event); } try { const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; const response = await fetch(`${apiUrl}/api/users/me`, { headers: { Cookie: `fa_session=${sessionId}` } }); if (response.ok) { event.locals.user = await response.json(); } else if (response.status === 401) { // Backend rejected the session (expired or invalidated). Drop the stale // cookie and surface the reason on the login page. PUBLIC_PATHS check // avoids a redirect loop if the user is already on /login. event.cookies.delete('fa_session', { path: '/' }); const isPublic = PUBLIC_PATHS.some((p) => event.url.pathname.startsWith(p)); if (!isPublic) { throw redirect(302, '/login?reason=expired'); } } } catch (error) { // Don't swallow SvelteKit redirects — they're thrown as objects with a `status` field. if (error instanceof Object && 'status' in error && 'location' in error) { throw error; } console.error('Error fetching user in hook:', error); } return resolve(event); }; export const handleFetch: HandleFetch = async ({ event, request, fetch }) => { const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; const isApi = request.url.startsWith(apiUrl) || request.url.includes('/api/'); if (isApi) { // Auth endpoints that establish/check their own credentials manage cookies themselves; // don't double-inject a stale fa_session. const PUBLIC_API_PATHS = [ '/api/auth/login', '/api/auth/logout', '/api/auth/forgot-password', '/api/auth/reset-password', '/api/auth/invite/', '/api/auth/register' ]; if (PUBLIC_API_PATHS.some((p) => request.url.includes(p))) { return fetch(request); } const sessionId = event.cookies.get('fa_session'); if (!sessionId) { return new Response('Unauthorized', { status: 401 }); } // Clone first so the body stream is preserved on the new Request. const cloned = request.clone(); const modified = new Request(cloned, { headers: { ...Object.fromEntries(cloned.headers), Cookie: `fa_session=${sessionId}` } }); return fetch(modified); } return fetch(request); }; export const handle = sequence(userGroup, handleAuth, handleLocaleDetection, handleParaglide); export const handleError = Sentry.handleErrorWithSentry(() => { const errorId = Sentry.lastEventId() ?? crypto.randomUUID(); return { message: 'An unexpected error occurred', errorId }; });