import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; import { createServer } from 'http'; import { Server, Socket } from 'socket.io'; import discoverRouter from './routes/discover'; import configRouter from './routes/config'; import statsRouter from './routes/stats'; import filesRouter from './routes/files'; import terminalRouter from './routes/terminal'; import authRouter from './routes/auth'; import topologyRouter from './routes/topology'; import hostsRouter from './routes/hosts'; import { requireAuth } from './middleware/auth'; import { getHostConfigs } from './config'; import { requestLogger, logger } from './middleware/requestLogger'; import { errorHandler } from './middleware/errorHandler'; const app = express(); const httpServer = createServer(app); const PORT = 3001; const allowedOrigins = ['http://localhost:3000', 'http://localhost:4173']; const corsOrigin = process.env.CORS_ORIGIN ? [process.env.CORS_ORIGIN, ...allowedOrigins] : allowedOrigins; // --- Socket.IO setup (websocket-engineer skill) --- const io = new Server(httpServer, { cors: { origin: corsOrigin, credentials: true, }, pingInterval: 25000, pingTimeout: 10000, }); io.on('connection', (socket: Socket) => { logger.info({ socketId: socket.id }, 'Client connected via WebSocket'); socket.on('disconnect', (reason: string) => { logger.info({ socketId: socket.id, reason }, 'Client disconnected'); }); }); // Export io so routes can emit events export { io }; // --- Security middleware (api-security-hardening skill) --- app.use(helmet({ contentSecurityPolicy: false, // Disable CSP for dev — configure per-environment in production })); // CORS — restrict to configured origins app.use(cors({ origin: corsOrigin, credentials: true, })); // Rate limiting — general API app.use('/api/', rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 200, message: { status: 'error', message: 'Too many requests, please try again later' }, standardHeaders: true, legacyHeaders: false, })); // Stricter rate limiting for discovery (expensive operation) app.use('/api/discover', rateLimit({ windowMs: 1 * 60 * 1000, // 1 minute max: 10, message: { status: 'error', message: 'Discovery rate limited — max 10 per minute' }, standardHeaders: true, legacyHeaders: false, })); // --- Body parsing --- app.use(express.json({ limit: '1mb' })); // --- Observability (observability-monitoring skill) --- app.use(requestLogger); // --- Health check --- app.get('/api/health', (_req, res) => { res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString(), connections: io.engine.clientsCount, }); }); // --- Debug endpoint (dev only) --- if (process.env.NODE_ENV !== 'production') { app.get('/api/debug-config', async (_req, res) => { const hosts = await getHostConfigs(); res.json({ hosts }); }); } // --- Public Routes --- app.use('/api', authRouter); // --- Protected Routes --- app.use('/api', requireAuth, discoverRouter); app.use('/api', requireAuth, configRouter); app.use('/api', requireAuth, statsRouter); app.use('/api', requireAuth, filesRouter); app.use('/api', requireAuth, terminalRouter); app.use('/api', requireAuth, topologyRouter); app.use('/api', requireAuth, hostsRouter); // --- Global error handler (must be last) --- app.use(errorHandler); // --- Start server --- httpServer.listen(PORT, () => { logger.info(`Server running on http://localhost:${PORT}`); });