326 lines
13 KiB
JavaScript
326 lines
13 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');
|
|
require('dotenv').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 || 'fallback-secret-change-in-production';
|
|
const MONITORING_USER = process.env.MONITORING_USER || 'monitoring';
|
|
const MONITORING_PASSWORD = process.env.MONITORING_PASSWORD || 'monitoring123';
|
|
|
|
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]
|
|
});
|
|
|
|
if (!API_KEY) {
|
|
console.error('FATAL: HOTEL777KEY environment variable not set');
|
|
process.exit(1);
|
|
}
|
|
|
|
app.use(express.json());
|
|
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((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
|
|
)`);
|
|
db.run(`ALTER TABLE bookings ADD COLUMN wishes TEXT`, (err) => {});
|
|
db.run(`ALTER TABLE bookings ADD COLUMN status TEXT DEFAULT 'новая'`, (err) => {});
|
|
db.run(`ALTER TABLE bookings ADD COLUMN room_type TEXT`, (err) => {});
|
|
db.run(`ALTER TABLE bookings ADD COLUMN comment TEXT`, (err) => {});
|
|
db.run(`ALTER TABLE bookings ADD COLUMN base_price REAL`, (err) => {});
|
|
db.run(`ALTER TABLE bookings ADD COLUMN discount_percent INTEGER DEFAULT 0`, (err) => {});
|
|
db.run(`ALTER TABLE bookings ADD COLUMN discount_amount REAL DEFAULT 0`, (err) => {});
|
|
db.run(`ALTER TABLE bookings ADD COLUMN total_price REAL`, (err) => {});
|
|
db.run(`ALTER TABLE bookings ADD COLUMN promocode_id INTEGER`, (err) => {});
|
|
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
|
|
)`);
|
|
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,
|
|
single_beds INTEGER DEFAULT 0,
|
|
double_beds INTEGER DEFAULT 0,
|
|
has_sofa INTEGER DEFAULT 0,
|
|
has_ac INTEGER DEFAULT 0,
|
|
has_wifi INTEGER DEFAULT 0,
|
|
has_shower INTEGER DEFAULT 0,
|
|
max_guests INTEGER DEFAULT 2,
|
|
price_per_guest INTEGER NOT NULL,
|
|
image_path TEXT,
|
|
is_active INTEGER DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`);
|
|
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)
|
|
)`);
|
|
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
|
|
)`);
|
|
});
|
|
|
|
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 { runStartupTests } = require('./tests/runStartupTests');
|
|
|
|
modules.auth = authModule;
|
|
modules.bookings = bookingsModule;
|
|
modules.promocodes = promocodesModule;
|
|
modules.rooms = roomsModule;
|
|
modules.users = usersModule;
|
|
modules.adminBookings = adminBookingsModule;
|
|
|
|
authModule.init(db, JWT_SECRET);
|
|
bookingsModule.init(db);
|
|
adminBookingsModule.init(db);
|
|
promocodesModule.init(db);
|
|
roomsModule.init(db);
|
|
usersModule.init(db, bcrypt);
|
|
|
|
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 defaults = [
|
|
{ type: 'Эконом', name: 'Эконом 1', description: 'Бюджетный номер', rooms_count: 3, single_beds: 2, double_beds: 0, has_sofa: 0, has_ac: 0, has_wifi: 1, has_shower: 1, max_guests: 2, price_per_guest: 2500, image_path: null, is_active: 1 },
|
|
{ type: 'Стандарт', name: 'Стандарт 1', description: 'Комфортный номер', rooms_count: 2, single_beds: 0, double_beds: 1, has_sofa: 1, has_ac: 1, has_wifi: 1, has_shower: 1, max_guests: 3, price_per_guest: 4000, image_path: null, is_active: 1 },
|
|
{ type: 'VIP Люкс', name: 'VIP Люкс 1', description: 'Премиум номер', rooms_count: 1, single_beds: 0, double_beds: 1, has_sofa: 1, has_ac: 1, has_wifi: 1, has_shower: 1, max_guests: 4, price_per_guest: 8000, image_path: null, is_active: 1 }
|
|
];
|
|
const stmt = db.prepare(`INSERT INTO rooms (type, name, description, rooms_count, single_beds, double_beds, has_sofa, has_ac, has_wifi, has_shower, max_guests, price_per_guest, image_path, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
defaults.forEach(r => stmt.run(r.type, r.name, r.description, r.rooms_count, r.single_beds, r.double_beds, r.has_sofa, r.has_ac, r.has_wifi, r.has_shower, r.max_guests, r.price_per_guest, r.image_path, r.is_active));
|
|
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);
|
|
usersModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin);
|
|
|
|
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(() => {
|
|
app.listen(PORT, async () => {
|
|
console.log('✅ HOTEL777KEY is', API_KEY);
|
|
console.log('📊 Prometheus metrics available at: http://localhost:' + PORT + '/metrics');
|
|
console.log('🔐 Monitoring credentials: ' + MONITORING_USER + ' / ' + (MONITORING_PASSWORD ? '***' : 'NOT SET'));
|
|
console.log('');
|
|
await runStartupTests(db, modules);
|
|
console.log(`✅ Hotel 777 server running on http://localhost:${PORT}`);
|
|
});
|
|
}); |