374 lines
18 KiB
JavaScript
374 lines
18 KiB
JavaScript
const bcrypt = require('bcryptjs');
|
||
const jwt = require('jsonwebtoken');
|
||
const config = require('../../config');
|
||
|
||
const promocodeRateLimit = new Map();
|
||
const PROMOCODE_WINDOW = 60 * 1000;
|
||
const MAX_PROMOCODE_REQUESTS = 20;
|
||
|
||
function checkPromocodeRateLimit(ip) {
|
||
const now = Date.now();
|
||
const record = promocodeRateLimit.get(ip);
|
||
if (!record) {
|
||
promocodeRateLimit.set(ip, { count: 1, firstRequest: now });
|
||
return true;
|
||
}
|
||
if (now - record.firstRequest > PROMOCODE_WINDOW) {
|
||
promocodeRateLimit.set(ip, { count: 1, firstRequest: now });
|
||
return true;
|
||
}
|
||
if (record.count >= MAX_PROMOCODE_REQUESTS) {
|
||
return false;
|
||
}
|
||
record.count++;
|
||
return true;
|
||
}
|
||
|
||
setInterval(() => {
|
||
const now = Date.now();
|
||
for (const [ip, record] of promocodeRateLimit.entries()) {
|
||
if (now - record.firstRequest > PROMOCODE_WINDOW) {
|
||
promocodeRateLimit.delete(ip);
|
||
}
|
||
}
|
||
}, PROMOCODE_WINDOW);
|
||
|
||
let db;
|
||
let JWT_SECRET;
|
||
|
||
function init(database, jwtSecret) {
|
||
db = database;
|
||
JWT_SECRET = jwtSecret;
|
||
}
|
||
|
||
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 ? OR r.name LIKE ?)';
|
||
const searchTerm = `%${search}%`;
|
||
params.push(searchTerm, searchTerm, searchTerm, searchTerm);
|
||
}
|
||
|
||
if (statusFilter && statusFilter !== 'all') {
|
||
whereClause += ' AND b.status = ?';
|
||
params.push(statusFilter);
|
||
}
|
||
|
||
db.get(`SELECT COUNT(*) as total FROM bookings b LEFT JOIN rooms r ON b.room_id = r.id 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, r.name as room_name, r.type as room_type
|
||
FROM bookings b
|
||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||
LEFT JOIN rooms r ON b.room_id = r.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 = config.STATUS_LIST;
|
||
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, r.name as room_name, r.type as room_type
|
||
FROM bookings b
|
||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||
LEFT JOIN rooms r ON b.room_id = r.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_id } = req.body;
|
||
|
||
db.get(`SELECT * FROM bookings WHERE id = ?`, [bookingId], (err, booking) => {
|
||
if (err) return res.status(500).json({ error: 'Database error' });
|
||
if (!booking) return res.status(404).json({ error: 'Booking not found' });
|
||
|
||
if (!room_id) {
|
||
return res.status(400).json({ error: 'room_id is required' });
|
||
}
|
||
|
||
db.get(`SELECT * FROM rooms WHERE id = ?`, [room_id], (err, room) => {
|
||
if (err) return res.status(500).json({ error: 'Database error' });
|
||
if (!room) return res.status(400).json({ error: 'Room not found' });
|
||
|
||
const oldValue = booking.room_name ? booking.room_type + ' — ' + booking.room_name : booking.room_type || 'Не указан';
|
||
const newValue = room.type + ' — ' + room.name;
|
||
const basePrice = config.calculateBasePrice(room.type, booking.checkin_date, booking.checkout_date);
|
||
const discountAmount = Math.round(basePrice * (booking.discount_percent || 0) / 100);
|
||
const totalPrice = basePrice - discountAmount;
|
||
|
||
db.run(`UPDATE bookings SET room_id = ?, room_type = ?, base_price = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
|
||
[room.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', oldValue, newValue);
|
||
db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type
|
||
FROM bookings b
|
||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||
LEFT JOIN rooms r ON b.room_id = r.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, r.name as room_name, r.type as room_type
|
||
FROM bookings b
|
||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||
LEFT JOIN rooms r ON b.room_id = r.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, r.name as room_name, r.type as room_type
|
||
FROM bookings b
|
||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||
LEFT JOIN rooms r ON b.room_id = r.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 : config.getRoomPrice(roomType);
|
||
const nights = config.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, r.name as room_name, r.type as room_type
|
||
FROM bookings b
|
||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||
LEFT JOIN rooms r ON b.room_id = r.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 clientIp = req.ip || req.connection.remoteAddress || 'unknown';
|
||
|
||
if (!checkPromocodeRateLimit(clientIp)) {
|
||
return res.status(429).json({ error: 'Too many requests. Please try again later.' });
|
||
}
|
||
|
||
const { code, room_type, checkin, checkout, guests } = 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 guestsCount = parseInt(guests) || 1;
|
||
const basePrice = config.calculateBasePrice(room_type, checkin, checkout) * guestsCount;
|
||
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 }; |