177 lines
6.4 KiB
TypeScript
177 lines
6.4 KiB
TypeScript
import http from 'http';
|
|
import net from 'net';
|
|
import dotenv from 'dotenv';
|
|
import path from 'path';
|
|
|
|
// Load environment variables from .env file FIRST
|
|
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
|
// Stop queue metrics collection on shutdown
|
|
// Note: This is imported statically but doesn't trigger database/queue activity until called
|
|
import { stopQueueMetrics } from './utils/queueMetrics';
|
|
|
|
const PORT: number = parseInt(process.env.PORT || '5000', 10);
|
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
const MAX_PORT_ATTEMPTS = isDev ? 6 : 1; // In dev try PORT..PORT+5 if in use
|
|
|
|
/**
|
|
* Check if a port is free (no one listening).
|
|
*/
|
|
function isPortFree(port: number): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const socket = new net.Socket();
|
|
const onError = () => {
|
|
socket.destroy();
|
|
resolve(true); // Error (e.g. ECONNREFUSED) means nothing listening → port free
|
|
};
|
|
socket.setTimeout(200);
|
|
socket.once('error', onError);
|
|
socket.once('timeout', () => {
|
|
socket.destroy();
|
|
resolve(true);
|
|
});
|
|
socket.connect(port, '127.0.0.1', () => {
|
|
socket.destroy();
|
|
resolve(false); // Connected → something is listening → port in use
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Find first free port in [startPort, startPort + maxAttempts).
|
|
*/
|
|
async function findFreePort(startPort: number, maxAttempts: number): Promise<number> {
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
const port = startPort + i;
|
|
if (await isPortFree(port)) return port;
|
|
}
|
|
return startPort; // Fallback to original; listen will then fail with EADDRINUSE
|
|
}
|
|
|
|
// Start server
|
|
const startServer = async (): Promise<void> => {
|
|
try {
|
|
// Initialize Google Secret Manager before starting server
|
|
// This will merge secrets from GCS into process.env if enabled
|
|
const { initializeGoogleSecretManager } = require('./services/googleSecretManager.service');
|
|
console.log('🔐 Initializing secrets...');
|
|
await initializeGoogleSecretManager();
|
|
|
|
const { default: app, initializeAppDatabase } = require('./app');
|
|
const { initSocket } = require('./realtime/socket');
|
|
|
|
// Initialize database connection explicitly after secrets are loaded
|
|
await initializeAppDatabase();
|
|
|
|
require('./queues/tatWorker'); // Initialize TAT worker
|
|
const { logTatConfig } = require('./config/tat.config');
|
|
const { logSystemConfig } = require('./config/system.config');
|
|
const { initializeHolidaysCache } = require('./utils/tatTimeUtils');
|
|
const { seedDefaultConfigurations } = require('./services/configSeed.service');
|
|
const { startPauseResumeJob } = require('./jobs/pauseResumeJob');
|
|
require('./queues/pauseResumeWorker'); // Initialize pause resume worker
|
|
const { initializeQueueMetrics } = require('./utils/queueMetrics');
|
|
const { emailService } = require('./services/email.service');
|
|
|
|
// Initialize email service after secrets are loaded
|
|
try {
|
|
await emailService.initialize();
|
|
console.log('📧 Email service initialized');
|
|
} catch (error) {
|
|
console.warn('⚠️ Email service initialization warning (will use test account if SMTP not configured):', error);
|
|
}
|
|
|
|
const server = http.createServer(app);
|
|
initSocket(server);
|
|
|
|
// Seed default configurations if table is empty
|
|
try {
|
|
await seedDefaultConfigurations();
|
|
} catch (error) {
|
|
console.error('⚠️ Configuration seeding error:', error);
|
|
}
|
|
|
|
// Seed default activity types if table is empty
|
|
const { seedDefaultActivityTypes } = require('./services/activityTypeSeed.service');
|
|
try {
|
|
await seedDefaultActivityTypes();
|
|
} catch (error) {
|
|
console.error('⚠️ Activity type seeding error:', error);
|
|
}
|
|
|
|
// Ensure demo admin user exists (admin@example.com / Admin@123)
|
|
const { ensureDemoAdminUser } = require('./scripts/seed-admin-user');
|
|
try {
|
|
await ensureDemoAdminUser();
|
|
} catch (error) {
|
|
console.warn('⚠️ Demo admin user setup warning:', error);
|
|
}
|
|
|
|
// Initialize holidays cache for TAT calculations
|
|
try {
|
|
await initializeHolidaysCache();
|
|
} catch (error) {
|
|
// Silently fall back to weekends-only TAT calculation
|
|
}
|
|
|
|
// Start scheduled jobs
|
|
startPauseResumeJob();
|
|
const { startForm16NotificationJobs } = require('./jobs/form16NotificationJob');
|
|
startForm16NotificationJobs();
|
|
const { startForm16SapResponseJob } = require('./jobs/form16SapResponseJob');
|
|
startForm16SapResponseJob();
|
|
const { startForm16ArchiveJob } = require('./services/form16Archive.service');
|
|
startForm16ArchiveJob();
|
|
|
|
// Initialize queue metrics collection for Prometheus
|
|
initializeQueueMetrics();
|
|
|
|
// In development, if default port is in use (e.g. previous run or another app), try next ports
|
|
let portToUse = PORT;
|
|
if (isDev) {
|
|
const freePort = await findFreePort(PORT, MAX_PORT_ATTEMPTS);
|
|
if (freePort !== PORT) {
|
|
console.warn(`⚠️ Port ${PORT} is in use. Using port ${freePort} instead.`);
|
|
console.warn(` Update frontend .env VITE_API_BASE_URL to http://localhost:${freePort}/api/v1 if needed.`);
|
|
portToUse = freePort;
|
|
}
|
|
}
|
|
|
|
server.once('error', (err: NodeJS.ErrnoException) => {
|
|
if (err.code === 'EADDRINUSE') {
|
|
console.error('');
|
|
console.error('❌ Port ' + portToUse + ' is already in use.');
|
|
console.error(' Another process (often a previous backend instance) is using it.');
|
|
console.error(' Windows: netstat -ano | findstr :' + portToUse);
|
|
console.error(' Then: taskkill /PID <PID> /F');
|
|
console.error(' Or run in a single terminal and avoid starting backend twice.');
|
|
console.error('');
|
|
} else {
|
|
console.error('❌ Server error:', err);
|
|
}
|
|
process.exit(1);
|
|
});
|
|
|
|
server.listen(portToUse, () => {
|
|
console.log(`🚀 Server running on port ${portToUse} | ${process.env.NODE_ENV || 'development'}`);
|
|
console.log(` API base: http://localhost:${portToUse}/api/v1 (ensure frontend uses this and CORS allows your origin)`);
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Unable to start server:', error);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', () => {
|
|
console.log('🛑 SIGTERM signal received: closing HTTP server');
|
|
stopQueueMetrics();
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
console.log('🛑 SIGINT signal received: closing HTTP server');
|
|
stopQueueMetrics();
|
|
process.exit(0);
|
|
});
|
|
|
|
startServer(); |