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 ?)'; 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 = 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 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 = config.ROOM_TYPE_LIST; 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 = config.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 : 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 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 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 } = 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 = config.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 };