Files
profilarr/src/hooks.server.ts
Sam Chau d2133aa457 feat(auth): implement authentication system
- Username/password login with bcrypt and session cookies
- API key authentication (X-Api-Key header or ?apikey query param)
- AUTH env var modes: on, local, off, oidc
- Generic OIDC support for external providers
- Session metadata tracking (IP, browser, device)
- Security settings page (password, sessions, API key)
- Login analysis with typo and attack detection
- Auth event logging throughout
2026-01-26 00:22:05 +10:30

117 lines
3.1 KiB
TypeScript

import type { Handle } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { config } from '$config';
import { printBanner, getServerInfo, logContainerConfig } from '$logger/startup.ts';
import { logSettings } from '$logger/settings.ts';
import { logger } from '$logger/logger.ts';
import { db } from '$db/db.ts';
import { runMigrations } from '$db/migrations.ts';
import { initializeJobs } from '$jobs/init.ts';
import { jobScheduler } from '$jobs/scheduler.ts';
import { pcdManager } from '$pcd/pcd.ts';
import {
getAuthState,
isPublicPath,
maybeExtendSession,
cleanupExpiredSessions
} from '$auth/middleware.ts';
import { getClientIp } from '$auth/network.ts';
// Initialize configuration on server startup
await config.init();
// Initialize database
await db.initialize();
// Run database migrations
await runMigrations();
// Load log settings from database (must be after migrations)
logSettings.load();
// Log container config (if running in Docker)
await logContainerConfig();
// Initialize PCD caches (must be after migrations and log settings)
await pcdManager.initialize();
// Initialize and start job system
await initializeJobs();
await jobScheduler.start();
// Clean expired sessions on startup
const expiredCount = cleanupExpiredSessions();
if (expiredCount > 0) {
await logger.info(`Cleaned up ${expiredCount} expired session${expiredCount === 1 ? '' : 's'}`, {
source: 'Auth:Session',
meta: { count: expiredCount }
});
}
// Log server ready
await logger.info('Server ready', {
source: 'Startup',
meta: getServerInfo()
});
// Print startup banner with URL
printBanner();
/**
* Auth middleware
* Handles authentication, authorization, and session management
*/
export const handle: Handle = async ({ event, resolve }) => {
const auth = getAuthState(event);
// First-run setup flow (applies to all auth modes except AUTH=off)
if (auth.needsSetup) {
if (event.url.pathname === '/auth/setup') {
return resolve(event);
}
throw redirect(303, '/auth/setup');
}
// AUTH=off or AUTH=local with local IP - skip auth after setup
if (auth.skipAuth) {
return resolve(event);
}
// Block setup page after user exists
if (event.url.pathname === '/auth/setup') {
throw redirect(303, '/');
}
// Public paths don't need auth
if (isPublicPath(event.url.pathname)) {
return resolve(event);
}
// Not authenticated - redirect or return 401
if (!auth.user) {
if (event.url.pathname.startsWith('/api')) {
const ip = getClientIp(event);
void logger.warn('Unauthorized API access', {
source: 'Auth',
meta: { ip, endpoint: event.url.pathname, method: event.request.method }
});
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
throw redirect(303, '/auth/login');
}
// Sliding expiration: extend session if past halfway point
if (auth.session) {
maybeExtendSession(auth.session);
}
// Authenticated - attach user to locals for use in routes
event.locals.user = auth.user;
event.locals.session = auth.session;
return resolve(event);
};