Private
Public Access
1
0
Files
Kanban/src/hooks.server.ts
T
Catherine Renelle 15f8e8218a Allow Google Fonts in CSP for Tailwind's Inter font
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:14:15 -04:00

86 lines
3.0 KiB
TypeScript

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('</head>', `${tenantCSS}</head>`)
: 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;
};