import express from 'express'; import { createServer } from 'http'; import { toNodeHandler } from 'better-auth/node'; import cors from 'cors'; import helmet from 'helmet'; import swaggerUi from 'swagger-ui-express'; import { auth } from './auth'; import { buildOpenApiDocument } from './docs/openapi'; import videosRoutes from './routes/videos'; import adminRoutes from './routes/admin'; import devicesRoutes from './routes/devices'; import deviceLinksRoutes from './routes/device-links'; import commandsRoutes from './routes/commands'; import eventsRoutes from './routes/events'; import streamsRoutes from './routes/streams'; import recordingsRoutes from './routes/recordings'; import pushNotificationsRoutes from './routes/push-notifications'; import auditRoutes from './routes/audit'; import opsRoutes from './routes/ops'; import { rateLimit } from './middleware/security'; import { requestContext } from './middleware/observability'; import { setupRealtimeGateway } from './realtime/gateway'; import { ensureMinioBucket } from './utils/minio'; import { startRecordingsWorker } from './workers/recordings'; import { startPushWorker } from './services/push'; const app = express(); const openApiDocument = buildOpenApiDocument(); const trustedOrigins = process.env.BETTER_AUTH_TRUSTED_ORIGINS ? process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(',').map((origin) => origin.trim()).filter(Boolean) : []; const buildMinioConnectOrigin = (): string | null => { const endpoint = process.env.MINIO_ENDPOINT?.trim(); if (!endpoint) { return null; } if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) { try { return new URL(endpoint).origin; } catch { return null; } } const useSSL = (process.env.MINIO_USE_SSL ?? 'false').toLowerCase() === 'true'; const port = Number(process.env.MINIO_PORT ?? (useSSL ? 443 : 80)); const scheme = useSSL ? 'https' : 'http'; const includePort = !(useSSL && port === 443) && !(!useSSL && port === 80); return `${scheme}://${endpoint}${includePort ? `:${port}` : ''}`; }; const minioConnectOrigin = buildMinioConnectOrigin(); const connectSrcDirectives = ["'self'", 'cdn.jsdelivr.net', ...(minioConnectOrigin ? [minioConnectOrigin] : [])]; const mediaSrcDirectives = ["'self'", 'blob:', 'data:', ...(minioConnectOrigin ? [minioConnectOrigin] : [])]; app.get('/', (_req, res) => { res.send('API is running'); }); app.get('/favicon.ico', (_req, res) => { res.status(204).end(); }); app.get('/openapi.json', (_req, res) => { res.json(openApiDocument); }); app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument)); app.all('/api/auth/*splat', toNodeHandler(auth)); app.use( helmet({ contentSecurityPolicy: { directives: { ...helmet.contentSecurityPolicy.getDefaultDirectives(), "script-src": ["'self'", "'unsafe-inline'", "cdn.jsdelivr.net", "cdn.tailwindcss.com"], "style-src": ["'self'", "'unsafe-inline'", "cdn.jsdelivr.net", "fonts.googleapis.com"], "font-src": ["'self'", "fonts.gstatic.com"], "connect-src": connectSrcDirectives, "media-src": mediaSrcDirectives, "img-src": ["'self'", "data:", "blob:"], }, }, }), ); app.use( cors({ origin: trustedOrigins.length > 0 ? trustedOrigins : true, credentials: true, }), ); app.use(rateLimit({ keyPrefix: 'global', windowMs: 60_000, max: 400 })); app.use(requestContext); app.use(express.json()); app.use('/sim', express.static('public')); app.use('/videos', videosRoutes); app.use('/admin', adminRoutes); app.use('/devices', rateLimit({ keyPrefix: 'devices', windowMs: 60_000, max: 120 }), devicesRoutes); app.use('/device-links', deviceLinksRoutes); app.use('/commands', rateLimit({ keyPrefix: 'commands', windowMs: 60_000, max: 120 }), commandsRoutes); app.use('/events', rateLimit({ keyPrefix: 'events', windowMs: 60_000, max: 120 }), eventsRoutes); app.use('/streams', rateLimit({ keyPrefix: 'streams', windowMs: 60_000, max: 120 }), streamsRoutes); app.use('/recordings', rateLimit({ keyPrefix: 'recordings', windowMs: 60_000, max: 120 }), recordingsRoutes); app.use('/push-notifications', pushNotificationsRoutes); app.use('/audit', auditRoutes); app.use('/ops', opsRoutes); app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { console.error(err); res.status(500).json({ message: 'Internal server error' }); }); const port = Number(process.env.PORT ?? 3000); const server = createServer(app); const start = async () => { try { await ensureMinioBucket(); } catch (error) { console.error('MinIO initialization failed', error); process.exit(1); } setupRealtimeGateway(server); startRecordingsWorker(); startPushWorker(); server.listen(port, () => { console.log(`Server is running on port ${port}`); }); }; start();