/** * Audit Logging Helper * * Provides easy-to-use functions for logging audit events from API endpoints. * This is an Enterprise-only feature. */ import type { RequestEvent } from '@sveltejs/kit'; import { isEnterprise } from './license'; import { logAuditEvent, type AuditAction, type AuditEntityType, type AuditLogCreateData } from './db'; import { authorize } from './authorize'; export interface AuditContext { userId?: number | null; username: string; ipAddress?: string | null; userAgent?: string | null; } /** * Extract audit context from a request event */ export async function getAuditContext(event: RequestEvent): Promise { const auth = await authorize(event.cookies); // Get IP address from various headers (proxied requests) const forwardedFor = event.request.headers.get('x-forwarded-for'); const realIp = event.request.headers.get('x-real-ip'); let ipAddress = forwardedFor?.split(',')[0]?.trim() || realIp || event.getClientAddress?.() || null; // Convert IPv6 loopback to more readable format if (ipAddress === '::1' || ipAddress === '::ffff:127.0.0.1') { ipAddress = '127.0.0.1'; } else if (ipAddress?.startsWith('::ffff:')) { // Strip IPv6 prefix from IPv4-mapped addresses ipAddress = ipAddress.substring(7); } // Get user agent const userAgent = event.request.headers.get('user-agent') || null; return { userId: auth.user?.id ?? null, username: auth.user?.username ?? 'anonymous', ipAddress, userAgent }; } /** * Log an audit event (only logs if Enterprise license is active) */ export async function audit( event: RequestEvent, action: AuditAction, entityType: AuditEntityType, options: { entityId?: string | null; entityName?: string | null; environmentId?: number | null; description?: string | null; details?: any | null; } = {} ): Promise { // Only log if enterprise if (!(await isEnterprise())) return; const ctx = await getAuditContext(event); const data: AuditLogCreateData = { userId: ctx.userId, username: ctx.username, action, entityType: entityType, entityId: options.entityId ?? null, entityName: options.entityName ?? null, environmentId: options.environmentId ?? null, description: options.description ?? null, details: options.details ?? null, ipAddress: ctx.ipAddress ?? null, userAgent: ctx.userAgent ?? null }; try { await logAuditEvent(data); } catch (error) { // Don't let audit logging errors break the main operation console.error('Failed to log audit event:', error); } } /** * Helper for container actions */ export async function auditContainer( event: RequestEvent, action: AuditAction, containerId: string, containerName: string, environmentId?: number | null, details?: any ): Promise { await audit(event, action, 'container', { entityId: containerId, entityName: containerName, environmentId, description: `Container ${containerName} ${action}`, details }); } /** * Helper for image actions */ export async function auditImage( event: RequestEvent, action: AuditAction, imageId: string, imageName: string, environmentId?: number | null, details?: any ): Promise { await audit(event, action, 'image', { entityId: imageId, entityName: imageName, environmentId, description: `Image ${imageName} ${action}`, details }); } /** * Helper for stack actions */ export async function auditStack( event: RequestEvent, action: AuditAction, stackName: string, environmentId?: number | null, details?: any ): Promise { await audit(event, action, 'stack', { entityId: stackName, entityName: stackName, environmentId, description: `Stack ${stackName} ${action}`, details }); } /** * Helper for volume actions */ export async function auditVolume( event: RequestEvent, action: AuditAction, volumeId: string, volumeName: string, environmentId?: number | null, details?: any ): Promise { await audit(event, action, 'volume', { entityId: volumeId, entityName: volumeName, environmentId, description: `Volume ${volumeName} ${action}`, details }); } /** * Helper for network actions */ export async function auditNetwork( event: RequestEvent, action: AuditAction, networkId: string, networkName: string, environmentId?: number | null, details?: any ): Promise { await audit(event, action, 'network', { entityId: networkId, entityName: networkName, environmentId, description: `Network ${networkName} ${action}`, details }); } /** * Helper for user actions */ export async function auditUser( event: RequestEvent, action: AuditAction, userId: number, username: string, details?: any ): Promise { await audit(event, action, 'user', { entityId: String(userId), entityName: username, description: `User ${username} ${action}`, details }); } /** * Helper for settings actions */ export async function auditSettings( event: RequestEvent, action: AuditAction, settingName: string, details?: any ): Promise { await audit(event, action, 'settings', { entityId: settingName, entityName: settingName, description: `Settings ${settingName} ${action}`, details }); } /** * Helper for environment actions */ export async function auditEnvironment( event: RequestEvent, action: AuditAction, environmentId: number, environmentName: string, details?: any ): Promise { await audit(event, action, 'environment', { entityId: String(environmentId), entityName: environmentName, environmentId, description: `Environment ${environmentName} ${action}`, details }); } /** * Helper for registry actions */ export async function auditRegistry( event: RequestEvent, action: AuditAction, registryId: number, registryName: string, details?: any ): Promise { await audit(event, action, 'registry', { entityId: String(registryId), entityName: registryName, description: `Registry ${registryName} ${action}`, details }); } /** * Helper for auth actions (login/logout) */ export async function auditAuth( event: RequestEvent, action: 'login' | 'logout', username: string, details?: any ): Promise { // For login/logout, we want to log even without a session if (!(await isEnterprise())) return; const forwardedFor = event.request.headers.get('x-forwarded-for'); const realIp = event.request.headers.get('x-real-ip'); let ipAddress = forwardedFor?.split(',')[0]?.trim() || realIp || event.getClientAddress?.() || null; // Convert IPv6 loopback to more readable format if (ipAddress === '::1' || ipAddress === '::ffff:127.0.0.1') { ipAddress = '127.0.0.1'; } else if (ipAddress?.startsWith('::ffff:')) { ipAddress = ipAddress.substring(7); } const userAgent = event.request.headers.get('user-agent') || null; const data: AuditLogCreateData = { userId: null, // Will be set from details if available username, action, entityType: 'user', entityId: null, entityName: username, environmentId: null, description: `User ${username} ${action}`, details, ipAddress: ipAddress, userAgent: userAgent }; try { await logAuditEvent(data); } catch (error) { console.error('Failed to log audit event:', error); } }