251 lines
7.5 KiB
TypeScript
251 lines
7.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 { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
|
|
import { WebSocketService } from '../Application/Services/WebSocketService';
|
|
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'];
|
|
|
|
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);
|
|
|
|
// 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;
|
|
|
|
// 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'
|
|
});
|
|
})
|
|
.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 service for game integration
|
|
export { webSocketService };
|