This commit is contained in:
2026-05-10 16:20:29 +05:00
parent 67b7008bf3
commit 001587799c
9 changed files with 1036 additions and 680 deletions

View File

@@ -0,0 +1,317 @@
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
let db;
let JWT_SECRET;
const ROOM_PRICES = { 'Эконом': 2500, 'Стандарт': 4000, 'VIP Люкс': 8000 };
function init(database, jwtSecret) {
db = database;
JWT_SECRET = jwtSecret;
}
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);
});
}
function getBookingsForAdmin(req, res) {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const search = req.query.search || '';
const statusFilter = req.query.status || '';
let whereClause = '1=1';
const params = [];
if (search) {
whereClause += ' AND (b.name LIKE ? OR b.phone LIKE ? OR b.room_type LIKE ?)';
const searchTerm = `%${search}%`;
params.push(searchTerm, searchTerm, searchTerm);
}
if (statusFilter && statusFilter !== 'all') {
whereClause += ' AND b.status = ?';
params.push(statusFilter);
}
db.get(`SELECT COUNT(*) as total FROM bookings b WHERE ${whereClause}`, params, (err, countRow) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
const total = countRow.total;
const totalPages = Math.ceil(total / limit);
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
WHERE ${whereClause}
ORDER BY b.checkin_date ASC
LIMIT ? OFFSET ?`, [...params, limit, offset], (err, rows) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json({
data: rows,
pagination: {
page,
limit,
total,
totalPages
}
});
});
});
}
function updateBookingStatus(req, res) {
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 });
});
});
});
}
function updateBookingRoom(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 });
});
});
});
}
function updateBookingComment(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 });
});
});
});
}
function updateBookingDiscount(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 });
});
});
});
}
function updateBookingDetails(req, res) {
const bookingId = parseInt(req.params.id);
const { adults, children, checkin_date, checkout_date } = req.body;
if (adults === undefined && children === undefined && checkin_date === undefined && checkout_date === undefined) {
return res.status(400).json({ error: 'No fields to update' });
}
const newAdults = adults !== undefined ? parseInt(adults) : undefined;
const newChildren = children !== undefined ? parseInt(children) : undefined;
const newCheckin = checkin_date || undefined;
const newCheckout = checkout_date || undefined;
if (newAdults !== undefined && (isNaN(newAdults) || newAdults < 1)) {
return res.status(400).json({ error: 'Количество взрослых должно быть минимум 1' });
}
if (newChildren !== undefined && (isNaN(newChildren) || newChildren < 0)) {
return res.status(400).json({ error: 'Количество детей не может быть отрицательным' });
}
const checkinDate = newCheckin ? new Date(newCheckin) : new Date();
const checkoutDate = newCheckout ? new Date(newCheckout) : new Date();
if (newCheckin && isNaN(checkinDate.getTime())) {
return res.status(400).json({ error: 'Некорректная дата заезда' });
}
if (newCheckout && isNaN(checkoutDate.getTime())) {
return res.status(400).json({ error: 'Некорректная дата выезда' });
}
if (newCheckin && newCheckout && checkoutDate <= checkinDate) {
return res.status(400).json({ error: 'Дата выезда должна быть позже даты заезда' });
}
db.get(`SELECT * 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 oldValues = {
adults: row.adults,
children: row.children,
checkin_date: row.checkin_date,
checkout_date: row.checkout_date
};
let fields = [];
let values = [];
if (newAdults !== undefined) { fields.push('adults = ?'); values.push(newAdults); }
if (newChildren !== undefined) { fields.push('children = ?'); values.push(newChildren); }
if (newCheckin !== undefined) { fields.push('checkin_date = ?'); values.push(newCheckin); }
if (newCheckout !== undefined) { fields.push('checkout_date = ?'); values.push(newCheckout); }
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
values.push(bookingId);
db.run(`UPDATE bookings SET ${fields.join(', ')} WHERE id = ?`, values, function(err) {
if (err) return res.status(500).json({ error: 'Database error' });
if (adults !== undefined && adults !== oldValues.adults) {
logHistory(bookingId, req.user.id, req.user.login, 'adults', oldValues.adults.toString(), adults.toString());
}
if (children !== undefined && children !== oldValues.children) {
logHistory(bookingId, req.user.id, req.user.login, 'children', oldValues.children.toString(), children.toString());
}
if (checkin_date !== undefined && checkin_date !== oldValues.checkin_date) {
logHistory(bookingId, req.user.id, req.user.login, 'checkin_date', oldValues.checkin_date, checkin_date);
}
if (checkout_date !== undefined && checkout_date !== oldValues.checkout_date) {
logHistory(bookingId, req.user.id, req.user.login, 'checkout_date', oldValues.checkout_date, checkout_date);
}
const finalAdults = adults !== undefined ? adults : oldValues.adults;
const finalChildren = children !== undefined ? children : oldValues.children;
const finalCheckin = checkin_date !== undefined ? checkin_date : oldValues.checkin_date;
const finalCheckout = checkout_date !== undefined ? checkout_date : oldValues.checkout_date;
const totalGuests = finalAdults + finalChildren;
const roomType = row.room_type;
if (!roomType) {
return finishUpdate();
}
db.get(`SELECT price_per_guest FROM rooms WHERE type = ? AND is_active = 1`, [roomType], (err, roomData) => {
if (err) return res.status(500).json({ error: 'Database error' });
const pricePerGuest = roomData ? roomData.price_per_guest : (ROOM_PRICES[roomType] || 0);
const nights = calculateNights(finalCheckin, finalCheckout);
const basePrice = pricePerGuest * totalGuests * nights;
const discountPercent = row.discount_percent || 0;
const discountAmount = Math.round(basePrice * discountPercent / 100);
const totalPrice = basePrice - discountAmount;
db.run(`UPDATE bookings SET base_price = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
[basePrice, discountAmount, totalPrice, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
finishUpdate();
});
});
function finishUpdate() {
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: 'Booking details updated', booking: row });
});
}
});
});
}
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);
});
}
function validatePromocodeAPI(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
});
});
}
function setupRoutes(app, authenticateToken, requireAdmin) {
app.get('/api/admin/bookings', authenticateToken, getBookingsForAdmin);
app.patch('/api/admin/bookings/:id', authenticateToken, requireAdmin, updateBookingStatus);
app.patch('/api/admin/bookings/:id/room', authenticateToken, requireAdmin, updateBookingRoom);
app.patch('/api/admin/bookings/:id/comment', authenticateToken, requireAdmin, updateBookingComment);
app.patch('/api/admin/bookings/:id/discount', authenticateToken, requireAdmin, updateBookingDiscount);
app.patch('/api/admin/bookings/:id/details', authenticateToken, requireAdmin, updateBookingDetails);
app.post('/api/promocodes/validate', validatePromocodeAPI);
}
module.exports = { init, setupRoutes, calculateNights, calculateBasePrice, validatePromocode };

79
modules/auth/index.js Normal file
View File

@@ -0,0 +1,79 @@
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
let db;
let JWT_SECRET;
function init(database, jwtSecret) {
db = database;
JWT_SECRET = jwtSecret;
}
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();
}
function 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 }
});
});
}
function updateProfile(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 });
});
});
}
function changePassword(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' });
});
});
}
function setupRoutes(app) {
app.post('/api/auth/login', login);
app.put('/api/auth/me', authenticateToken, updateProfile);
app.post('/api/auth/change-password', authenticateToken, changePassword);
}
module.exports = { init, setupRoutes, authenticateToken, requireAdmin, login };

105
modules/bookings/index.js Normal file
View File

@@ -0,0 +1,105 @@
let db;
function init(database) {
db = database;
}
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 ROOM_PRICES = { 'Эконом': 2500, 'Стандарт': 4000, 'VIP Люкс': 8000 };
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);
});
}
function createBooking(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();
});
}
function getBookings(req, res) {
const providedKey = req.headers['x-api-key'];
if (!providedKey) return res.status(401).json({ error: 'Invalid or missing API key' });
const API_KEY = process.env.HOTEL777KEY;
if (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);
});
}
function getBookingHistory(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);
});
}
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);
});
}
function setupRoutes(app, authenticateToken, requireAdmin) {
app.post('/api/bookings', createBooking);
app.get('/api/bookings', getBookings);
app.get('/api/admin/bookings/:id/history', authenticateToken, getBookingHistory);
}
module.exports = { init, setupRoutes, calculateNights, calculateBasePrice, validatePromocode, logHistory };

View File

@@ -0,0 +1,78 @@
let db;
function init(database) {
db = database;
}
function getAll(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);
});
}
function create(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 });
});
});
}
function update(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 });
});
});
});
}
function remove(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' });
});
});
}
function setupRoutes(app, authenticateToken, requireAdmin) {
app.get('/api/admin/promocodes', authenticateToken, requireAdmin, getAll);
app.post('/api/admin/promocodes', authenticateToken, requireAdmin, create);
app.put('/api/admin/promocodes/:id', authenticateToken, requireAdmin, update);
app.delete('/api/admin/promocodes/:id', authenticateToken, requireAdmin, remove);
}
module.exports = { init, setupRoutes };

18
modules/rooms/index.js Normal file
View File

@@ -0,0 +1,18 @@
let db;
function init(database) {
db = database;
}
function getAll(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);
});
}
function setupRoutes(app) {
app.get('/api/rooms', getAll);
}
module.exports = { init, setupRoutes };

84
modules/users/index.js Normal file
View File

@@ -0,0 +1,84 @@
let db;
let bcrypt;
function init(database, bcryptModule) {
db = database;
bcrypt = bcryptModule;
}
function getAllUsers(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);
});
}
function createUser(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 });
});
});
}
function updateUser(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 });
});
});
});
}
function deleteUser(req, res) {
const userId = parseInt(req.params.id);
const ADMIN_LOGIN = process.env.ADMIN_LOGIN;
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' });
});
});
}
function setupRoutes(app, authenticateToken, requireAdmin) {
app.get('/api/admin/users', authenticateToken, requireAdmin, getAllUsers);
app.post('/api/admin/users', authenticateToken, requireAdmin, createUser);
app.put('/api/admin/users/:id', authenticateToken, requireAdmin, updateUser);
app.delete('/api/admin/users/:id', authenticateToken, requireAdmin, deleteUser);
}
module.exports = { init, setupRoutes };

View File

@@ -4,6 +4,7 @@
"dotenv": "^17.4.2",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"prom-client": "^15.1.3",
"sharp": "^0.34.5",
"sqlite3": "^6.0.1"
},

902
server.js
View File

@@ -5,6 +5,7 @@ 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();
@@ -13,56 +14,54 @@ 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 ROOM_PRICES = {
'Эконом': 2500,
'Стандарт': 4000,
'VIP Люкс': 8000
};
const register = new client.Registry();
client.collectDefaultMetrics({ register });
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');
});
}
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'path', 'status'],
registers: [register]
});
function calculateNights(checkin, checkout) {
const ci = new Date(checkin);
const co = new Date(checkout);
return Math.ceil((co - ci) / (1000 * 60 * 60 * 24));
}
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]
});
function calculateBasePrice(roomType, checkin, checkout) {
const pricePerNight = ROOM_PRICES[roomType] || 0;
const nights = calculateNights(checkin, checkout);
return pricePerNight * nights;
}
const activeConnections = new client.Gauge({
name: 'active_connections',
help: 'Number of active connections',
registers: [register]
});
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);
});
}
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');
@@ -79,131 +78,173 @@ app.use((req, res, 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.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.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
)`);
});
db.run(`ALTER TABLE bookings ADD COLUMN wishes TEXT`, (err) => {
if (err && !err.message.includes('duplicate column name')) {
console.error('Migration error:', err);
}
});
const modules = {};
db.run(`ALTER TABLE bookings ADD COLUMN status TEXT DEFAULT 'новая'`, (err) => {
if (err && !err.message.includes('duplicate column name')) {
console.error('Migration error:', err);
}
});
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');
db.run(`ALTER TABLE bookings ADD COLUMN room_type TEXT`, (err) => {
if (err && !err.message.includes('duplicate column name')) {
console.error('Migration error:', err);
}
});
modules.auth = authModule;
modules.bookings = bookingsModule;
modules.promocodes = promocodesModule;
modules.rooms = roomsModule;
modules.users = usersModule;
modules.adminBookings = adminBookingsModule;
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);
});
authModule.init(db, JWT_SECRET);
bookingsModule.init(db);
adminBookingsModule.init(db);
promocodesModule.init(db);
roomsModule.init(db);
usersModule.init(db, bcrypt);
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);
});
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');
});
}
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');
@@ -227,21 +268,25 @@ function syncAdmin() {
});
}
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();
});
}
setTimeout(() => {
initDefaultRooms();
syncAdmin();
}, 500);
function requireAdmin(req, res, next) {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin access required' });
next();
}
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');
@@ -269,516 +314,13 @@ async function convertImages() {
}
}
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) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const search = req.query.search || '';
const statusFilter = req.query.status || '';
let whereClause = '1=1';
const params = [];
if (search) {
whereClause += ' AND (b.name LIKE ? OR b.phone LIKE ? OR b.room_type LIKE ?)';
const searchTerm = `%${search}%`;
params.push(searchTerm, searchTerm, searchTerm);
}
if (statusFilter && statusFilter !== 'all') {
whereClause += ' AND b.status = ?';
params.push(statusFilter);
}
db.get(`SELECT COUNT(*) as total FROM bookings b WHERE ${whereClause}`, params, (err, countRow) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
const total = countRow.total;
const totalPages = Math.ceil(total / limit);
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
WHERE ${whereClause}
ORDER BY b.checkin_date ASC
LIMIT ? OFFSET ?`, [...params, limit, offset], (err, rows) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json({
data: rows,
pagination: {
page,
limit,
total,
totalPages
}
});
});
});
});
app.patch('/api/admin/bookings/:id', authenticateToken, requireAdmin, (req, res) => {
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.patch('/api/admin/bookings/:id/details', authenticateToken, requireAdmin, (req, res) => {
const bookingId = parseInt(req.params.id);
const { adults, children, checkin_date, checkout_date } = req.body;
if (adults === undefined && children === undefined && checkin_date === undefined && checkout_date === undefined) {
return res.status(400).json({ error: 'No fields to update' });
}
const newAdults = adults !== undefined ? parseInt(adults) : undefined;
const newChildren = children !== undefined ? parseInt(children) : undefined;
const newCheckin = checkin_date || undefined;
const newCheckout = checkout_date || undefined;
if (newAdults !== undefined && (isNaN(newAdults) || newAdults < 1)) {
return res.status(400).json({ error: 'Количество взрослых должно быть минимум 1' });
}
if (newChildren !== undefined && (isNaN(newChildren) || newChildren < 0)) {
return res.status(400).json({ error: 'Количество детей не может быть отрицательным' });
}
const checkinDate = newCheckin ? new Date(newCheckin) : new Date();
const checkoutDate = newCheckout ? new Date(newCheckout) : new Date();
if (newCheckin && isNaN(checkinDate.getTime())) {
return res.status(400).json({ error: 'Некорректная дата заезда' });
}
if (newCheckout && isNaN(checkoutDate.getTime())) {
return res.status(400).json({ error: 'Некорректная дата выезда' });
}
if (newCheckin && newCheckout && checkoutDate <= checkinDate) {
return res.status(400).json({ error: 'Дата выезда должна быть позже даты заезда' });
}
db.get(`SELECT * 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 oldValues = {
adults: row.adults,
children: row.children,
checkin_date: row.checkin_date,
checkout_date: row.checkout_date
};
let fields = [];
let values = [];
if (newAdults !== undefined) { fields.push('adults = ?'); values.push(newAdults); }
if (newChildren !== undefined) { fields.push('children = ?'); values.push(newChildren); }
if (newCheckin !== undefined) { fields.push('checkin_date = ?'); values.push(newCheckin); }
if (newCheckout !== undefined) { fields.push('checkout_date = ?'); values.push(newCheckout); }
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
values.push(bookingId);
db.run(`UPDATE bookings SET ${fields.join(', ')} WHERE id = ?`, values, function(err) {
if (err) return res.status(500).json({ error: 'Database error' });
if (adults !== undefined && adults !== oldValues.adults) {
logHistory(bookingId, req.user.id, req.user.login, 'adults', oldValues.adults.toString(), adults.toString());
}
if (children !== undefined && children !== oldValues.children) {
logHistory(bookingId, req.user.id, req.user.login, 'children', oldValues.children.toString(), children.toString());
}
if (checkin_date !== undefined && checkin_date !== oldValues.checkin_date) {
logHistory(bookingId, req.user.id, req.user.login, 'checkin_date', oldValues.checkin_date, checkin_date);
}
if (checkout_date !== undefined && checkout_date !== oldValues.checkout_date) {
logHistory(bookingId, req.user.id, req.user.login, 'checkout_date', oldValues.checkout_date, checkout_date);
}
const newAdults = adults !== undefined ? adults : oldValues.adults;
const newChildren = children !== undefined ? children : oldValues.children;
const newCheckin = checkin_date !== undefined ? checkin_date : oldValues.checkin_date;
const newCheckout = checkout_date !== undefined ? checkout_date : oldValues.checkout_date;
const totalGuests = newAdults + newChildren;
const roomType = row.room_type;
if (!roomType) {
return finishUpdate();
}
db.get(`SELECT price_per_guest FROM rooms WHERE type = ? AND is_active = 1`, [roomType], (err, roomData) => {
if (err) return res.status(500).json({ error: 'Database error' });
const pricePerGuest = roomData ? roomData.price_per_guest : (ROOM_PRICES[roomType] || 0);
const nights = calculateNights(newCheckin, newCheckout);
const basePrice = pricePerGuest * totalGuests * nights;
const discountPercent = row.discount_percent || 0;
const discountAmount = Math.round(basePrice * discountPercent / 100);
const totalPrice = basePrice - discountAmount;
db.run(`UPDATE bookings SET base_price = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
[basePrice, discountAmount, totalPrice, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
finishUpdate();
});
});
function finishUpdate() {
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: 'Booking details 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, () => {
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}`);
});
});
});

