import { Elysia, t } from 'elysia'; import { cors } from '@elysiajs/cors'; import { jwt } from '@elysiajs/jwt'; import { resolve, normalize } from 'path'; import { existsSync } from 'fs'; import { JWT_SECRET, logger, LOG_LEVEL, logToFrontend } from './config.js'; import { getPerformanceSummary, resetPerformanceMetrics } from './services/performance.service.js'; import { getCacheStats } from './services/cache.service.js'; import { registerUser, authenticateUser, getUserById, updateLoTWCredentials, updateDCLCredentials, } from './services/auth.service.js'; import { getSystemStats, getUserStats, getAdminActions, impersonateUser, verifyImpersonation, stopImpersonation, getImpersonationStatus, changeUserRole, deleteUser, } from './services/admin.service.js'; import { getAllUsers } from './services/auth.service.js'; import { getUserQSOs, getQSOStats, deleteQSOs, getQSOById, } from './services/lotw.service.js'; import { enqueueJob, getJobStatus, getUserActiveJob, getUserJobs, cancelJob, } from './services/job-queue.service.js'; import { getAllAwards, getAwardProgressDetails, getAwardEntityBreakdown, } from './services/awards.service.js'; /** * Main backend application * Serves API routes and static frontend files */ // SECURITY: Stricter email validation // Elysia's built-in email format check is lenient; this provides better validation // Allows: alphanumeric, dots, hyphens, underscores in local part // Requires: valid domain with at least one dot const EMAIL_REGEX = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; function isValidEmail(email) { // Basic format check with regex if (!EMAIL_REGEX.test(email)) { return false; } // Additional checks const [local, domain] = email.split('@'); // Local part shouldn't start or end with dot if (local.startsWith('.') || local.endsWith('.')) { return false; } // Local part shouldn't have consecutive dots if (local.includes('..')) { return false; } // Domain must have at least one dot and valid TLD if (!domain || !domain.includes('.') || domain.split('.').pop().length < 2) { return false; } // Length checks (RFC 5321 limits) if (email.length > 254 || local.length > 64) { return false; } return true; } // SECURITY: Validate job ID parameter // Prevents injection and DoS via extremely large numbers function isValidJobId(jobId) { const id = parseInt(jobId, 10); // Check: must be a number, positive, and within reasonable range (1 to 2^31-1) return !isNaN(id) && id > 0 && id <= 2147483647 && Number.isSafeInteger(id); } // SECURITY: Validate QSO ID parameter function isValidQsoId(qsoId) { const id = parseInt(qsoId, 10); return !isNaN(id) && id > 0 && id <= 2147483647 && Number.isSafeInteger(id); } // SECURITY: In-memory rate limiter for auth endpoints // Prevents brute force attacks on login/register const rateLimitStore = new Map(); // IP -> { count, resetTime } function checkRateLimit(ip, limit = 5, windowMs = 60000) { const now = Date.now(); const record = rateLimitStore.get(ip); // Clean up expired records if (record && now > record.resetTime) { rateLimitStore.delete(ip); return { allowed: true, remaining: limit - 1 }; } // Check if limit exceeded if (record) { if (record.count >= limit) { const retryAfter = Math.ceil((record.resetTime - now) / 1000); return { allowed: false, retryAfter }; } record.count++; return { allowed: true, remaining: limit - record.count }; } // Create new record rateLimitStore.set(ip, { count: 1, resetTime: now + windowMs, }); return { allowed: true, remaining: limit - 1 }; } // SECURITY: Validate that a path doesn't escape the allowed directory // Prevents path traversal attacks like ../../../etc/passwd const BUILD_DIR = resolve('src/frontend/build'); function isPathSafe(requestedPath) { try { // Normalize the path to resolve any ../ sequences const normalized = normalize(requestedPath); const fullPath = resolve(BUILD_DIR, normalized); // Check if the resolved path is within the build directory return fullPath.startsWith(BUILD_DIR); } catch { return false; } } // Get allowed origins from environment // SECURITY: Never allow all origins, even in development // Always require explicit ALLOWED_ORIGINS or VITE_APP_URL const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : process.env.NODE_ENV === 'production' ? [process.env.VITE_APP_URL || 'https://awards.dj7nt.de'] : [process.env.VITE_APP_URL || 'http://localhost:5173']; // Default dev origin const app = new Elysia() // Enable CORS for frontend communication .use(cors({ origin: ALLOWED_ORIGINS, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], })) // JWT plugin .use(jwt({ name: 'jwt', secret: JWT_SECRET, })) // Authentication: derive user from JWT token .derive(async ({ jwt, headers }) => { const authHeader = headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return { user: null }; } const token = authHeader.substring(7); try { const payload = await jwt.verify(token); if (!payload) { return { user: null }; } // Check if this is an impersonation token const isImpersonation = !!payload.impersonatedBy; return { user: { id: payload.userId, email: payload.email, callsign: payload.callsign, isAdmin: payload.isAdmin, impersonatedBy: payload.impersonatedBy, // Admin ID if impersonating }, isImpersonation, }; } catch (error) { return { user: null }; } }) // Security headers middleware .onAfterHandle(({ set, request }) => { const url = new URL(request.url); const isProduction = process.env.NODE_ENV === 'production'; // Prevent clickjacking set.headers['X-Frame-Options'] = 'DENY'; // Prevent MIME type sniffing set.headers['X-Content-Type-Options'] = 'nosniff'; // Enable XSS filter set.headers['X-XSS-Protection'] = '1; mode=block'; // Referrer policy set.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'; // Content Security Policy (basic protection) // In development, allow any origin; in production, restrict const appUrl = process.env.VITE_APP_URL || 'https://awards.dj7nt.de'; const connectSrc = isProduction ? `'self' ${appUrl}` : "'self' *"; set.headers['Content-Security-Policy'] = [ "default-src 'self'", `script-src 'self' 'unsafe-inline'`, // unsafe-inline needed for SvelteKit inline scripts `style-src 'self' 'unsafe-inline'`, // unsafe-inline needed for SvelteKit inline styles `img-src 'self' data: https:`, `connect-src ${connectSrc}`, "font-src 'self'", "object-src 'none'", "base-uri 'self'", "form-action 'self'", ].join('; '); // Strict Transport Security (HTTPS only) - only in production if (isProduction) { set.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'; } }) // Request logging middleware .onRequest(({ request, params }) => { const url = new URL(request.url); const method = request.method; const path = url.pathname; const query = url.search; // Skip logging for health checks in development to reduce noise if (path === '/api/health' && process.env.NODE_ENV === 'development') { return; } logger.info('Incoming request', { method, path, query: query || undefined, ip: request.headers.get('x-forwarded-for') || 'unknown', userAgent: request.headers.get('user-agent')?.substring(0, 100), }); }) .onAfterHandle(({ request, set }) => { const url = new URL(request.url); const path = url.pathname; // Skip logging for health checks in development if (path === '/api/health' && process.env.NODE_ENV === 'development') { return; } // Log error responses if (set.status >= 400) { logger.warn('Request failed', { path, status: set.status, }); } }) /** * POST /api/auth/register * Register a new user */ .post( '/api/auth/register', async ({ body, jwt, set, request }) => { // SECURITY: Rate limiting to prevent abuse const ip = request.headers.get('x-forwarded-for') || 'unknown'; const rateLimit = checkRateLimit(ip, 5, 60000); // 5 requests per minute if (!rateLimit.allowed) { set.status = 429; set.headers['Retry-After'] = rateLimit.retryAfter.toString(); return { success: false, error: 'Too many registration attempts. Please try again later.', }; } set.headers['X-RateLimit-Remaining'] = rateLimit.remaining.toString(); // SECURITY: Additional server-side email validation if (!isValidEmail(body.email)) { set.status = 400; return { success: false, error: 'Invalid email address format', }; } // Create user const user = await registerUser(body); if (!user) { set.status = 400; return { success: false, error: 'Email already registered', }; } // Generate JWT token with expiration (24 hours) const exp = Math.floor(Date.now() / 1000) + (24 * 60 * 60); // 24 hours from now const token = await jwt.sign({ userId: user.id, email: user.email, callsign: user.callsign, exp, }); set.status = 201; return { success: true, token, user, }; }, { body: t.Object({ email: t.String({ format: 'email', error: 'Invalid email address', }), password: t.String({ minLength: 8, error: 'Password must be at least 8 characters', }), callsign: t.String({ minLength: 3, maxLength: 10, error: 'Callsign must be 3-10 characters', }), }), } ) /** * POST /api/auth/login * Authenticate user and return JWT token */ .post( '/api/auth/login', async ({ body, jwt, set, request }) => { // SECURITY: Rate limiting to prevent brute force attacks const ip = request.headers.get('x-forwarded-for') || 'unknown'; const rateLimit = checkRateLimit(ip, 10, 60000); // 10 requests per minute (more lenient than register) if (!rateLimit.allowed) { set.status = 429; set.headers['Retry-After'] = rateLimit.retryAfter.toString(); return { success: false, error: 'Too many login attempts. Please try again later.', }; } set.headers['X-RateLimit-Remaining'] = rateLimit.remaining.toString(); // Authenticate user const user = await authenticateUser(body.email, body.password); if (!user) { set.status = 401; return { success: false, error: 'Invalid email or password', }; } // Generate JWT token with expiration (24 hours) const exp = Math.floor(Date.now() / 1000) + (24 * 60 * 60); // 24 hours from now const token = await jwt.sign({ userId: user.id, email: user.email, callsign: user.callsign, isAdmin: user.isAdmin, exp, }); return { success: true, token, user, }; }, { body: t.Object({ email: t.String({ format: 'email' }), password: t.String(), }), } ) /** * GET /api/auth/me * Get current user profile (requires authentication) */ .get('/api/auth/me', async ({ user, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } // Get full user data from database const userData = await getUserById(user.id); if (!userData) { set.status = 404; return { success: false, error: 'User not found' }; } // Include impersonatedBy from JWT if present (not stored in database) const responseUser = { ...userData, impersonatedBy: user.impersonatedBy, }; return { success: true, user: responseUser, }; }) /** * PUT /api/auth/lotw-credentials * Update LoTW credentials (requires authentication) */ .put( '/api/auth/lotw-credentials', async ({ user, body, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { // Get current user data to preserve password if not provided const userData = await getUserById(user.id); if (!userData) { set.status = 404; return { success: false, error: 'User not found' }; } // If password is empty, keep existing password const lotwPassword = body.lotwPassword || userData.lotwPassword; await updateLoTWCredentials(user.id, body.lotwUsername, lotwPassword); return { success: true, message: 'LoTW credentials updated successfully', }; } catch (error) { set.status = 500; return { success: false, error: 'Failed to update LoTW credentials', }; } }, { body: t.Object({ lotwUsername: t.String(), lotwPassword: t.Optional(t.String()), }), } ) /** * PUT /api/auth/dcl-credentials * Update DCL credentials (requires authentication) */ .put( '/api/auth/dcl-credentials', async ({ user, body, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { await updateDCLCredentials(user.id, body.dclApiKey); return { success: true, message: 'DCL credentials updated successfully', }; } catch (error) { set.status = 500; return { success: false, error: 'Failed to update DCL credentials', }; } }, { body: t.Object({ dclApiKey: t.String(), }), } ) /** * POST /api/lotw/sync * Queue a LoTW sync job (requires authentication) * Returns immediately with job ID */ .post('/api/lotw/sync', async ({ user, set }) => { if (!user) { logger.warn('/api/lotw/sync: Unauthorized access attempt'); set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const result = await enqueueJob(user.id, 'lotw_sync'); if (!result.success && result.existingJob) { return { success: true, jobId: result.existingJob, message: 'A LoTW sync job is already running', }; } return result; } catch (error) { logger.error('Error in /api/lotw/sync', { error: error.message }); set.status = 500; return { success: false, error: `Failed to queue LoTW sync job: ${error.message}`, }; } }) /** * POST /api/dcl/sync * Queue a DCL sync job (requires authentication) * Returns immediately with job ID */ .post('/api/dcl/sync', async ({ user, set }) => { if (!user) { logger.warn('/api/dcl/sync: Unauthorized access attempt'); set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const result = await enqueueJob(user.id, 'dcl_sync'); if (!result.success && result.existingJob) { return { success: true, jobId: result.existingJob, message: 'A DCL sync job is already running', }; } return result; } catch (error) { logger.error('Error in /api/dcl/sync', { error: error.message }); set.status = 500; return { success: false, error: `Failed to queue DCL sync job: ${error.message}`, }; } }) /** * GET /api/jobs/:jobId * Get job status (requires authentication) */ .get('/api/jobs/:jobId', async ({ user, params, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { // SECURITY: Validate job ID to prevent injection and DoS if (!isValidJobId(params.jobId)) { set.status = 400; return { success: false, error: 'Invalid job ID' }; } const jobId = parseInt(params.jobId, 10); const job = await getJobStatus(jobId); if (!job) { set.status = 404; return { success: false, error: 'Job not found' }; } // Verify user owns this job if (job.userId !== user.id) { set.status = 403; return { success: false, error: 'Forbidden' }; } return { success: true, job, }; } catch (error) { set.status = 500; return { success: false, error: 'Failed to fetch job status', }; } }) /** * GET /api/jobs/active * Get user's active job (requires authentication) */ .get('/api/jobs/active', async ({ user, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const job = await getUserActiveJob(user.id); if (!job) { return { success: true, job: null, }; } return { success: true, job: { id: job.id, type: job.type, status: job.status, createdAt: job.createdAt, startedAt: job.startedAt, }, }; } catch (error) { set.status = 500; return { success: false, error: 'Failed to fetch active job', }; } }) /** * GET /api/jobs * Get user's recent jobs (requires authentication) */ .get('/api/jobs', async ({ user, query, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const limit = query.limit ? parseInt(query.limit) : 10; const jobs = await getUserJobs(user.id, limit); return { success: true, jobs, }; } catch (error) { set.status = 500; return { success: false, error: 'Failed to fetch jobs', }; } }) /** * DELETE /api/jobs/:jobId * Cancel and rollback a sync job (requires authentication) * Only allows cancelling failed, completed, or stale running jobs (>1 hour) */ .delete('/api/jobs/:jobId', async ({ user, params, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { // SECURITY: Validate job ID to prevent injection and DoS if (!isValidJobId(params.jobId)) { set.status = 400; return { success: false, error: 'Invalid job ID' }; } const jobId = parseInt(params.jobId, 10); const result = await cancelJob(jobId, user.id); if (!result.success) { set.status = 400; return result; } return result; } catch (error) { logger.error('Error cancelling job', { error: error.message, userId: user?.id, jobId: params.jobId }); set.status = 500; return { success: false, error: 'Failed to cancel job', }; } }) /** * GET /api/qsos * Get user's QSOs (requires authentication) * Supports pagination: ?page=1&limit=100 * Supports filters: band, mode, confirmed, confirmationType (all, lotw, dcl, both, none), search */ .get('/api/qsos', async ({ user, query, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const filters = {}; if (query.band) filters.band = query.band; if (query.mode) filters.mode = query.mode; if (query.confirmed) filters.confirmed = query.confirmed === 'true'; if (query.confirmationType && query.confirmationType !== 'all') { filters.confirmationType = query.confirmationType; } if (query.search) filters.search = query.search; // Pagination options const options = { page: query.page ? parseInt(query.page) : 1, limit: query.limit ? parseInt(query.limit) : 100, }; const result = await getUserQSOs(user.id, filters, options); return { success: true, ...result, }; } catch (error) { set.status = 500; return { success: false, error: 'Failed to fetch QSOs', }; } }) /** * GET /api/qsos/:id * Get a single QSO by ID (requires authentication) */ .get('/api/qsos/:id', async ({ user, params, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { // SECURITY: Validate QSO ID to prevent injection and DoS if (!isValidQsoId(params.id)) { set.status = 400; return { success: false, error: 'Invalid QSO ID' }; } const qsoId = parseInt(params.id, 10); const qso = await getQSOById(user.id, qsoId); if (!qso) { set.status = 404; return { success: false, error: 'QSO not found' }; } return { success: true, qso, }; } catch (error) { logger.error('Failed to fetch QSO by ID', { error: error.message, userId: user?.id, qsoId: params.id }); set.status = 500; return { success: false, error: 'Failed to fetch QSO', }; } }) /** * GET /api/qsos/stats * Get QSO statistics (requires authentication) */ .get('/api/qsos/stats', async ({ user, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const stats = await getQSOStats(user.id); return { success: true, stats, }; } catch (error) { set.status = 500; return { success: false, error: 'Failed to fetch statistics', }; } }) /** * DELETE /api/qsos/all * Delete all QSOs for authenticated user */ .delete('/api/qsos/all', async ({ user, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const deleted = await deleteQSOs(user.id); return { success: true, deleted, message: `Deleted ${deleted} QSO(s)`, }; } catch (error) { logger.error('Failed to delete QSOs', { error: error.message, stack: error.stack }); set.status = 500; return { success: false, error: 'Failed to delete QSOs', }; } }) /** * GET /api/awards * Get all available awards (requires authentication) */ .get('/api/awards', async ({ user, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const awards = await getAllAwards(); return { success: true, awards, }; } catch (error) { logger.error('Error fetching awards', { error: error.message }); set.status = 500; return { success: false, error: 'Failed to fetch awards', }; } }) /** * GET /api/awards/:awardId/progress * Get award progress for user (requires authentication) */ .get('/api/awards/:awardId/progress', async ({ user, params, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } const { awardId } = params; const progress = await getAwardProgressDetails(user.id, awardId); if (!progress) { set.status = 404; return { success: false, error: 'Award not found', }; } return { success: true, ...progress, }; }) /** * GET /api/awards/batch/progress * Get progress for ALL awards in a single request (fixes N+1 query problem) */ .get('/api/awards/batch/progress', async ({ user, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } try { const awards = await getAllAwards(); // Calculate all awards in parallel const progressMap = await Promise.all( awards.map(async (award) => { const progress = await getAwardProgressDetails(user.id, award.id); return { awardId: award.id, ...progress, }; }) ); return { success: true, awards: progressMap, }; } catch (error) { logger.error('Failed to fetch batch award progress', { error: error.message, userId: user.id }); set.status = 500; return { success: false, error: 'Failed to fetch award progress', }; } }) /** * GET /api/awards/:awardId/entities * Get detailed entity breakdown for an award (requires authentication) */ .get('/api/awards/:awardId/entities', async ({ user, params, set }) => { if (!user) { set.status = 401; return { success: false, error: 'Unauthorized' }; } const { awardId } = params; const breakdown = await getAwardEntityBreakdown(user.id, awardId); if (!breakdown) { set.status = 404; return { success: false, error: 'Award not found', }; } return { success: true, ...breakdown, }; }) // Health check endpoint .get('/api/health', () => ({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime(), performance: getPerformanceSummary(), cache: getCacheStats() })) /** * POST /api/logs * Receive frontend logs and write them to frontend.log file */ .post( '/api/logs', async ({ body, user, headers }) => { // Extract context from headers (Elysia provides headers as a plain object) const userAgent = headers['user-agent'] || 'unknown'; const context = { userId: user?.id, userAgent, timestamp: new Date().toISOString(), }; // Log each entry const entries = Array.isArray(body) ? body : [body]; for (const entry of entries) { logToFrontend(entry.level || 'info', entry.message || 'No message', entry.data || null, context); } return { success: true }; }, { body: t.Union([ t.Object({ level: t.Optional(t.Union([t.Literal('debug'), t.Literal('info'), t.Literal('warn'), t.Literal('error')])), message: t.String(), data: t.Optional(t.Any()), }), t.Array( t.Object({ level: t.Optional(t.Union([t.Literal('debug'), t.Literal('info'), t.Literal('warn'), t.Literal('error')])), message: t.String(), data: t.Optional(t.Any()), }) ), ]), } ) /** * ================================================================ * ADMIN ROUTES * ================================================================ * All admin routes require authentication and admin role */ /** * GET /api/admin/stats * Get system-wide statistics (admin only) */ .get('/api/admin/stats', async ({ user, set }) => { if (!user || !user.isAdmin) { set.status = !user ? 401 : 403; return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' }; } try { const stats = await getSystemStats(); return { success: true, stats, }; } catch (error) { logger.error('Error fetching system stats', { error: error.message, userId: user.id }); set.status = 500; return { success: false, error: 'Failed to fetch system statistics', }; } }) /** * GET /api/admin/users * Get all users with statistics (admin only) */ .get('/api/admin/users', async ({ user, set }) => { if (!user || !user.isAdmin) { set.status = !user ? 401 : 403; return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' }; } try { const users = await getUserStats(); return { success: true, users, }; } catch (error) { logger.error('Error fetching users', { error: error.message, userId: user.id }); set.status = 500; return { success: false, error: 'Failed to fetch users', }; } }) /** * GET /api/admin/users/:userId * Get detailed information about a specific user (admin only) */ .get('/api/admin/users/:userId', async ({ user, params, set }) => { if (!user || !user.isAdmin) { set.status = !user ? 401 : 403; return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' }; } const userId = parseInt(params.userId, 10); if (isNaN(userId) || userId <= 0) { set.status = 400; return { success: false, error: 'Invalid user ID' }; } try { const targetUser = await getAllUsers(); const userDetails = targetUser.find(u => u.id === userId); if (!userDetails) { set.status = 404; return { success: false, error: 'User not found' }; } return { success: true, user: userDetails, }; } catch (error) { logger.error('Error fetching user details', { error: error.message, userId: user.id }); set.status = 500; return { success: false, error: 'Failed to fetch user details', }; } }) /** * POST /api/admin/users/:userId/role * Update user admin status (admin only) */ .post('/api/admin/users/:userId/role', async ({ user, params, body, set }) => { if (!user || !user.isAdmin) { set.status = !user ? 401 : 403; return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' }; } const targetUserId = parseInt(params.userId, 10); if (isNaN(targetUserId) || targetUserId <= 0) { set.status = 400; return { success: false, error: 'Invalid user ID' }; } const { isAdmin } = body; if (typeof isAdmin !== 'boolean') { set.status = 400; return { success: false, error: 'isAdmin (boolean) is required' }; } try { await changeUserRole(user.id, targetUserId, isAdmin); return { success: true, message: 'User admin status updated successfully', }; } catch (error) { logger.error('Error updating user admin status', { error: error.message, userId: user.id }); set.status = 400; return { success: false, error: error.message, }; } }) /** * DELETE /api/admin/users/:userId * Delete a user (admin only) */ .delete('/api/admin/users/:userId', async ({ user, params, set }) => { if (!user || !user.isAdmin) { set.status = !user ? 401 : 403; return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' }; } const targetUserId = parseInt(params.userId, 10); if (isNaN(targetUserId) || targetUserId <= 0) { set.status = 400; return { success: false, error: 'Invalid user ID' }; } try { await deleteUser(user.id, targetUserId); return { success: true, message: 'User deleted successfully', }; } catch (error) { logger.error('Error deleting user', { error: error.message, userId: user.id }); set.status = 400; return { success: false, error: error.message, }; } }) /** * POST /api/admin/impersonate/:userId * Start impersonating a user (admin only) */ .post('/api/admin/impersonate/:userId', async ({ user, params, jwt, set }) => { if (!user || !user.isAdmin) { set.status = !user ? 401 : 403; return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' }; } const targetUserId = parseInt(params.userId, 10); if (isNaN(targetUserId) || targetUserId <= 0) { set.status = 400; return { success: false, error: 'Invalid user ID' }; } try { const targetUser = await impersonateUser(user.id, targetUserId); // Generate impersonation token with shorter expiration (1 hour) const exp = Math.floor(Date.now() / 1000) + (60 * 60); // 1 hour from now const token = await jwt.sign({ userId: targetUser.id, email: targetUser.email, callsign: targetUser.callsign, isAdmin: targetUser.isAdmin, impersonatedBy: user.id, // Admin ID who started impersonation exp, }); return { success: true, token, impersonating: { userId: targetUser.id, email: targetUser.email, callsign: targetUser.callsign, }, message: `Impersonating ${targetUser.email}`, }; } catch (error) { logger.error('Error starting impersonation', { error: error.message, userId: user.id }); set.status = 400; return { success: false, error: error.message, }; } }) /** * POST /api/admin/impersonate/stop * Stop impersonating and return to admin account (admin only) */ .post('/api/admin/impersonate/stop', async ({ user, jwt, body, set }) => { if (!user || !user.impersonatedBy) { set.status = 400; return { success: false, error: 'Not currently impersonating a user', }; } try { // Log impersonation stop await stopImpersonation(user.impersonatedBy, user.id); // Get admin user details to generate new token const adminUsers = await getAllUsers(); const adminUser = adminUsers.find(u => u.id === user.impersonatedBy); if (!adminUser) { set.status = 500; return { success: false, error: 'Admin account not found', }; } // Generate new admin token (24 hours) const exp = Math.floor(Date.now() / 1000) + (24 * 60 * 60); const token = await jwt.sign({ userId: adminUser.id, email: adminUser.email, callsign: adminUser.callsign, isAdmin: adminUser.isAdmin, exp, }); return { success: true, token, user: adminUser, message: 'Impersonation stopped. Returned to admin account.', }; } catch (error) { logger.error('Error stopping impersonation', { error: error.message }); set.status = 500; return { success: false, error: 'Failed to stop impersonation', }; } }) /** * GET /api/admin/impersonation/status * Get current impersonation status */ .get('/api/admin/impersonation/status', async ({ user }) => { if (!user) { return { success: true, impersonating: false, }; } const isImpersonating = !!user.impersonatedBy; return { success: true, impersonating: isImpersonating, impersonatedBy: user.impersonatedBy, }; }) /** * GET /api/admin/actions * Get admin actions log (admin only) */ .get('/api/admin/actions', async ({ user, set, query }) => { if (!user || !user.isAdmin) { set.status = !user ? 401 : 403; return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' }; } const limit = parseInt(query.limit || '50', 10); const offset = parseInt(query.offset || '0', 10); try { const actions = await getAdminActions(null, { limit, offset }); return { success: true, actions, }; } catch (error) { logger.error('Error fetching admin actions', { error: error.message, userId: user.id }); set.status = 500; return { success: false, error: 'Failed to fetch admin actions', }; } }) /** * GET /api/admin/actions/my * Get current admin's action log (admin only) */ .get('/api/admin/actions/my', async ({ user, set, query }) => { if (!user || !user.isAdmin) { set.status = !user ? 401 : 403; return { success: false, error: !user ? 'Unauthorized' : 'Admin access required' }; } const limit = parseInt(query.limit || '50', 10); const offset = parseInt(query.offset || '0', 10); try { const actions = await getAdminActions(user.id, { limit, offset }); return { success: true, actions, }; } catch (error) { logger.error('Error fetching admin actions', { error: error.message, userId: user.id }); set.status = 500; return { success: false, error: 'Failed to fetch admin actions', }; } }) // Serve static files and SPA fallback for all non-API routes .get('/*', ({ request }) => { const url = new URL(request.url); const pathname = url.pathname; // Don't intercept API routes if (pathname.startsWith('/api')) { return new Response('Not found', { status: 404 }); } // Check for common missing assets before trying to open files // This prevents Elysia from trying to get file size of non-existent files const commonMissingFiles = ['/favicon.ico', '/robots.txt']; if (commonMissingFiles.includes(pathname)) { return new Response('Not found', { status: 404 }); } // Handle SvelteKit assets path - replace %sveltekit.assets% with the assets directory if (pathname.startsWith('/%sveltekit.assets%/')) { // Extract the actual file path after %sveltekit.assets%/ const assetPath = pathname.replace('/%sveltekit.assets%/', ''); // SECURITY: Validate path before serving const assetsDirPath = `_app/immutable/assets/${assetPath}`; const rootPath = assetPath; if (isPathSafe(assetsDirPath)) { try { const assetsPath = `src/frontend/build/${assetsDirPath}`; const file = Bun.file(assetsPath); const exists = file.exists(); if (exists) { return new Response(file); } } catch (err) { // Fall through to 404 } } // If not in assets, try root directory (with path validation) if (isPathSafe(rootPath)) { try { const rootFile = Bun.file(`src/frontend/build/${rootPath}`); const rootExists = rootFile.exists(); if (rootExists) { return new Response(rootFile); } } catch (err) { // Fall through to 404 } } return new Response('Not found', { status: 404 }); } // Try to serve the file from the build directory // Remove leading slash for file path const filePath = pathname === '/' ? '/index.html' : pathname; // SECURITY: Validate path is within build directory before serving if (!isPathSafe(filePath.substring(1))) { // Remove leading slash for relative path logger.warn('Path traversal attempt blocked', { pathname }); return new Response('Not found', { status: 404 }); } try { const fullPath = `src/frontend/build${filePath}`; // For paths without extensions or directories, use SPA fallback immediately // This prevents errors when trying to open directories as files const ext = filePath.split('.').pop(); const hasExtension = ext !== filePath && ext.length <= 5; // Simple check for file extension if (!hasExtension) { // No extension means it's a route, not a file - serve index.html const indexFile = Bun.file('src/frontend/build/index.html'); return new Response(indexFile, { headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache, no-store, must-revalidate', }, }); } // Try to serve actual files (with extensions) const file = Bun.file(fullPath); const exists = file.exists(); if (exists) { // Determine content type const contentTypes = { 'js': 'application/javascript', 'css': 'text/css', 'html': 'text/html; charset=utf-8', 'json': 'application/json', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'svg': 'image/svg+xml', 'ico': 'image/x-icon', 'woff': 'font/woff', 'woff2': 'font/woff2', 'ttf': 'font/ttf', }; const headers = {}; if (contentTypes[ext]) { headers['Content-Type'] = contentTypes[ext]; } // Cache headers if (ext === 'html') { headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'; } else { headers['Cache-Control'] = 'public, max-age=86400'; } return new Response(file, { headers }); } } catch (err) { // File not found or error, fall through to SPA fallback logger.debug('Error serving static file, falling back to SPA', { path: pathname, error: err.message }); } // SPA fallback - serve index.html for all other routes try { const indexFile = Bun.file('src/frontend/build/index.html'); return new Response(indexFile, { headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache, no-store, must-revalidate', }, }); } catch (err) { logger.error('Frontend build not found', { error: err.message }); return new Response( '
The frontend application is not currently available. This usually means the application is being updated or restarted.
' + 'Please try refreshing the page in a few moments.
', { status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8' } } ); } }) // Start server - uses PORT environment variable if set, otherwise defaults to 3001 .listen(process.env.PORT || 3001); logger.info('Server started', { port: process.env.PORT || 3001, nodeEnv: process.env.NODE_ENV || 'unknown', logLevel: LOG_LEVEL, });