628 lines
24 KiB
JavaScript
628 lines
24 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');
|
|
const helmet = require('helmet');
|
|
const cookieParser = require('cookie-parser');
|
|
require('dotenv').config();
|
|
|
|
const config = require('./config');
|
|
const { ensureEnvSecret, initDatabase } = require('./modules/database');
|
|
|
|
ensureEnvSecret('HOTEL777KEY', 'HOTEL777KEY');
|
|
ensureEnvSecret('JWT_SECRET', 'JWT_SECRET');
|
|
|
|
const app = express();
|
|
app.set('trust proxy', true);
|
|
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 roomsUploadsDir = path.join(__dirname, 'data', 'room_images');
|
|
if (!fs.existsSync(roomsUploadsDir)) fs.mkdirSync(roomsUploadsDir, { recursive: true });
|
|
const activityUploadsDir = path.join(__dirname, 'data', 'activity_images');
|
|
if (!fs.existsSync(activityUploadsDir)) fs.mkdirSync(activityUploadsDir, { recursive: true });
|
|
const heroImagesDir = path.join(__dirname, 'data', 'hero_images');
|
|
if (!fs.existsSync(heroImagesDir)) fs.mkdirSync(heroImagesDir, { recursive: true });
|
|
const heroVideosDir = path.join(__dirname, 'data', 'hero_videos');
|
|
if (!fs.existsSync(heroVideosDir)) fs.mkdirSync(heroVideosDir, { 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'));
|
|
}
|
|
});
|
|
|
|
const activityStorage = multer.diskStorage({
|
|
destination: (req, file, cb) => cb(null, activityUploadsDir),
|
|
filename: (req, file, cb) => {
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
const timestamp = Date.now();
|
|
cb(null, `${timestamp}${ext}`);
|
|
}
|
|
});
|
|
|
|
const uploadActivity = multer({
|
|
storage: activityStorage,
|
|
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'));
|
|
}
|
|
});
|
|
|
|
const heroImageStorage = multer.diskStorage({
|
|
destination: (req, file, cb) => cb(null, heroImagesDir),
|
|
filename: (req, file, cb) => {
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
const timestamp = Date.now();
|
|
cb(null, `${timestamp}${ext}`);
|
|
}
|
|
});
|
|
|
|
const uploadHeroImage = multer({
|
|
storage: heroImageStorage,
|
|
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'));
|
|
}
|
|
});
|
|
|
|
const heroVideoStorage = multer.diskStorage({
|
|
destination: (req, file, cb) => cb(null, heroVideosDir),
|
|
filename: (req, file, cb) => {
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
const timestamp = Date.now();
|
|
cb(null, `${timestamp}${ext}`);
|
|
}
|
|
});
|
|
|
|
const uploadHeroVideo = multer({
|
|
storage: heroVideoStorage,
|
|
limits: { fileSize: 100 * 1024 * 1024 },
|
|
fileFilter: (req, file, cb) => {
|
|
const allowed = ['.mp4', '.webm', '.mov', '.avi'];
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
if (allowed.includes(ext)) cb(null, true);
|
|
else cb(new Error('Только видео: mp4, webm, mov, avi'));
|
|
}
|
|
});
|
|
|
|
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.Counter({
|
|
name: 'login_attempts_total',
|
|
help: 'Total 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: '1mb' }));
|
|
app.use(helmet({
|
|
contentSecurityPolicy: false,
|
|
crossOriginEmbedderPolicy: false
|
|
}));
|
|
app.use(cookieParser());
|
|
const CORS_ORIGIN = process.env.CORS_ORIGIN || '*';
|
|
app.use((req, res, next) => {
|
|
res.header('Access-Control-Allow-Origin', CORS_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();
|
|
});
|
|
|
|
const geoip = require('geoip-lite');
|
|
const ipCountryCache = {};
|
|
|
|
function getCountryByIp(ip) {
|
|
if (!ip || ip === '127.0.0.1' || ip === '::1' || ip.startsWith('::ffff:127.')) return null;
|
|
if (ipCountryCache[ip] !== undefined) return ipCountryCache[ip];
|
|
try {
|
|
const geo = geoip.lookup(ip);
|
|
const result = geo ? { country: geo.country, code: geo.country } : null;
|
|
ipCountryCache[ip] = result;
|
|
return result;
|
|
} catch (e) {
|
|
ipCountryCache[ip] = null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function parseCookies(header) {
|
|
const cookies = {};
|
|
if (!header) return cookies;
|
|
header.split(';').forEach(c => {
|
|
const idx = c.indexOf('=');
|
|
if (idx > 0) cookies[c.substring(0, idx).trim()] = c.substring(idx + 1).trim();
|
|
});
|
|
return cookies;
|
|
}
|
|
|
|
app.use((req, res, next) => {
|
|
if (req.method !== 'GET' || req.path.startsWith('/api/') ||
|
|
req.path.startsWith('/css/') || req.path.startsWith('/js/') ||
|
|
req.path.startsWith('/img/') || req.path.startsWith('/data/') ||
|
|
req.path.startsWith('/uploads/') || req.path.startsWith('/webfonts/') ||
|
|
req.path === '/metrics' || req.path === '/favicon.ico') {
|
|
return next();
|
|
}
|
|
|
|
const ip = req.ip || req.connection.remoteAddress || '';
|
|
|
|
if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') {
|
|
return next();
|
|
}
|
|
|
|
const cookies = parseCookies(req.headers.cookie);
|
|
let sessionId = cookies.visitor_sid;
|
|
|
|
if (!sessionId) {
|
|
sessionId = require('crypto').randomUUID();
|
|
res.setHeader('Set-Cookie', `visitor_sid=${sessionId}; Path=/; Max-Age=86400; SameSite=Lax`);
|
|
}
|
|
|
|
const geo = getCountryByIp(ip);
|
|
|
|
db.run(
|
|
`INSERT INTO visitor_log (ip, session_id, user_agent, accept_language, country, country_code, page_path, visit_date)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, date('now'))`,
|
|
[ip, sessionId, (req.headers['user-agent'] || '').substring(0, 500),
|
|
(req.headers['accept-language'] || '').substring(0, 200),
|
|
geo ? geo.country : null, geo ? geo.code : null, req.path],
|
|
(err) => { if (err) console.error('Visitor log error:', err.message); }
|
|
);
|
|
|
|
next();
|
|
});
|
|
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
|
app.use('/data/room_images', express.static(roomsUploadsDir));
|
|
app.use('/data/activity_images', express.static(activityUploadsDir));
|
|
app.use('/data/hero_images', express.static(heroImagesDir));
|
|
app.use('/data/hero_videos', express.static(heroVideosDir));
|
|
|
|
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 = 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, (err) => {
|
|
if (err) { console.error('FATAL: Cannot open database:', err.message); process.exit(1); }
|
|
});
|
|
|
|
initDatabase(db);
|
|
|
|
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 seasonalPricesModule = require('./modules/seasonalPrices');
|
|
const emailModule = require('./modules/email');
|
|
const telegramModule = require('./modules/telegram');
|
|
const syncModule = require('./modules/sync');
|
|
const reportsModule = require('./modules/reports');
|
|
const activitiesModule = require('./modules/activities');
|
|
const heroModule = require('./modules/hero');
|
|
const visitorsModule = require('./modules/visitors');
|
|
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;
|
|
modules.seasonalPrices = seasonalPricesModule;
|
|
modules.email = emailModule;
|
|
modules.telegram = telegramModule;
|
|
modules.sync = syncModule;
|
|
modules.reports = reportsModule;
|
|
modules.activities = activitiesModule;
|
|
modules.hero = heroModule;
|
|
modules.visitors = visitorsModule;
|
|
|
|
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);
|
|
seasonalPricesModule.init(db);
|
|
emailModule.init(db, settingsModule);
|
|
telegramModule.init(db, settingsModule);
|
|
syncModule.init(db, settingsModule, emailModule, telegramModule);
|
|
reportsModule.init(db);
|
|
activitiesModule.init(db);
|
|
heroModule.init(db);
|
|
visitorsModule.init(db);
|
|
|
|
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;
|
|
|
|
db.serialize(() => {
|
|
db.run("BEGIN TRANSACTION");
|
|
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();
|
|
db.run("COMMIT", (err) => {
|
|
if (err) { console.error('Default rooms commit error:', err); db.run("ROLLBACK"); }
|
|
else 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;
|
|
}
|
|
bcrypt.hash(ADMIN_PASSWORD, 10).then(hash => {
|
|
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`);
|
|
});
|
|
}
|
|
});
|
|
}).catch(err => console.error('Admin hash error:', err));
|
|
}
|
|
|
|
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);
|
|
seasonalPricesModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
|
|
emailModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
|
|
telegramModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
|
|
reportsModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
|
|
activitiesModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin, uploadActivity);
|
|
heroModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin, uploadHeroImage, uploadHeroVideo);
|
|
visitorsModule.setupRoutes(app, authModule.authenticateToken);
|
|
|
|
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/stats', (req, res) => {
|
|
db.get(`SELECT COUNT(DISTINCT type) as count FROM rooms WHERE is_active = 1`, [], (err, row) => {
|
|
if (err) {
|
|
console.error('Stats categories error:', err);
|
|
return res.status(500).json({ error: 'Database error' });
|
|
}
|
|
const categories = (row && row.count > 0) ? row.count : Object.keys(config.ROOM_TYPES).length;
|
|
|
|
db.get(`SELECT COUNT(*) as total, SUM(CASE WHEN stars >= 4 THEN 1 ELSE 0 END) as satisfied FROM reviews WHERE is_approved = 1`, [], (err2, row2) => {
|
|
if (err2) {
|
|
console.error('Stats reviews error:', err2);
|
|
return res.status(500).json({ error: 'Database error' });
|
|
}
|
|
let satisfaction = 100;
|
|
if (row2 && row2.total > 0) {
|
|
satisfaction = Math.round((row2.satisfied / row2.total) * 100);
|
|
}
|
|
res.json({ categories, satisfaction });
|
|
});
|
|
});
|
|
});
|
|
|
|
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(/(\{|\,)\s*(\w+):/g, '$1"$2":');
|
|
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' });
|
|
}
|
|
});
|
|
});
|
|
|
|
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 = [];
|
|
try {
|
|
cities = new Function(data.replace(/^const\s+\w+\s*=\s*/, 'return '))();
|
|
} catch (e) {
|
|
console.error('Failed to parse cities:', e);
|
|
}
|
|
|
|
res.json({ cities, popular: [], countryCode });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
|
|
|
|
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'));
|
|
});
|
|
|
|
app.get('/privacy', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'privacy.html'));
|
|
});
|
|
|
|
app.get('/terms', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'terms.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}`);
|
|
setTimeout(() => { telegramModule.startPolling(); }, 2000);
|
|
setTimeout(() => { syncModule.start(); }, 3000);
|
|
});
|
|
|
|
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'));
|
|
});
|