d418c367cb
Security fixes: - Validate theme background image URLs against whitelist pattern - Add board permission check to Socket.IO join-board handler - Disable raw HTML passthrough in markdown parser (XSS prevention) - Add UUID validation on avatar and branding route params (path traversal) - Add security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy) - Validate webhook URLs (HTTPS only, block internal addresses) - Add import payload size limit (5MB) - Remove server uptime from health endpoint - Make seed password configurable via SEED_ADMIN_PASSWORD env var - Patch DOMPurify dependency vulnerability via npm audit fix Warning fixes: - Convert $state(prop) to $effect.pre sync pattern (state_referenced_locally) - Add for/id pairings to form labels (a11y_label_has_associated_control) - Add svelte-ignore for intentional autofocus usage (a11y_autofocus) - Add ARIA roles to interactive/overlay divs (a11y_no_static_element_interactions) - Add aria-label to icon-only buttons (a11y_consider_explicit_label) - Add keyboard handler alongside click events (a11y_click_events_have_key_events) - Fix non-reactive fileInput declaration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
84 lines
2.7 KiB
TypeScript
84 lines
2.7 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');
|
|
|
|
logger.info({
|
|
method: event.request.method,
|
|
path: event.url.pathname,
|
|
status: response.status,
|
|
duration_ms: Date.now() - start
|
|
}, 'request');
|
|
|
|
return response;
|
|
};
|