Files
hotell777_260507/server.js
2026-05-11 23:44:01 +05:00

707 lines
27 KiB
JavaScript

const express = require('express');
const path = require('path');
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
const sharp = require('sharp');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const client = require('prom-client');
const multer = require('multer');
require('dotenv').config();
const config = require('./config');
const app = express();
const PORT = process.env.PORT || 3000;
const API_KEY = process.env.HOTEL777KEY;
const ADMIN_LOGIN = process.env.ADMIN_LOGIN;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
const JWT_SECRET = process.env.JWT_SECRET;
const MONITORING_USER = process.env.MONITORING_USER || 'monitoring';
const MONITORING_PASSWORD = process.env.MONITORING_PASSWORD || 'monitoring123';
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
const roomsUploadsDir = path.join(uploadsDir, 'rooms');
if (!fs.existsSync(roomsUploadsDir)) fs.mkdirSync(roomsUploadsDir, { recursive: true });
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, roomsUploadsDir),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
const timestamp = Date.now();
cb(null, `${timestamp}${ext}`);
}
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowed = ['.jpg', '.jpeg', '.png', '.webp'];
const ext = path.extname(file.originalname).toLowerCase();
if (allowed.includes(ext)) cb(null, true);
else cb(new Error('Только изображения: jpg, jpeg, png, webp'));
}
});
if (!JWT_SECRET) {
console.error('FATAL: JWT_SECRET environment variable not set');
process.exit(1);
}
if (JWT_SECRET === 'change-this-secret-in-production-min-32-chars') {
console.warn('WARNING: Using default JWT_SECRET. Change it in production!');
}
const register = new client.Registry();
client.collectDefaultMetrics({ register });
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'path', 'status'],
registers: [register]
});
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'path', 'status'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
registers: [register]
});
const activeConnections = new client.Gauge({
name: 'active_connections',
help: 'Number of active connections',
registers: [register]
});
const dbQueryDuration = new client.Histogram({
name: 'db_query_duration_seconds',
help: 'Duration of database queries in seconds',
labelNames: ['operation'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
registers: [register]
});
const bookingsTotal = new client.Gauge({
name: 'bookings_total',
help: 'Total number of bookings',
labelNames: ['status'],
registers: [register]
});
const roomAvailability = new client.Gauge({
name: 'room_availability',
help: 'Number of available rooms by type',
labelNames: ['type'],
registers: [register]
});
const loginAttempts = new client.Gauge({
name: 'login_attempts',
help: 'Number of login attempts',
registers: [register]
});
if (!API_KEY) {
console.error('FATAL: HOTEL777KEY environment variable not set');
process.exit(1);
}
app.use(express.json({ limit: '10kb' }));
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') return res.sendStatus(200);
next();
});
app.use(express.static(path.join(__dirname, 'public')));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use((req, res, next) => {
const start = Date.now();
activeConnections.inc();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const path = req.route ? req.route.path : req.path;
httpRequestsTotal.inc({ method: req.method, path: path, status: res.statusCode });
httpRequestDuration.observe({ method: req.method, path: path, status: res.statusCode }, duration);
activeConnections.dec();
});
next();
});
app.use('/metrics', (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
res.set('WWW-Authenticate', 'Basic realm="Monitoring"');
return res.status(401).send('Authentication required');
}
const auth = new Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
const user = auth[0];
const pass = auth[1];
if (user === MONITORING_USER && pass === MONITORING_PASSWORD) {
res.set('Content-Type', register.contentType);
register.metrics().then(metrics => res.end(metrics)).catch(err => res.status(500).send(err.message));
} else {
res.set('WWW-Authenticate', 'Basic realm="Monitoring"');
return res.status(401).send('Authentication required');
}
});
function updateMetrics() {
db.all(`SELECT status, COUNT(*) as count FROM bookings GROUP BY status`, [], (err, rows) => {
if (!err && rows) {
rows.forEach(row => {
bookingsTotal.set({ status: row.status || 'unknown' }, row.count);
});
}
});
db.all(`SELECT type, rooms_count FROM rooms WHERE is_active = 1`, [], (err, rows) => {
if (!err && rows) {
rows.forEach(row => {
roomAvailability.set({ type: row.type }, row.rooms_count);
});
}
});
}
setInterval(updateMetrics, 30000);
setTimeout(updateMetrics, 5000);
const dataDir = path.join(__dirname, 'data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir);
const dbPath = path.join(dataDir, 'bookings.db');
const db = new sqlite3.Database(dbPath);
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS bookings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT NOT NULL,
adults INTEGER NOT NULL,
children INTEGER NOT NULL,
checkin_date TEXT NOT NULL,
checkout_date TEXT NOT NULL,
wishes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
const columnsToAdd = [
'wishes TEXT',
'status TEXT DEFAULT "новая"',
'room_type TEXT',
'room_id INTEGER',
'comment TEXT',
'base_price REAL',
'discount_percent INTEGER DEFAULT 0',
'discount_amount REAL DEFAULT 0',
'total_price REAL',
'promocode_id INTEGER'
];
function addColumnSafely(columns, index) {
if (index >= columns.length) {
setupPromocodesTable();
return;
}
db.all("PRAGMA table_info(bookings)", [], (err, cols) => {
if (err) {
setupPromocodesTable();
return;
}
const colNames = cols.map(c => c.name);
const columnDef = columns[index];
const colName = columnDef.split(' ')[0];
if (!colNames.includes(colName)) {
db.run(`ALTER TABLE bookings ADD COLUMN ${columnDef}`, (err) => {
if (err && !err.message.includes('duplicate')) {
console.log('Migration note (ignore if exists):', err.message);
}
});
}
addColumnSafely(columns, index + 1);
});
}
function setupPromocodesTable() {
db.run(`CREATE TABLE IF NOT EXISTS promocodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
discount_percent INTEGER NOT NULL CHECK(discount_percent BETWEEN 1 AND 99),
valid_from DATETIME,
valid_to DATETIME,
valid_days INTEGER,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
setupRoomsTable();
}
function setupRoomsTable() {
db.run(`CREATE TABLE IF NOT EXISTS rooms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
rooms_count INTEGER DEFAULT 1,
area_sqm INTEGER DEFAULT 20,
max_guests INTEGER DEFAULT 2,
furniture TEXT DEFAULT '[]',
amenities TEXT DEFAULT '[]',
floors TEXT DEFAULT '[]',
price_per_night INTEGER NOT NULL,
image_path TEXT,
extra_beds INTEGER DEFAULT 0,
extra_bed_price INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.all("PRAGMA table_info(rooms)", [], (err, cols) => {
if (err) { setupBookingHistoryTable(); return; }
const colNames = cols.map(c => c.name);
const migrations = [
['area_sqm', 'INTEGER DEFAULT 20'],
['furniture', "TEXT DEFAULT '[]'"],
['amenities', "TEXT DEFAULT '[]'"],
['floors', "TEXT DEFAULT '[]'"],
['extra_beds', 'INTEGER DEFAULT 0'],
['extra_bed_price', 'INTEGER DEFAULT 0']
];
let pending = migrations.length;
if (pending === 0) {
migratePricePerNight();
return;
}
migrations.forEach(([colName, colDef], i) => {
if (!colNames.includes(colName)) {
db.run(`ALTER TABLE rooms ADD COLUMN ${colName} ${colDef}`, (err) => {
if (err && !err.message.includes('duplicate') && !err.message.includes('NOT NULL')) {
console.log('Room migration note:', err.message);
}
});
}
if (--pending === 0) migratePricePerNight();
});
});
}
function migratePricePerNight() {
db.all("PRAGMA table_info(rooms)", [], (err, cols) => {
if (err) { setupBookingHistoryTable(); return; }
const colNames = cols.map(c => c.name);
if (!colNames.includes('price_per_night')) {
db.run(`ALTER TABLE rooms ADD COLUMN price_per_night INTEGER DEFAULT 0`, (err) => {
if (err && !err.message.includes('duplicate') && !err.message.includes('NOT NULL')) {
console.log('Room migration note (add price_per_night):', err.message);
}
});
}
db.run(`CREATE TABLE rooms_backup AS SELECT id, type, name, description, rooms_count, area_sqm, max_guests, furniture, amenities, floors, price_per_night, image_path, extra_beds, extra_bed_price, is_active, created_at FROM rooms`, (err) => {
if (err) {
console.log('Room backup failed:', err.message);
setupBookingHistoryTable();
return;
}
db.run(`DROP TABLE rooms`, (err) => {
if (err) {
console.log('Room drop failed:', err.message);
setupBookingHistoryTable();
return;
}
db.run(`CREATE TABLE rooms (id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, name TEXT NOT NULL, description TEXT, rooms_count INTEGER DEFAULT 1, area_sqm INTEGER DEFAULT 20, max_guests INTEGER DEFAULT 2, furniture TEXT DEFAULT '[]', amenities TEXT DEFAULT '[]', floors TEXT DEFAULT '[]', price_per_night INTEGER NOT NULL, image_path TEXT, extra_beds INTEGER DEFAULT 0, extra_bed_price INTEGER DEFAULT 0, is_active INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)`, (err) => {
if (err) {
console.log('Room recreate failed:', err.message);
setupBookingHistoryTable();
return;
}
db.run(`INSERT INTO rooms (id, type, name, description, rooms_count, area_sqm, max_guests, furniture, amenities, floors, price_per_night, image_path, extra_beds, extra_bed_price, is_active, created_at) SELECT id, type, name, description, rooms_count, area_sqm, max_guests, COALESCE(furniture, '[]'), COALESCE(amenities, '[]'), COALESCE(floors, '[]'), COALESCE(price_per_night, 0) as price_per_night, image_path, COALESCE(extra_beds, 0), COALESCE(extra_bed_price, 0), is_active, created_at FROM rooms_backup`, (err) => {
if (err) console.log('Room data restore failed:', err.message);
db.run(`DROP TABLE rooms_backup`, (err) => {});
setupBookingHistoryTable();
});
});
});
});
});
}
function setupBookingHistoryTable() {
db.run(`CREATE TABLE IF NOT EXISTS booking_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
booking_id INTEGER NOT NULL,
user_id INTEGER,
user_login TEXT,
field TEXT NOT NULL,
old_value TEXT,
new_value TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (booking_id) REFERENCES bookings(id)
)`);
setupUsersTable();
}
function setupUsersTable() {
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
full_name TEXT,
email TEXT,
role TEXT NOT NULL DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
setupSettingsTable();
}
function setupSettingsTable() {
db.run(`CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err) {
console.error('CREATE TABLE settings FAILED:', err.message);
} else {
console.log('CREATE TABLE settings SUCCESS');
}
});
setupReviewsTable();
}
function setupReviewsTable() {
db.run(`CREATE TABLE IF NOT EXISTS reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author_name TEXT NOT NULL,
country TEXT NOT NULL,
country_code TEXT,
city TEXT NOT NULL,
stars REAL NOT NULL CHECK(stars >= 0 AND stars <= 5),
text TEXT NOT NULL,
review_code TEXT NOT NULL,
ip_address TEXT,
is_approved INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err && !err.message.includes('already exists')) {
console.error('Reviews table error:', err.message);
}
db.run(`PRAGMA foreign_keys = ON`, (err) => {
if (err) console.error('Foreign keys error:', err.message);
});
db.all("PRAGMA table_info(reviews)", [], (err, cols) => {
if (err || !cols) return;
const colNames = cols.map(c => c.name);
if (!colNames.includes('country_code')) {
db.run("ALTER TABLE reviews ADD COLUMN country_code TEXT", (err) => {
if (err) console.log('Migration: country_code column (ignore if exists):', err.message);
else console.log('Migration: country_code column added');
});
}
});
});
}
addColumnSafely(columnsToAdd, 0);
db.run(`CREATE INDEX IF NOT EXISTS idx_bookings_checkin ON bookings(checkin_date)`, (err) => {
if (err) console.log('Index idx_bookings_checkin:', err.message);
});
db.run(`CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status)`, (err) => {
if (err) console.log('Index idx_bookings_status:', err.message);
});
});
const modules = {};
const authModule = require('./modules/auth');
const bookingsModule = require('./modules/bookings');
const adminBookingsModule = require('./modules/adminBookings');
const promocodesModule = require('./modules/promocodes');
const roomsModule = require('./modules/rooms');
const usersModule = require('./modules/users');
const settingsModule = require('./modules/settings');
const reviewsModule = require('./modules/reviews');
const translationsModule = require('./modules/translations');
const backupModule = require('./modules/backup');
const { runStartupTests } = require('./tests/runStartupTests');
modules.auth = authModule;
modules.bookings = bookingsModule;
modules.promocodes = promocodesModule;
modules.rooms = roomsModule;
modules.users = usersModule;
modules.adminBookings = adminBookingsModule;
modules.settings = settingsModule;
modules.reviews = reviewsModule;
modules.translations = translationsModule;
modules.backup = backupModule;
authModule.init(db, JWT_SECRET);
bookingsModule.init(db);
adminBookingsModule.init(db);
promocodesModule.init(db);
roomsModule.init(db);
usersModule.init(db, bcrypt);
settingsModule.init(db);
reviewsModule.init(db, settingsModule);
backupModule.init(db, dbPath);
function initDefaultRooms() {
db.get("SELECT COUNT(*) as count FROM rooms", (err, row) => {
if (err) return console.error('Check rooms count error:', err);
if (row.count > 0) return;
const stmt = db.prepare(`INSERT INTO rooms (type, name, description, rooms_count, area_sqm, max_guests, furniture, amenities, floors, price_per_night, image_path, extra_beds, extra_bed_price, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`);
config.DEFAULT_ROOMS.forEach(r => {
stmt.run(
r.type,
r.name,
r.description,
r.rooms_count,
r.area_sqm,
r.max_guests,
JSON.stringify(r.furniture || []),
JSON.stringify(r.amenities || []),
JSON.stringify(r.floors || []),
r.price_per_night,
r.image_path,
r.extra_beds || 0,
r.extra_bed_price || 0
);
});
stmt.finalize();
console.log('✅ Default rooms initialized');
});
}
function syncAdmin() {
if (!ADMIN_LOGIN || !ADMIN_PASSWORD) {
console.warn('WARNING: ADMIN_LOGIN or ADMIN_PASSWORD not set, skipping admin sync');
return;
}
const hash = bcrypt.hashSync(ADMIN_PASSWORD, 10);
db.get(`SELECT id, role FROM users WHERE login = ?`, [ADMIN_LOGIN], (err, row) => {
if (err) { console.error('Admin sync error:', err); return; }
if (row) {
db.run(`UPDATE users SET password_hash = ?, role = 'admin' WHERE login = ?`, [hash, ADMIN_LOGIN], (err) => {
if (err) console.error('Admin update error:', err);
else console.log(`✅ Superadmin "${ADMIN_LOGIN}" updated from .env`);
});
} else {
db.run(`INSERT INTO users (login, password_hash, full_name, email, role) VALUES (?, ?, 'Администратор', NULL, 'admin')`,
[ADMIN_LOGIN, hash], (err) => {
if (err) console.error('Admin creation error:', err);
else console.log(`✅ Superadmin "${ADMIN_LOGIN}" created from .env`);
});
}
});
}
setTimeout(() => {
initDefaultRooms();
syncAdmin();
}, 500);
authModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
bookingsModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
adminBookingsModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
promocodesModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
roomsModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin, upload);
usersModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
settingsModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
reviewsModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
backupModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
app.post('/api/promocodes/validate', (req, res) => {
const { code, room_type, checkin, checkout, guests } = req.body;
if (!code) return res.status(400).json({ error: 'Promocode required' });
db.get(`SELECT * FROM promocodes WHERE code = ? AND is_active = 1`, [code], (err, promo) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!promo) return res.status(404).json({ error: 'Invalid or expired promocode' });
const configModule = require('./config');
const guestsCount = parseInt(guests) || 1;
const basePrice = configModule.calculateBasePrice(room_type, checkin, checkout) * guestsCount;
const discountAmount = Math.round(basePrice * promo.discount_percent / 100);
const totalPrice = basePrice - discountAmount;
res.json({
valid: true,
discount_percent: promo.discount_percent,
base_price: basePrice,
discount_amount: discountAmount,
total_price: totalPrice,
code: promo.code
});
});
});
app.get('/api/translations/:lang', (req, res) => {
const lang = req.params.lang;
const translations = translationsModule.getTranslations(lang);
if (!translations) {
return res.status(404).json({ error: 'Language not found' });
}
res.json(translations);
});
app.get('/api/countries-cities', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'data', 'countries-cities.js'));
});
app.get('/api/countries', (req, res) => {
const countriesPath = path.join(__dirname, 'public', 'data', 'countries.js');
fs.readFile(countriesPath, 'utf8', (err, data) => {
if (err) return res.status(500).json({ error: 'File not found' });
const match = data.match(/const\s+COUNTRIES\s*=\s*(\[.*\]);/s);
if (match) {
try {
let jsonStr = match[1];
jsonStr = jsonStr.replace(/'/g, '"');
const countries = JSON.parse(jsonStr);
res.json(countries);
} catch (e) {
res.status(500).json({ error: 'Parse error' });
}
} else {
res.status(500).json({ error: 'Parse error' });
}
});
});
function safeJsonString(str) {
return str.replace(/'/g, "'").replace(/\\"/g, '"');
}
app.get('/api/cities/:countryCode', (req, res) => {
const countryCode = req.params.countryCode;
const citiesPath = path.join(__dirname, 'public', 'data', 'cities', `${countryCode}.js`);
const majorCitiesPath = path.join(__dirname, 'public', 'data', 'cities', 'major.js');
const filePath = fs.existsSync(citiesPath) ? citiesPath : (fs.existsSync(majorCitiesPath) ? majorCitiesPath : null);
if (!filePath) {
return res.json({ cities: [], popular: [], countryCode });
}
try {
const data = fs.readFileSync(filePath, 'utf8');
let cities = [];
const arrayMatch = data.match(/const\s+CITIES_\w+\s*=\s*(\[.*?\]);/s);
if (arrayMatch) {
try {
cities = JSON.parse(safeJsonString(arrayMatch[1]));
} catch (e) {
const jsMatch = arrayMatch[1].replace(/'/g, '"');
try {
cities = JSON.parse(jsMatch);
} catch (e2) {
console.error('Failed to parse cities:', e2);
}
}
}
res.json({ cities, popular: [], countryCode });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.get('/uploads/rooms/:filename', (req, res) => {
const filePath = path.join(roomsUploadsDir, req.params.filename);
if (fs.existsSync(filePath)) {
res.sendFile(filePath);
} else {
res.status(404).json({ error: 'File not found' });
}
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'admin.html'));
});
async function convertImages() {
const imgDir = path.join(__dirname, 'public', 'img');
if (!fs.existsSync(imgDir)) {
console.log('Папка img не найдена, пропускаем конвертацию.');
return;
}
const files = fs.readdirSync(imgDir);
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (ext === '.jpg' || ext === '.jpeg' || ext === '.png') {
const name = path.parse(file).name;
const webpPath = path.join(imgDir, `${name}.webp`);
if (!fs.existsSync(webpPath)) {
try {
await sharp(path.join(imgDir, file))
.webp({ quality: 85 })
.toFile(webpPath);
console.log(`✅ Сконвертировано: ${file} -> ${name}.webp`);
} catch (err) {
console.error(`❌ Ошибка при конвертации ${file}:`, err);
}
}
}
}
}
convertImages().then(() => {
const server = app.listen(PORT, async () => {
console.log('✅ HOTEL777KEY is set');
console.log('📊 Prometheus metrics available at: http://localhost:' + PORT + '/metrics');
const monitoringSet = MONITORING_PASSWORD ? 'configured' : 'NOT SET';
console.log('🔐 Monitoring auth: ' + monitoringSet);
console.log('');
await runStartupTests(db, modules);
console.log(`✅ Hotel 777 server running on http://localhost:${PORT}`);
});
function gracefulShutdown(signal) {
console.log(`\n${signal} received. Shutting down gracefully...`);
server.close(() => {
db.close((err) => {
if (err) {
console.error('Error closing database:', err);
} else {
console.log('Database connection closed.');
}
console.log('Server closed.');
process.exit(0);
});
});
setTimeout(() => {
console.error('Forced shutdown after timeout.');
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
});