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}`); }); });