import express from 'express'; import { createServer } from 'http'; import cookieParser from 'cookie-parser'; import helmet from 'helmet'; import { AppDataSource } from '../Infrastructure/ormconfig'; import userRouter from './routers/userRouter'; import organizationRouter from './routers/organizationRouter'; import deckRouter from './routers/deckRouter'; import chatRouter from './routers/chatRouter'; import contactRouter from './routers/contactRouter'; import adminRouter from './routers/adminRouter'; import deckImportExportRouter from './routers/deckImportExportRouter'; import gameRouter from './routers/gameRouter'; import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger'; import { WebSocketService } from '../Application/Services/WebSocketService'; import { GameWebSocketService } from '../Application/Services/GameWebSocketService'; import { container } from '../Application/Services/DIContainer'; import { GameRepository } from '../Infrastructure/Repository/GameRepository'; import { UserRepository } from '../Infrastructure/Repository/UserRepository'; import { RedisService } from '../Application/Services/RedisService'; import { setupSwagger } from './swagger/swaggerUiSetup'; const app = express(); const httpServer = createServer(app); const PORT = process.env.PORT || 3000; const isDevelopment = process.env.NODE_ENV === 'development'; const loggingService = LoggingService.getInstance(); logStartup('SerpentRace Backend starting up', { environment: process.env.NODE_ENV || 'development', port: PORT, nodeVersion: process.version, chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30' }); app.use(helmet({ contentSecurityPolicy: isDevelopment ? false : undefined })); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(cookieParser()); app.use(loggingService.requestLoggingMiddleware()); app.use((req, res, next) => { const origin = req.headers.origin; const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080', process.env.FRONTEND_URL]; if (!origin || allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin || '*'); } res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cookie'); if (req.method === 'OPTIONS') { res.status(200).end(); return; } next(); }); if (isDevelopment) { app.use((req, res, next) => { logRequest(`${req.method} ${req.path}`, req, res); next(); }); } // Setup Swagger documentation setupSwagger(app); app.get('/', (req, res) => { res.json({ service: 'SerpentRace Backend API', status: 'running', version: '1.0.0', endpoints: { swagger: '/api-docs', users: '/api/users', organizations: '/api/organizations', decks: '/api/decks', chats: '/api/chats', contacts: '/api/contacts', admin: '/api/admin', deckImportExport: '/api/deck-import-export', health: '/health' }, websocket: { enabled: true, events: [ 'chat:join', 'chat:leave', 'message:send', 'group:create', 'chat:direct', 'game:chat:create', 'chat:history' ] } }); }); app.get('/health', async (req, res) => { try { const isDbConnected = AppDataSource.isInitialized; res.json({ status: 'healthy', timestamp: new Date().toISOString(), service: 'SerpentRace Backend API', version: '1.0.0', environment: process.env.NODE_ENV || 'development', database: { connected: isDbConnected, type: AppDataSource.options.type }, websocket: { enabled: true }, uptime: process.uptime() }); } catch (error) { res.status(503).json({ status: 'unhealthy', timestamp: new Date().toISOString(), error: 'Service health check failed' }); } }); // API Routes app.use('/api/users', userRouter); app.use('/api/organizations', organizationRouter); app.use('/api/decks', deckRouter); app.use('/api/chats', chatRouter); app.use('/api/contacts', contactRouter); app.use('/api/admin', adminRouter); app.use('/api/deck-import-export', deckImportExportRouter); app.use('/api/games', gameRouter); // Global error handler (must be after routes) app.use(loggingService.errorLoggingMiddleware()); app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { logError('Global error handler caught unhandled error', error, req, res); // Don't expose internal error details in production const isDevelopment = process.env.NODE_ENV === 'development'; res.status(500).json({ error: 'Internal server error', timestamp: new Date().toISOString(), ...(isDevelopment && { details: error.message, stack: error.stack }) }); }); // Handle 404 routes app.use((req: express.Request, res: express.Response) => { res.status(404).json({ error: 'Route not found', path: req.originalUrl, method: req.method, timestamp: new Date().toISOString() }); }); // Initialize WebSocket service after database connection let webSocketService: WebSocketService; let gameWebSocketService: GameWebSocketService; // Initialize database connection AppDataSource.initialize() .then(() => { const dbOptions = AppDataSource.options as any; logConnection('Database connection established', 'postgresql', 'success', { type: dbOptions.type, host: dbOptions.host, database: dbOptions.database }); // Initialize WebSocket service after database is connected webSocketService = new WebSocketService(httpServer); logStartup('WebSocket service initialized', { chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30' }); // Initialize Game WebSocket service for /game namespace via DIContainer container.setSocketIO(webSocketService['io']); gameWebSocketService = container.gameWebSocketService; logStartup('Game WebSocket service initialized for /game namespace'); // Restore active games from snapshots (if any exist) gameWebSocketService.restoreAllActiveGames() .then(restoredCount => { if (restoredCount > 0) { logStartup(`Restored ${restoredCount} active game(s) from snapshots`); } }) .catch(error => { logError('Failed to restore games from snapshots', error); }); }) .catch((error) => { const dbOptions = AppDataSource.options as any; logConnection('Database connection failed', 'postgresql', 'failure', { error: error.message, type: dbOptions.type, host: dbOptions.host, database: dbOptions.database }); process.exit(1); }); // Start server with WebSocket support const server = httpServer.listen(PORT, () => { logStartup('Server started successfully', { port: PORT, environment: process.env.NODE_ENV || 'development', timestamp: new Date().toISOString(), endpoints: { health: `/health`, swagger: `/api-docs`, users: `/api/users`, organizations: `/api/organizations`, decks: `/api/decks`, chats: `/api/chats` }, websocket: { enabled: true, chatInactivityTimeout: `${process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'} minutes` } }); }); // Graceful shutdown const gracefulShutdown = async (signal: string) => { logStartup(`Received ${signal}. Shutting down gracefully...`); // Snapshot all active games before shutdown if (gameWebSocketService) { try { const snapshotCount = await gameWebSocketService.snapshotAllActiveGames(); logStartup(`Created ${snapshotCount} game snapshot(s) before shutdown`); } catch (error) { logError('Failed to snapshot games before shutdown', error as Error); } } server.close(() => { logStartup('HTTP server closed'); if (AppDataSource.isInitialized) { AppDataSource.destroy() .then(() => { logConnection('Database connection closed', 'postgresql', 'success'); process.exit(0); }) .catch((error) => { logError('Error during database shutdown', error); process.exit(1); }); } else { process.exit(0); } }); }; process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Handle uncaught exceptions process.on('uncaughtException', (error) => { logError('Uncaught Exception - Server will shut down', error); process.exit(1); }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { logError('Unhandled Rejection - Server will shut down', new Error(String(reason)), undefined, undefined); process.exit(1); }); // Export WebSocket services for game integration export { webSocketService, gameWebSocketService };