import type { Handle } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; import { verifyAccessToken } from '$lib/server/services/authService.js'; import * as authService from '$lib/server/services/authService.js'; import * as userService from '$lib/server/services/userService.js'; import * as apiTokenService from '$lib/server/services/apiTokenService.js'; import { extractBearerToken } from '$lib/server/middleware/authenticate.js'; import { isBoardGuestAccessible } from '$lib/server/middleware/guestAccess.js'; import { initBackupScheduler } from '$lib/server/jobs/backupScheduler.js'; import { clearSessionCookies, rotateSessionCookies, COOKIE_NAMES } from '$lib/server/utils/sessionCookies.js'; // Initialize backup scheduler on server startup initBackupScheduler(); const PUBLIC_PATHS = ['/login', '/register', '/auth/', '/api/health', '/api/onboarding', '/status']; function isPublicPath(pathname: string): boolean { return PUBLIC_PATHS.some((path) => pathname === path || pathname.startsWith(path)); } export const handle: Handle = async ({ event, resolve }) => { event.locals.user = null; event.locals.session = null; event.locals.apiTokenScope = null; const accessToken = event.cookies.get(COOKIE_NAMES.ACCESS_TOKEN); const refreshToken = event.cookies.get(COOKIE_NAMES.REFRESH_TOKEN); const sessionId = event.cookies.get(COOKIE_NAMES.SESSION_ID); if (accessToken) { try { const payload = verifyAccessToken(accessToken); const user = await userService.findById(payload.userId); event.locals.user = { id: user.id, email: user.email, displayName: user.displayName, role: user.role as 'admin' | 'user' }; event.locals.session = { id: sessionId ?? payload.userId, expiresAt: new Date(Date.now() + 15 * 60 * 1000) }; } catch { // Access token invalid/expired — try refresh below } } // If no valid session but refresh + session id exist, try to rotate. if (!event.locals.user && refreshToken && sessionId) { try { const session = await authService.validateSession(sessionId, refreshToken); if (session) { const user = await userService.findById(session.userId); await rotateSessionCookies( event.cookies, session.id, { id: user.id, email: user.email, role: user.role }, session.rememberMe ); event.locals.user = { id: user.id, email: user.email, displayName: user.displayName, role: user.role as 'admin' | 'user' }; event.locals.session = { id: session.id, expiresAt: new Date(Date.now() + 15 * 60 * 1000) }; } } catch { clearSessionCookies(event.cookies); } } // Bearer API tokens (no session cookie). if (!event.locals.user) { const bearerToken = extractBearerToken(event); if (bearerToken) { try { const tokenResult = await apiTokenService.validateToken(bearerToken); if (tokenResult) { const user = await userService.findById(tokenResult.userId); event.locals.user = { id: user.id, email: user.email, displayName: user.displayName, role: user.role as 'admin' | 'user' }; event.locals.session = { id: user.id, expiresAt: new Date(Date.now() + 15 * 60 * 1000) }; event.locals.apiTokenScope = tokenResult.scope as 'read' | 'write' | 'admin'; } } catch { // API token validation failed — continue as unauthenticated } } } const { pathname } = event.url; // API token scope enforcement if (event.locals.apiTokenScope) { const method = event.request.method; const scope = event.locals.apiTokenScope; const isWriteMethod = method === 'POST' || method === 'PATCH' || method === 'PUT' || method === 'DELETE'; const isAdminPath = pathname.startsWith('/api/admin') || pathname.startsWith('/admin'); if (scope === 'read' && isWriteMethod) { return new Response( JSON.stringify({ success: false, data: null, error: 'API token scope "read" does not allow write operations' }), { status: 403, headers: { 'Content-Type': 'application/json' } } ); } if (scope !== 'admin' && isAdminPath) { return new Response( JSON.stringify({ success: false, data: null, error: 'API token scope does not allow admin operations' }), { status: 403, headers: { 'Content-Type': 'application/json' } } ); } } if (!event.locals.user && !isPublicPath(pathname)) { const boardMatch = pathname.match(/^\/boards\/([^/]+)/); if (boardMatch) { const boardId = boardMatch[1]; const isGuestAccessible = await isBoardGuestAccessible(boardId); if (isGuestAccessible) { return resolve(event); } } if (pathname === '/') { return resolve(event); } throw redirect(302, '/login'); } return resolve(event); };