132
tests/runStartupTests.js Normal file
View File

@@ -0,0 +1,132 @@
function runStartupTests(db, modules) {
return new Promise((resolve) => {
const results = [];
const tableHeader = '\n┌─────────────────────────────────────┬──────────┬──────────────────────────┐';
const tableRow = '├─────────────────────────────────────┼──────────┼──────────────────────────┤';
const tableFooter = '└─────────────────────────────────────┴──────────┴──────────────────────────┘';
console.log('\n========================================');
console.log(' ЗАПУСК ТЕСТОВ API ПРИ СТАРТЕ');
console.log('========================================');
console.log(tableHeader);
console.log('│ API Функция │ Статус │ Описание │');
console.log(tableRow);
function addResult(name, status, description) {
const statusText = status === 'OK' ? 'OK' : 'FAIL';
const paddedName = name.padEnd(37);
const paddedStatus = statusText.padEnd(8);
const paddedDesc = (description || '').padEnd(26);
console.log(`${paddedName}${paddedStatus}${paddedDesc}`);
results.push({ name, status, description });
}
try {
addResult('DB Connection', 'OK', 'SQLite connected');
db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name='bookings'`, [], (err, row) => {
if (err || !row) {
addResult('Table: bookings', 'FAIL', 'Not found');
} else {
addResult('Table: bookings', 'OK', 'Exists');
}
db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name='users'`, [], (err, row) => {
if (err || !row) {
addResult('Table: users', 'FAIL', 'Not found');
} else {
addResult('Table: users', 'OK', 'Exists');
}
db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name='promocodes'`, [], (err, row) => {
if (err || !row) {
addResult('Table: promocodes', 'FAIL', 'Not found');
} else {
addResult('Table: promocodes', 'OK', 'Exists');
}
db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name='rooms'`, [], (err, row) => {
if (err || !row) {
addResult('Table: rooms', 'FAIL', 'Not found');
} else {
addResult('Table: rooms', 'OK', 'Exists');
}
db.all(`SELECT COUNT(*) as cnt FROM users`, [], (err, rows) => {
if (err) {
addResult('Query: users count', 'FAIL', err.message.substring(0, 24));
} else {
addResult('Query: users count', 'OK', `Found ${rows[0].cnt} users`);
}
db.all(`SELECT COUNT(*) as cnt FROM bookings`, [], (err, rows) => {
if (err) {
addResult('Query: bookings count', 'FAIL', err.message.substring(0, 24));
} else {
addResult('Query: bookings count', 'OK', `Found ${rows[0].cnt} bookings`);
}
if (modules.auth) {
addResult('Module: auth', 'OK', 'Initialized');
} else {
addResult('Module: auth', 'FAIL', 'Not loaded');
}
if (modules.bookings) {
addResult('Module: bookings', 'OK', 'Initialized');
} else {
addResult('Module: bookings', 'FAIL', 'Not loaded');
}
if (modules.promocodes) {
addResult('Module: promocodes', 'OK', 'Initialized');
} else {
addResult('Module: promocodes', 'FAIL', 'Not loaded');
}
if (modules.rooms) {
addResult('Module: rooms', 'OK', 'Initialized');
} else {
addResult('Module: rooms', 'FAIL', 'Not loaded');
}
if (modules.users) {
addResult('Module: users', 'OK', 'Initialized');
} else {
addResult('Module: users', 'FAIL', 'Not loaded');
}
if (modules.adminBookings) {
addResult('Module: adminBookings', 'OK', 'Initialized');
} else {
addResult('Module: adminBookings', 'FAIL', 'Not loaded');
}
console.log(tableFooter);
const passed = results.filter(r => r.status === 'OK').length;
const total = results.length;
const allPassed = passed === total;
console.log(`\nРезультат: ${passed}/${total} тестов пройдено`);
console.log(allPassed ? '✅ Все системы готовы к работе!' : '❌ Имеются проблемы - проверьте логи');
console.log('========================================\n');
resolve({ results, passed, total, allPassed });
});
});
});
});
});
});
} catch (error) {
addResult('Critical Error', 'FAIL', error.message.substring(0, 24));
console.log(tableFooter);
console.log(`\n❌ Критическая ошибка: ${error.message}`);
resolve({ results, passed: 0, total: results.length, allPassed: false });
}
});
}
module.exports = { runStartupTests };