Files
hotell777_260507/server.js
2026-05-08 23:49:27 +05:00

650 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
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 ROOM_PRICES = {
'Эконом': 2500,
'Стандарт': 4000,
'VIP Люкс': 8000
};
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 calculateNights(checkin, checkout) {
const ci = new Date(checkin);
const co = new Date(checkout);
return Math.ceil((co - ci) / (1000 * 60 * 60 * 24));
}
function calculateBasePrice(roomType, checkin, checkout) {
const pricePerNight = ROOM_PRICES[roomType] || 0;
const nights = calculateNights(checkin, checkout);
return pricePerNight * nights;
}
function validatePromocode(promocode, callback) {
if (!promocode) return callback(null, null);
const now = new Date().toISOString();
db.get(`SELECT * FROM promocodes WHERE code = ? AND is_active = 1`, [promocode], (err, row) => {
if (err || !row) return callback(null, null);
if (row.valid_from && row.valid_from > now) return callback(null, null);
if (row.valid_to && row.valid_to < now) return callback(null, null);
if (row.valid_days) {
const createdDate = new Date(row.created_at);
const expireDate = new Date(createdDate.getTime() + row.valid_days * 24 * 60 * 60 * 1000);
if (expireDate < new Date()) return callback(null, null);
}
callback(null, row);
});
}
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')));
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.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
)`, (err) => {
if (err) console.error('Table creation error:', err);
});
db.run(`ALTER TABLE bookings ADD COLUMN wishes TEXT`, (err) => {
if (err && !err.message.includes('duplicate column name')) {
console.error('Migration error:', err);
}
});
db.run(`ALTER TABLE bookings ADD COLUMN status TEXT DEFAULT 'новая'`, (err) => {
if (err && !err.message.includes('duplicate column name')) {
console.error('Migration error:', err);
}
});
db.run(`ALTER TABLE bookings ADD COLUMN room_type TEXT`, (err) => {
if (err && !err.message.includes('duplicate column name')) {
console.error('Migration error:', err);
}
});
db.run(`ALTER TABLE bookings ADD COLUMN comment TEXT`, (err) => {
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
});
db.run(`ALTER TABLE bookings ADD COLUMN base_price REAL`, (err) => {
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
});
db.run(`ALTER TABLE bookings ADD COLUMN discount_percent INTEGER DEFAULT 0`, (err) => {
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
});
db.run(`ALTER TABLE bookings ADD COLUMN discount_amount REAL DEFAULT 0`, (err) => {
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
});
db.run(`ALTER TABLE bookings ADD COLUMN total_price REAL`, (err) => {
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
});
db.run(`ALTER TABLE bookings ADD COLUMN promocode_id INTEGER`, (err) => {
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', 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
)`, (err) => {
if (err) console.error('Promocodes table creation error:', err);
});
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
)`, (err) => {
if (err) console.error('Rooms table creation error:', err);
else initDefaultRooms();
});
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)
)`, (err) => {
if (err) console.error('Booking history table creation error:', err);
});
function logHistory(bookingId, userId, userLogin, field, oldValue, newValue) {
db.run(`INSERT INTO booking_history (booking_id, user_id, user_login, field, old_value, new_value) VALUES (?, ?, ?, ?, ?, ?)`,
[bookingId, userId, userLogin, field, oldValue, newValue], (err) => {
if (err) console.error('History log error:', err);
});
}
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
)`, (err) => {
if (err) console.error('Users table creation error:', err);
else syncAdmin();
});
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`);
});
}
});
}
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
req.user = user;
next();
});
}
function requireAdmin(req, res, next) {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin access required' });
next();
}
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);
}
}
}
}
}
app.post('/api/bookings', (req, res) => {
const { name, phone, adults, children, checkin, checkout, wishes, room, promocode } = req.body;
if (!name || !phone || !adults || !checkin || !checkout) {
return res.status(400).json({ error: 'Missing required fields' });
}
const basePrice = calculateBasePrice(room, checkin, checkout);
validatePromocode(promocode, (err, promo) => {
if (err) return res.status(500).json({ error: 'Database error' });
let discountPercent = 0;
let promocodeId = null;
if (promo) {
discountPercent = promo.discount_percent;
promocodeId = promo.id;
}
const safeBasePrice = basePrice || 0;
const discountAmount = Math.round(safeBasePrice * discountPercent / 100);
const totalPrice = safeBasePrice - discountAmount;
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, base_price, discount_percent, discount_amount, total_price, promocode_id)
VALUES (?, ?, ?, ?, ?, ?, ?, 'новая', ?, ?, ?, ?, ?, ?)`);
stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, room || null,
safeBasePrice || null, discountPercent || 0, discountAmount || 0, totalPrice || null, promocodeId, function(err) {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json({ id: this.lastID, message: 'Booking saved', base_price: safeBasePrice, discount_percent: discountPercent, discount_amount: discountAmount, total_price: totalPrice });
});
stmt.finalize();
});
});
app.get('/api/admin/bookings', authenticateToken, (req, res) => {
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
ORDER BY b.checkin_date ASC`, [], (err, rows) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json(rows);
});
});
app.patch('/api/admin/bookings/:id', authenticateToken, requireAdmin, (req, res) => {
console.log('PATCH /api/admin/bookings/:id hit', req.params.id, req.body);
const bookingId = parseInt(req.params.id);
const { status } = req.body;
const validStatuses = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена'];
if (!status || !validStatuses.includes(status)) {
return res.status(400).json({ error: 'Invalid status. Valid: ' + validStatuses.join(', ') });
}
db.get(`SELECT status FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValue = row.status;
db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'status', oldValue, status);
db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Status updated', booking: row });
});
});
});
});
app.patch('/api/admin/bookings/:id/room', authenticateToken, requireAdmin, (req, res) => {
const bookingId = parseInt(req.params.id);
const { room_type } = req.body;
const validRooms = ['Эконом', 'Стандарт', 'VIP Люкс'];
if (!room_type || !validRooms.includes(room_type)) {
return res.status(400).json({ error: 'Invalid room type. Valid: ' + validRooms.join(', ') });
}
db.get(`SELECT room_type, checkin_date, checkout_date, discount_percent FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValue = row.room_type || 'Не указан';
const basePrice = calculateBasePrice(room_type, row.checkin_date, row.checkout_date);
const discountAmount = Math.round(basePrice * (row.discount_percent || 0) / 100);
const totalPrice = basePrice - discountAmount;
db.run(`UPDATE bookings SET room_type = ?, base_price = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
[room_type, basePrice, discountAmount, totalPrice, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'room_type', oldValue, room_type);
db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Room updated', booking: row });
});
});
});
});
app.patch('/api/admin/bookings/:id/comment', authenticateToken, requireAdmin, (req, res) => {
const bookingId = parseInt(req.params.id);
const { comment } = req.body;
db.get(`SELECT comment FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValue = row.comment || 'Нет';
db.run(`UPDATE bookings SET comment = ? WHERE id = ?`, [comment || null, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'comment', oldValue, comment || 'Нет');
db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Comment updated', booking: row });
});
});
});
});
app.patch('/api/admin/bookings/:id/discount', authenticateToken, requireAdmin, (req, res) => {
const bookingId = parseInt(req.params.id);
const { discount_percent } = req.body;
if (discount_percent === undefined || discount_percent < 0 || discount_percent > 99) {
return res.status(400).json({ error: 'Discount percent must be between 0 and 99' });
}
db.get(`SELECT base_price, discount_percent FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValue = row.discount_percent || 0;
const basePrice = row.base_price || 0;
const discountAmount = Math.round(basePrice * discount_percent / 100);
const totalPrice = basePrice - discountAmount;
db.run(`UPDATE bookings SET discount_percent = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
[discount_percent, discountAmount, totalPrice, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'discount_percent', oldValue.toString(), discount_percent.toString());
db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Discount updated', booking: row });
});
});
});
});
app.get('/api/admin/bookings/:id/history', authenticateToken, (req, res) => {
const bookingId = parseInt(req.params.id);
db.all(`SELECT id, booking_id, user_id, user_login, field, old_value, new_value, created_at
FROM booking_history WHERE booking_id = ? ORDER BY created_at DESC`, [bookingId], (err, rows) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json(rows);
});
});
app.get('/api/bookings', (req, res) => {
const providedKey = req.headers['x-api-key'];
if (!providedKey || providedKey !== API_KEY) {
return res.status(401).json({ error: 'Invalid or missing API key' });
}
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
ORDER BY b.checkin_date ASC`, (err, rows) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json(rows);
});
});
app.post('/api/promocodes/validate', (req, res) => {
const { code, room_type, checkin, checkout } = req.body;
if (!code) return res.status(400).json({ error: 'Promocode required' });
validatePromocode(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 basePrice = calculateBasePrice(room_type, checkin, checkout);
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
});
});
});
// Public rooms endpoint
app.get('/api/rooms', (req, res) => {
db.all(`SELECT * FROM rooms WHERE is_active = 1 ORDER BY price_per_guest ASC`, [], (err, rows) => {
if (err) { console.error('Rooms API error:', err); return res.status(500).json({ error: 'Database error' }); }
res.json(rows);
});
});
app.get('/api/admin/promocodes', authenticateToken, requireAdmin, (req, res) => {
db.all(`SELECT * FROM promocodes ORDER BY created_at DESC`, [], (err, rows) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json(rows);
});
});
app.post('/api/admin/promocodes', authenticateToken, requireAdmin, (req, res) => {
const { code, discount_percent, valid_from, valid_to, valid_days, is_active } = req.body;
if (!code || !discount_percent) return res.status(400).json({ error: 'Code and discount percent required' });
if (discount_percent < 1 || discount_percent > 99) return res.status(400).json({ error: 'Discount must be between 1 and 99' });
db.run(`INSERT INTO promocodes (code, discount_percent, valid_from, valid_to, valid_days, is_active)
VALUES (?, ?, ?, ?, ?, ?)`,
[code, discount_percent, valid_from || null, valid_to || null, valid_days || null, is_active !== undefined ? is_active : 1], function(err) {
if (err) {
if (err.message.includes('UNIQUE constraint')) return res.status(409).json({ error: 'Promocode already exists' });
return res.status(500).json({ error: 'Database error' });
}
db.get(`SELECT * FROM promocodes WHERE id = ?`, [this.lastID], (err, row) => {
res.status(201).json({ message: 'Promocode created', promocode: row });
});
});
});
app.put('/api/admin/promocodes/:id', authenticateToken, requireAdmin, (req, res) => {
const promoId = parseInt(req.params.id);
const { code, discount_percent, valid_from, valid_to, valid_days, is_active } = req.body;
if (discount_percent !== undefined && (discount_percent < 1 || discount_percent > 99)) {
return res.status(400).json({ error: 'Discount must be between 1 and 99' });
}
db.get(`SELECT id FROM promocodes WHERE id = ?`, [promoId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Promocode not found' });
let fields = [];
let values = [];
if (code !== undefined) { fields.push('code = ?'); values.push(code); }
if (discount_percent !== undefined) { fields.push('discount_percent = ?'); values.push(discount_percent); }
if (valid_from !== undefined) { fields.push('valid_from = ?'); values.push(valid_from || null); }
if (valid_to !== undefined) { fields.push('valid_to = ?'); values.push(valid_to || null); }
if (valid_days !== undefined) { fields.push('valid_days = ?'); values.push(valid_days || null); }
if (is_active !== undefined) { fields.push('is_active = ?'); values.push(is_active); }
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
values.push(promoId);
db.run(`UPDATE promocodes SET ${fields.join(', ')} WHERE id = ?`, values, (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
db.get(`SELECT * FROM promocodes WHERE id = ?`, [promoId], (err, row) => {
res.json({ message: 'Promocode updated', promocode: row });
});
});
});
});
app.delete('/api/admin/promocodes/:id', authenticateToken, requireAdmin, (req, res) => {
const promoId = parseInt(req.params.id);
db.get(`SELECT id FROM promocodes WHERE id = ?`, [promoId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Promocode not found' });
db.run(`DELETE FROM promocodes WHERE id = ?`, [promoId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Promocode deleted' });
});
});
});
app.post('/api/auth/login', (req, res) => {
const { login, password } = req.body;
if (!login || !password) return res.status(400).json({ error: 'Login and password required' });
db.get(`SELECT id, login, password_hash, full_name, email, role FROM users WHERE login = ?`, [login], (err, user) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const match = bcrypt.compareSync(password, user.password_hash);
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign({ id: user.id, login: user.login, role: user.role }, JWT_SECRET, { expiresIn: '24h' });
res.json({
token,
user: { id: user.id, login: user.login, full_name: user.full_name, email: user.email, role: user.role }
});
});
});
app.put('/api/auth/me', authenticateToken, (req, res) => {
const { full_name, email } = req.body;
db.run(`UPDATE users SET full_name = COALESCE(?, full_name), email = COALESCE(?, email) WHERE id = ?`,
[full_name || null, email || null, req.user.id], function(err) {
if (err) return res.status(500).json({ error: 'Database error' });
db.get(`SELECT id, login, full_name, email, role FROM users WHERE id = ?`, [req.user.id], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Profile updated', user: row });
});
});
});
app.post('/api/auth/change-password', authenticateToken, (req, res) => {
const { current_password, new_password } = req.body;
if (!current_password || !new_password) return res.status(400).json({ error: 'Current and new password required' });
if (new_password.length < 4) return res.status(400).json({ error: 'Password must be at least 4 characters' });
db.get(`SELECT password_hash FROM users WHERE id = ?`, [req.user.id], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'User not found' });
const match = bcrypt.compareSync(current_password, row.password_hash);
if (!match) return res.status(401).json({ error: 'Current password is incorrect' });
const hash = bcrypt.hashSync(new_password, 10);
db.run(`UPDATE users SET password_hash = ? WHERE id = ?`, [hash, req.user.id], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Password changed successfully' });
});
});
});
app.get('/api/admin/users', authenticateToken, requireAdmin, (req, res) => {
db.all(`SELECT id, login, full_name, email, role, created_at FROM users ORDER BY created_at DESC`, [], (err, rows) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json(rows);
});
});
app.post('/api/admin/users', authenticateToken, requireAdmin, (req, res) => {
const { login, password, full_name, email, role } = req.body;
if (!login || !password) return res.status(400).json({ error: 'Login and password required' });
if (!['admin', 'user'].includes(role)) return res.status(400).json({ error: 'Role must be "admin" or "user"' });
if (password.length < 4) return res.status(400).json({ error: 'Password must be at least 4 characters' });
const hash = bcrypt.hashSync(password, 10);
db.run(`INSERT INTO users (login, password_hash, full_name, email, role) VALUES (?, ?, ?, ?, ?)`,
[login, hash, full_name || null, email || null, role], function(err) {
if (err) {
if (err.message.includes('UNIQUE constraint')) return res.status(409).json({ error: 'Login already exists' });
return res.status(500).json({ error: 'Database error' });
}
db.get(`SELECT id, login, full_name, email, role, created_at FROM users WHERE id = ?`, [this.lastID], (err, row) => {
res.status(201).json({ message: 'User created', user: row });
});
});
});
app.put('/api/admin/users/:id', authenticateToken, requireAdmin, (req, res) => {
const userId = parseInt(req.params.id);
const { full_name, email, role, password } = req.body;
if (role && !['admin', 'user'].includes(role)) return res.status(400).json({ error: 'Role must be "admin" or "user"' });
db.get(`SELECT id FROM users WHERE id = ?`, [userId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'User not found' });
let fields = [];
let values = [];
if (full_name !== undefined) { fields.push('full_name = ?'); values.push(full_name || null); }
if (email !== undefined) { fields.push('email = ?'); values.push(email || null); }
if (role !== undefined) { fields.push('role = ?'); values.push(role); }
if (password) {
if (password.length < 4) return res.status(400).json({ error: 'Password must be at least 4 characters' });
fields.push('password_hash = ?');
values.push(bcrypt.hashSync(password, 10));
}
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
values.push(userId);
db.run(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`, values, (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
db.get(`SELECT id, login, full_name, email, role, created_at FROM users WHERE id = ?`, [userId], (err, row) => {
res.json({ message: 'User updated', user: row });
});
});
});
});
app.delete('/api/admin/users/:id', authenticateToken, requireAdmin, (req, res) => {
const userId = parseInt(req.params.id);
if (userId === req.user.id) return res.status(400).json({ error: 'Cannot delete yourself' });
db.get(`SELECT login FROM users WHERE id = ?`, [userId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'User not found' });
if (row.login === ADMIN_LOGIN) return res.status(403).json({ error: 'Cannot delete superadmin defined in .env' });
db.run(`DELETE FROM users WHERE id = ?`, [userId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'User deleted' });
});
});
});
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'));
});
convertImages().then(() => {
app.listen(PORT, () => {
console.log('✅ HOTEL777KEY is', API_KEY);
console.log(`✅ Hotel 777 server running on http://localhost:${PORT}`);
});
});