mirror of
https://dev.azure.com/tstanciu94/PhantomMind/_git/Bitip
synced 2025-10-13 01:52:19 +03:00
165 lines
4.6 KiB
TypeScript
165 lines
4.6 KiB
TypeScript
// Copyright (c) 2025 Tudor Stanciu
|
|
|
|
import express from 'express';
|
|
import cors from 'cors';
|
|
import helmet from 'helmet';
|
|
import compression from 'compression';
|
|
import path from 'path';
|
|
import { setTimeout } from 'timers';
|
|
import apiRoutes from './routes/api';
|
|
import apiKeyAuth from './middleware/auth';
|
|
import dynamicRateLimit from './middleware/rateLimit';
|
|
import { errorHandler } from './middleware/errorHandler';
|
|
import config from './services/config';
|
|
import logger from './services/logger';
|
|
import { generateRuntimeConfig } from './services/runtimeConfig';
|
|
import { healthCheckHandler } from './handlers/healthHandler';
|
|
import { paths } from './utils/paths';
|
|
|
|
const app = express();
|
|
|
|
// Security middleware
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: config.enableHttpsSecurity
|
|
? {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'", 'https://unpkg.com'],
|
|
scriptSrc: ["'self'"],
|
|
imgSrc: ["'self'", 'data:', 'https:'],
|
|
connectSrc: ["'self'"],
|
|
fontSrc: ["'self'"],
|
|
objectSrc: ["'none'"],
|
|
mediaSrc: ["'self'"],
|
|
frameSrc: ["'none'"],
|
|
},
|
|
}
|
|
: false,
|
|
strictTransportSecurity: config.enableHttpsSecurity,
|
|
crossOriginOpenerPolicy: config.enableHttpsSecurity,
|
|
crossOriginResourcePolicy: config.enableHttpsSecurity,
|
|
originAgentCluster: false, // Disable Origin-Agent-Cluster header to avoid browser warnings
|
|
})
|
|
);
|
|
|
|
app.use(
|
|
cors({
|
|
origin: process.env.NODE_ENV === 'production' ? false : true,
|
|
credentials: true,
|
|
})
|
|
);
|
|
|
|
app.use(compression());
|
|
app.use(express.json({ limit: '10mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
|
|
// Request logging
|
|
app.use((req, res, next) => {
|
|
logger.debug('Incoming request', {
|
|
method: req.method,
|
|
path: req.path,
|
|
ip: req.ip,
|
|
userAgent: req.headers['user-agent'],
|
|
});
|
|
next();
|
|
});
|
|
|
|
// Apply base path if configured
|
|
const basePath = config.basePath.endsWith('/')
|
|
? config.basePath.slice(0, -1)
|
|
: config.basePath;
|
|
|
|
// Serve static frontend files
|
|
const frontendPath = paths.frontendDir;
|
|
|
|
// Generate runtime configuration (env.js) at startup
|
|
generateRuntimeConfig(frontendPath, basePath);
|
|
|
|
// Health check endpoint at root (always accessible for Docker HEALTHCHECK)
|
|
app.get('/api/health', healthCheckHandler);
|
|
|
|
// API routes with authentication and rate limiting
|
|
app.use(`${basePath}/api`, apiKeyAuth, dynamicRateLimit, apiRoutes);
|
|
|
|
// Serve static frontend files
|
|
app.use(basePath, express.static(frontendPath));
|
|
|
|
// Fallback to index.html for client-side routing (Express 5 syntax)
|
|
// Named wildcard required in path-to-regexp v8: /*path matches any path
|
|
app.get(`${basePath}/*path`, (_req, res): void => {
|
|
res.sendFile(path.join(frontendPath, 'index.html'));
|
|
});
|
|
|
|
// Global error handler (must be last middleware)
|
|
app.use(errorHandler);
|
|
|
|
// Start server
|
|
const server = app.listen(config.port, () => {
|
|
logger.info('Bitip GeoIP Service started', {
|
|
port: config.port,
|
|
basePath: config.basePath,
|
|
environment: process.env.NODE_ENV || 'development',
|
|
dbPath: config.maxmindDbPath,
|
|
});
|
|
});
|
|
|
|
// Graceful shutdown
|
|
let isShuttingDown = false;
|
|
|
|
const gracefulShutdown = async (signal: string): Promise<void> => {
|
|
if (isShuttingDown) {
|
|
logger.warn(`Shutdown already in progress, ignoring ${signal}`);
|
|
return;
|
|
}
|
|
isShuttingDown = true;
|
|
|
|
logger.info(`Received ${signal}, shutting down gracefully`);
|
|
|
|
// Set timeout slightly less than Docker's default (10s)
|
|
const forceExitTimeout = setTimeout(() => {
|
|
logger.error('Forced shutdown after timeout');
|
|
process.exit(1);
|
|
}, 8000);
|
|
|
|
try {
|
|
// Stop accepting new connections
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close(err => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
logger.info('Server closed successfully');
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Flush logs before exit
|
|
await logger.flush();
|
|
logger.info('Shutdown complete');
|
|
|
|
clearTimeout(forceExitTimeout);
|
|
process.exit(0);
|
|
} catch (error) {
|
|
logger.error('Error during graceful shutdown', error as Error);
|
|
clearTimeout(forceExitTimeout);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
// Docker sends SIGTERM for graceful shutdown
|
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
// Ctrl+C in development
|
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
// Nodemon uses SIGUSR2 for restart
|
|
process.on('SIGUSR2', () => gracefulShutdown('SIGUSR2')); // nodemon uses SIGUSR2
|
|
|
|
// Handle server errors
|
|
server.on('error', (error: Error) => {
|
|
logger.error('Server error', error);
|
|
process.exit(1);
|
|
});
|
|
|
|
export default app;
|