import express, { Request, Response, NextFunction } from 'express'; import cors from 'cors'; import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; import { db } from './db/connection.js'; import quizzesRouter from './routes/quizzes.js'; import usersRouter from './routes/users.js'; import uploadRouter from './routes/upload.js'; import gamesRouter from './routes/games.js'; import generateRouter from './routes/generate.js'; import sharedRouter from './routes/shared.js'; const app = express(); const PORT = process.env.PORT || 3001; app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "blob:"], connectSrc: ["'self'"], }, }, crossOriginEmbedderPolicy: false, })); const isDev = process.env.NODE_ENV !== 'production'; const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: isDev ? 1000 : 500, standardHeaders: true, legacyHeaders: false, message: { error: 'Too many requests, please try again later.' }, skip: (req) => req.path === '/health', }); const corsOrigins = (process.env.CORS_ORIGIN || 'http://localhost:5173').split(',').map(o => o.trim()); app.use(cors({ origin: corsOrigins.length === 1 ? corsOrigins[0] : corsOrigins, credentials: true, })); app.use(apiLimiter); const LOG_REQUESTS = process.env.LOG_REQUESTS === 'true'; app.use((req: Request, res: Response, next: NextFunction) => { if (LOG_REQUESTS && req.path !== '/health') { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`); }); } next(); }); app.use((req: Request, res: Response, next: NextFunction) => { express.json({ limit: '10mb' })(req, res, (err) => { if (err instanceof SyntaxError && 'body' in err) { res.status(400).json({ error: 'Invalid JSON' }); return; } if (err) { next(err); return; } next(); }); }); app.get('/health', (_req: Request, res: Response) => { try { db.prepare('SELECT 1').get(); res.json({ status: 'ok', timestamp: new Date().toISOString(), database: 'connected' }); } catch { res.status(503).json({ status: 'error', timestamp: new Date().toISOString(), database: 'disconnected' }); } }); app.use('/api/quizzes', quizzesRouter); app.use('/api/users', usersRouter); app.use('/api/upload', uploadRouter); app.use('/api/games', gamesRouter); app.use('/api/generate', generateRouter); app.use('/api/shared', sharedRouter); app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { console.error('Unhandled error:', err); res.status(500).json({ error: 'Internal server error' }); }); app.listen(PORT, () => { console.log(`Kaboot backend running on port ${PORT}`); console.log(`Database: ${process.env.DATABASE_PATH || 'default location'}`); console.log(`CORS origin: ${process.env.CORS_ORIGIN || 'http://localhost:5173'}`); }); process.on('SIGTERM', () => { console.log('Shutting down...'); db.close(); process.exit(0); });