import type { Handle } from '@sveltejs/kit'; import { resolveTenant, seedSystemTemplates } from '$lib/server/tenant.js'; import { validateSession, sessionCookieName } from '$lib/server/auth.js'; import { logger } from '$lib/server/logger.js'; import { generateTenantCSS } from '$lib/server/theme-css.js'; // One-time seed on startup seedSystemTemplates().catch((e) => logger.error({ err: e }, 'Failed to seed system templates')); export const handle: Handle = async ({ event, resolve }) => { const start = Date.now(); // 1. Resolve tenant from hostname // Use the Host header (or X-Forwarded-Host behind a reverse proxy) // instead of event.url.hostname, which is derived from the ORIGIN env var // and doesn't reflect the actual domain the user visited. const hostHeader = event.request.headers.get('x-forwarded-host') || event.request.headers.get('host') || event.url.hostname; const hostname = hostHeader.split(':')[0]; const tenant = await resolveTenant(hostname); event.locals.tenant = tenant; // 2. Resolve user session const sessionId = event.cookies.get(sessionCookieName()); if (sessionId) { const session = await validateSession(sessionId); if (session) { event.locals.session = { id: session.id }; event.locals.user = { id: session.user.id, email: session.user.email, name: session.user.name, globalRole: session.user.globalRole, tenantRole: session.user.tenantRole, avatarUrl: session.user.avatarUrl }; // Ensure user belongs to current tenant (unless site admin) if ( tenant && session.user.tenantId !== tenant.id && session.user.globalRole !== 'SITE_ADMIN' ) { event.locals.user = null; event.locals.session = null; } } else { // Invalid/expired session — clear cookie event.cookies.delete(sessionCookieName(), { path: '/' }); event.locals.user = null; event.locals.session = null; } } else { event.locals.user = null; event.locals.session = null; } // 3. Read theme preference event.locals.theme = event.cookies.get('theme') || undefined; // 4. Generate tenant theme CSS for injection const tenantCSS = generateTenantCSS(tenant?.theme); const response = await resolve(event, { transformPageChunk: tenantCSS ? ({ html }) => html.replace('', `${tenantCSS}`) : undefined }); // Security headers response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); response.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' ws: wss:; frame-ancestors 'none'"); logger.info({ method: event.request.method, path: event.url.pathname, status: response.status, duration_ms: Date.now() - start }, 'request'); return response; };