Files
SerpentRace/SerpentRace_Backend/src/Api/index.ts
T

271 lines
8.5 KiB
TypeScript

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 { 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
const gameRepository = new GameRepository();
const userRepository = new UserRepository();
const redisService = RedisService.getInstance();
gameWebSocketService = new GameWebSocketService(
webSocketService['io'], // Access the io property directly
gameRepository,
userRepository,
redisService
);
logStartup('Game WebSocket service initialized for /game namespace');
})
.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...`);
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 };