114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
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);
|
|
});
|