Initial commit: CMS backend for Old Vine Hotel
- Complete Express.js API server - MongoDB integration with Mongoose - Admin authentication and authorization - Room management (CRUD operations) - Booking management system - Guest management - Payment processing (Stripe integration) - Content management (pages, blog, gallery) - Media upload and management - Integration services (Booking.com, Expedia, Opera PMS, Trip.com) - Email notifications - Comprehensive logging and error handling
This commit is contained in:
379
routes/admin.js
Normal file
379
routes/admin.js
Normal file
@@ -0,0 +1,379 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Admin = require('../models/Admin');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
|
||||
// @route POST /api/admin/login
|
||||
// @desc Admin login
|
||||
// @access Public
|
||||
router.post('/login', [
|
||||
body('username').notEmpty().trim().withMessage('Username or email is required'),
|
||||
body('password').notEmpty().withMessage('Password is required')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Find admin by credentials
|
||||
const admin = await Admin.findByCredentials(username, password);
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: admin._id,
|
||||
email: admin.email,
|
||||
role: admin.role,
|
||||
isAdmin: true
|
||||
},
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
data: {
|
||||
token,
|
||||
admin: {
|
||||
id: admin._id,
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
firstName: admin.firstName,
|
||||
lastName: admin.lastName,
|
||||
fullName: admin.fullName,
|
||||
avatar: admin.avatar,
|
||||
role: admin.role,
|
||||
permissions: admin.permissions
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Admin login error:', error);
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: error.message || 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/admin/register
|
||||
// @desc Register new admin (super-admin only)
|
||||
// @access Private (Super Admin)
|
||||
router.post('/register', adminAuth, [
|
||||
body('username').notEmpty().trim().toLowerCase().withMessage('Username is required'),
|
||||
body('email').isEmail().normalizeEmail().withMessage('Valid email is required'),
|
||||
body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters'),
|
||||
body('firstName').notEmpty().trim().withMessage('First name is required'),
|
||||
body('lastName').notEmpty().trim().withMessage('Last name is required'),
|
||||
body('role').optional().isIn(['admin', 'editor', 'manager']).withMessage('Invalid role')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
// Check if requester is super admin
|
||||
const requester = await Admin.findById(req.admin.id);
|
||||
if (!requester || !requester.isSuperAdmin) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Only super admins can create new admin accounts'
|
||||
});
|
||||
}
|
||||
|
||||
const { username, email, password, firstName, lastName, role, permissions } = req.body;
|
||||
|
||||
// Check if admin already exists
|
||||
const existingAdmin = await Admin.findOne({
|
||||
$or: [{ username }, { email }]
|
||||
});
|
||||
|
||||
if (existingAdmin) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Admin with this username or email already exists'
|
||||
});
|
||||
}
|
||||
|
||||
// Create new admin
|
||||
const newAdmin = new Admin({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
role: role || 'admin',
|
||||
permissions: permissions || ['manage_content', 'manage_rooms', 'manage_bookings']
|
||||
});
|
||||
|
||||
await newAdmin.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Admin account created successfully',
|
||||
data: {
|
||||
admin: {
|
||||
id: newAdmin._id,
|
||||
username: newAdmin.username,
|
||||
email: newAdmin.email,
|
||||
fullName: newAdmin.fullName,
|
||||
role: newAdmin.role,
|
||||
permissions: newAdmin.permissions
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Admin registration error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error creating admin account'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/admin/me
|
||||
// @desc Get current admin profile
|
||||
// @access Private (Admin)
|
||||
router.get('/me', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const admin = await Admin.findById(req.admin.id);
|
||||
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Admin not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { admin }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get admin profile error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching admin profile'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/admin/me
|
||||
// @desc Update current admin profile
|
||||
// @access Private (Admin)
|
||||
router.put('/me', adminAuth, [
|
||||
body('firstName').optional().notEmpty().trim(),
|
||||
body('lastName').optional().notEmpty().trim(),
|
||||
body('email').optional().isEmail().normalizeEmail(),
|
||||
body('avatar').optional().isURL()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { firstName, lastName, email, avatar } = req.body;
|
||||
const updateFields = {};
|
||||
|
||||
if (firstName) updateFields.firstName = firstName;
|
||||
if (lastName) updateFields.lastName = lastName;
|
||||
if (email) updateFields.email = email;
|
||||
if (avatar) updateFields.avatar = avatar;
|
||||
|
||||
const admin = await Admin.findByIdAndUpdate(
|
||||
req.admin.id,
|
||||
{ $set: updateFields },
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Profile updated successfully',
|
||||
data: { admin }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update admin profile error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating profile'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/admin/change-password
|
||||
// @desc Change admin password
|
||||
// @access Private (Admin)
|
||||
router.put('/change-password', adminAuth, [
|
||||
body('currentPassword').notEmpty().withMessage('Current password is required'),
|
||||
body('newPassword').isLength({ min: 8 }).withMessage('New password must be at least 8 characters')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const admin = await Admin.findById(req.admin.id);
|
||||
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Admin not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isMatch = await admin.comparePassword(currentPassword);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Current password is incorrect'
|
||||
});
|
||||
}
|
||||
|
||||
// Update password
|
||||
admin.password = newPassword;
|
||||
await admin.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Password changed successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error changing password'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/admin/list
|
||||
// @desc Get all admins (super-admin only)
|
||||
// @access Private (Super Admin)
|
||||
router.get('/list', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const requester = await Admin.findById(req.admin.id);
|
||||
if (!requester || !requester.isSuperAdmin) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Only super admins can view admin list'
|
||||
});
|
||||
}
|
||||
|
||||
const admins = await Admin.find().sort({ createdAt: -1 });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { admins }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get admin list error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching admin list'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/admin/stats
|
||||
// @desc Get dashboard statistics
|
||||
// @access Private (Admin)
|
||||
router.get('/stats', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const Booking = require('../models/Booking');
|
||||
const Room = require('../models/Room');
|
||||
const Guest = require('../models/Guest');
|
||||
const BlogPost = require('../models/BlogPost');
|
||||
|
||||
const [
|
||||
totalBookings,
|
||||
activeBookings,
|
||||
totalRooms,
|
||||
availableRooms,
|
||||
totalGuests,
|
||||
totalBlogPosts
|
||||
] = await Promise.all([
|
||||
Booking.countDocuments(),
|
||||
Booking.countDocuments({ status: { $in: ['Confirmed', 'Checked In'] } }),
|
||||
Room.countDocuments(),
|
||||
Room.countDocuments({ status: 'Available', isActive: true }),
|
||||
Guest.countDocuments(),
|
||||
BlogPost.countDocuments({ status: 'published' })
|
||||
]);
|
||||
|
||||
// Get revenue for current month
|
||||
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
|
||||
const revenueData = await Booking.aggregate([
|
||||
{
|
||||
$match: {
|
||||
status: { $in: ['Confirmed', 'Checked In', 'Checked Out'] },
|
||||
createdAt: { $gte: startOfMonth }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalRevenue: { $sum: '$totalAmount' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const monthlyRevenue = revenueData.length > 0 ? revenueData[0].totalRevenue : 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
stats: {
|
||||
bookings: {
|
||||
total: totalBookings,
|
||||
active: activeBookings
|
||||
},
|
||||
rooms: {
|
||||
total: totalRooms,
|
||||
available: availableRooms
|
||||
},
|
||||
guests: totalGuests,
|
||||
blogPosts: totalBlogPosts,
|
||||
revenue: {
|
||||
monthly: monthlyRevenue
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get stats error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching statistics'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Health check
|
||||
router.get('/health', (req, res) => {
|
||||
res.json({ success: true, service: 'admin', status: 'ok' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
509
routes/auth.js
Normal file
509
routes/auth.js
Normal file
@@ -0,0 +1,509 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Guest = require('../models/Guest');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const auth = require('../middleware/auth');
|
||||
const sendEmail = require('../utils/sendEmail');
|
||||
const logger = require('../utils/logger');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Generate JWT token
|
||||
const generateToken = (payload) => {
|
||||
return jwt.sign(payload, process.env.JWT_SECRET, {
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
|
||||
});
|
||||
};
|
||||
|
||||
// @route POST /api/auth/register
|
||||
// @desc Register a new guest
|
||||
// @access Public
|
||||
router.post('/register', [
|
||||
body('firstName').notEmpty().withMessage('First name is required').trim(),
|
||||
body('lastName').notEmpty().withMessage('Last name is required').trim(),
|
||||
body('email').isEmail().withMessage('Valid email is required').normalizeEmail(),
|
||||
body('phone').notEmpty().withMessage('Phone number is required').trim(),
|
||||
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
|
||||
body('confirmPassword').custom((value, { req }) => {
|
||||
if (value !== req.body.password) {
|
||||
throw new Error('Password confirmation does not match password');
|
||||
}
|
||||
return value;
|
||||
})
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { firstName, lastName, email, phone, password } = req.body;
|
||||
|
||||
// Check if guest already exists
|
||||
let guest = await Guest.findOne({ email });
|
||||
if (guest && guest.isRegistered) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Guest already registered with this email'
|
||||
});
|
||||
}
|
||||
|
||||
// Create or update guest
|
||||
if (guest) {
|
||||
// Update existing guest profile
|
||||
guest.firstName = firstName;
|
||||
guest.lastName = lastName;
|
||||
guest.phone = phone;
|
||||
guest.password = password;
|
||||
guest.isRegistered = true;
|
||||
guest.emailVerified = false;
|
||||
} else {
|
||||
// Create new guest
|
||||
guest = new Guest({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
phone,
|
||||
password,
|
||||
isRegistered: true,
|
||||
emailVerified: false
|
||||
});
|
||||
}
|
||||
|
||||
await guest.save();
|
||||
|
||||
// Generate email verification token
|
||||
const verificationToken = crypto.randomBytes(32).toString('hex');
|
||||
guest.emailVerificationToken = verificationToken;
|
||||
await guest.save();
|
||||
|
||||
// Send verification email
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Verify Your Email - The Old Vine Hotel',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
<h2>Email Verification</h2>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<p>Dear ${firstName} ${lastName},</p>
|
||||
|
||||
<p>Thank you for registering with The Old Vine Hotel! Please verify your email address to complete your registration.</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${process.env.CLIENT_URL}/verify-email?token=${verificationToken}"
|
||||
style="background: #D4AF37; color: black; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">
|
||||
Verify Email Address
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>If the button doesn't work, copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666;">${process.env.CLIENT_URL}/verify-email?token=${verificationToken}</p>
|
||||
|
||||
<p>This verification link will expire in 24 hours.</p>
|
||||
|
||||
<p>If you didn't create an account with us, please ignore this email.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Old Vine Hotel Team</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f4f4f4; padding: 20px; text-align: center; font-size: 12px;">
|
||||
<p>© 2025 The Old Vine Hotel. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.error('Failed to send verification email:', emailError);
|
||||
// Don't fail registration if email fails
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken({
|
||||
id: guest._id,
|
||||
email: guest.email,
|
||||
isAdmin: false
|
||||
});
|
||||
|
||||
logger.info('Guest registered successfully', {
|
||||
guestId: guest._id,
|
||||
email: guest.email
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Registration successful. Please check your email to verify your account.',
|
||||
data: {
|
||||
token,
|
||||
guest: {
|
||||
id: guest._id,
|
||||
firstName: guest.firstName,
|
||||
lastName: guest.lastName,
|
||||
email: guest.email,
|
||||
phone: guest.phone,
|
||||
emailVerified: guest.emailVerified,
|
||||
loyaltyProgram: guest.loyaltyProgram
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Registration error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during registration'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/auth/login
|
||||
// @desc Login guest
|
||||
// @access Public
|
||||
router.post('/login', [
|
||||
body('email').isEmail().withMessage('Valid email is required').normalizeEmail(),
|
||||
body('password').notEmpty().withMessage('Password is required')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Find guest with password field
|
||||
const guest = await Guest.findOne({ email, isRegistered: true }).select('+password');
|
||||
|
||||
if (!guest) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid email or password'
|
||||
});
|
||||
}
|
||||
|
||||
if (!guest.isActive) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Account is deactivated. Please contact support.'
|
||||
});
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isMatch = await guest.comparePassword(password);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid email or password'
|
||||
});
|
||||
}
|
||||
|
||||
// Update last login
|
||||
guest.lastLogin = new Date();
|
||||
guest.lastActivity = new Date();
|
||||
await guest.save();
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken({
|
||||
id: guest._id,
|
||||
email: guest.email,
|
||||
isAdmin: false
|
||||
});
|
||||
|
||||
logger.info('Guest login successful', {
|
||||
guestId: guest._id,
|
||||
email: guest.email
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
data: {
|
||||
token,
|
||||
guest: {
|
||||
id: guest._id,
|
||||
firstName: guest.firstName,
|
||||
lastName: guest.lastName,
|
||||
email: guest.email,
|
||||
phone: guest.phone,
|
||||
emailVerified: guest.emailVerified,
|
||||
loyaltyProgram: guest.loyaltyProgram,
|
||||
preferences: guest.preferences,
|
||||
isVIP: guest.isVIP
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during login'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/auth/me
|
||||
// @desc Get current guest
|
||||
// @access Private
|
||||
router.get('/me', auth, async (req, res) => {
|
||||
try {
|
||||
// Update last activity
|
||||
req.guest.lastActivity = new Date();
|
||||
await req.guest.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
guest: {
|
||||
id: req.guest._id,
|
||||
firstName: req.guest.firstName,
|
||||
lastName: req.guest.lastName,
|
||||
email: req.guest.email,
|
||||
phone: req.guest.phone,
|
||||
emailVerified: req.guest.emailVerified,
|
||||
loyaltyProgram: req.guest.loyaltyProgram,
|
||||
preferences: req.guest.preferences,
|
||||
isVIP: req.guest.isVIP,
|
||||
totalStays: req.guest.totalStays,
|
||||
totalSpent: req.guest.totalSpent
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get current guest error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/auth/forgot-password
|
||||
// @desc Send password reset email
|
||||
// @access Public
|
||||
router.post('/forgot-password', [
|
||||
body('email').isEmail().withMessage('Valid email is required').normalizeEmail()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { email } = req.body;
|
||||
|
||||
const guest = await Guest.findOne({ email, isRegistered: true });
|
||||
if (!guest) {
|
||||
// Don't reveal if email exists
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'If an account with that email exists, we have sent a password reset link.'
|
||||
});
|
||||
}
|
||||
|
||||
// Generate reset token
|
||||
const resetToken = guest.createPasswordResetToken();
|
||||
await guest.save();
|
||||
|
||||
// Send reset email
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Password Reset - The Old Vine Hotel',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
<h2>Password Reset</h2>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<p>Dear ${guest.firstName} ${guest.lastName},</p>
|
||||
|
||||
<p>You have requested to reset your password. Click the button below to reset it:</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${process.env.CLIENT_URL}/reset-password?token=${resetToken}"
|
||||
style="background: #D4AF37; color: black; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">
|
||||
Reset Password
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>If the button doesn't work, copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666;">${process.env.CLIENT_URL}/reset-password?token=${resetToken}</p>
|
||||
|
||||
<p>This reset link will expire in 10 minutes.</p>
|
||||
|
||||
<p>If you didn't request this password reset, please ignore this email.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Old Vine Hotel Team</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f4f4f4; padding: 20px; text-align: center; font-size: 12px;">
|
||||
<p>© 2025 The Old Vine Hotel. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.error('Failed to send password reset email:', emailError);
|
||||
guest.passwordResetToken = undefined;
|
||||
guest.passwordResetExpires = undefined;
|
||||
await guest.save();
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error sending password reset email'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'If an account with that email exists, we have sent a password reset link.'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Forgot password error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/auth/reset-password
|
||||
// @desc Reset password with token
|
||||
// @access Public
|
||||
router.post('/reset-password', [
|
||||
body('token').notEmpty().withMessage('Reset token is required'),
|
||||
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
|
||||
body('confirmPassword').custom((value, { req }) => {
|
||||
if (value !== req.body.password) {
|
||||
throw new Error('Password confirmation does not match password');
|
||||
}
|
||||
return value;
|
||||
})
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { token, password } = req.body;
|
||||
|
||||
// Hash the token
|
||||
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
|
||||
|
||||
// Find guest by token and check if token is still valid
|
||||
const guest = await Guest.findOne({
|
||||
passwordResetToken: hashedToken,
|
||||
passwordResetExpires: { $gt: Date.now() },
|
||||
isRegistered: true
|
||||
});
|
||||
|
||||
if (!guest) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Token is invalid or has expired'
|
||||
});
|
||||
}
|
||||
|
||||
// Set new password
|
||||
guest.password = password;
|
||||
guest.passwordResetToken = undefined;
|
||||
guest.passwordResetExpires = undefined;
|
||||
await guest.save();
|
||||
|
||||
logger.info('Password reset successful', {
|
||||
guestId: guest._id,
|
||||
email: guest.email
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Password reset successful. You can now log in with your new password.'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Reset password error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during password reset'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/auth/verify-email
|
||||
// @desc Verify email with token
|
||||
// @access Public
|
||||
router.post('/verify-email', [
|
||||
body('token').notEmpty().withMessage('Verification token is required')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { token } = req.body;
|
||||
|
||||
const guest = await Guest.findOne({
|
||||
emailVerificationToken: token,
|
||||
isRegistered: true
|
||||
});
|
||||
|
||||
if (!guest) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid verification token'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify email
|
||||
guest.emailVerified = true;
|
||||
guest.emailVerificationToken = undefined;
|
||||
await guest.save();
|
||||
|
||||
logger.info('Email verification successful', {
|
||||
guestId: guest._id,
|
||||
email: guest.email
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Email verified successfully!'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Email verification error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during email verification'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
286
routes/blog.js
Normal file
286
routes/blog.js
Normal file
@@ -0,0 +1,286 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const BlogPost = require('../models/BlogPost');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
|
||||
// @route GET /api/blog
|
||||
// @desc Get published blog posts
|
||||
// @access Public
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
category,
|
||||
tag,
|
||||
page = 1,
|
||||
limit = 10,
|
||||
featured = false
|
||||
} = req.query;
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const posts = await BlogPost.getPublished({
|
||||
category,
|
||||
tag,
|
||||
limit: parseInt(limit),
|
||||
skip,
|
||||
featured: featured === 'true'
|
||||
});
|
||||
|
||||
const total = await BlogPost.countDocuments({
|
||||
status: 'published',
|
||||
publishedAt: { $lte: new Date() },
|
||||
...(category && { category }),
|
||||
...(tag && { tags: tag }),
|
||||
...(featured === 'true' && { isFeatured: true })
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
posts,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get blog posts error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching blog posts'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/blog/:slug
|
||||
// @desc Get single blog post by slug
|
||||
// @access Public
|
||||
router.get('/:slug', async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
|
||||
const post = await BlogPost.findOne({
|
||||
slug,
|
||||
status: 'published',
|
||||
publishedAt: { $lte: new Date() }
|
||||
}).populate('author', 'firstName lastName avatar');
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Blog post not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Increment views
|
||||
await BlogPost.incrementViews(post._id);
|
||||
|
||||
// Get related posts
|
||||
const relatedPosts = await post.getRelatedPosts(3);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
post,
|
||||
relatedPosts
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get blog post error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching blog post'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/blog/admin/all
|
||||
// @desc Get all blog posts (admin)
|
||||
// @access Private (Admin)
|
||||
router.get('/admin/all', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { status, category, page = 1, limit = 20 } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const query = {};
|
||||
if (status) query.status = status;
|
||||
if (category) query.category = category;
|
||||
|
||||
const posts = await BlogPost.find(query)
|
||||
.populate('author', 'firstName lastName avatar')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit));
|
||||
|
||||
const total = await BlogPost.countDocuments(query);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
posts,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get all blog posts error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching blog posts'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/blog
|
||||
// @desc Create new blog post
|
||||
// @access Private (Admin)
|
||||
router.post('/', adminAuth, [
|
||||
body('title').notEmpty().trim().withMessage('Title is required'),
|
||||
body('excerpt').notEmpty().trim().withMessage('Excerpt is required'),
|
||||
body('content').notEmpty().withMessage('Content is required'),
|
||||
body('category').notEmpty().withMessage('Category is required')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const postData = {
|
||||
...req.body,
|
||||
author: req.admin.id
|
||||
};
|
||||
|
||||
const post = new BlogPost(postData);
|
||||
await post.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Blog post created successfully',
|
||||
data: { post }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create blog post error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error creating blog post'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/blog/:id
|
||||
// @desc Update blog post
|
||||
// @access Private (Admin)
|
||||
router.put('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const post = await BlogPost.findByIdAndUpdate(
|
||||
id,
|
||||
{ $set: req.body },
|
||||
{ new: true, runValidators: true }
|
||||
).populate('author', 'firstName lastName avatar');
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Blog post not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Blog post updated successfully',
|
||||
data: { post }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update blog post error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating blog post'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/blog/:id
|
||||
// @desc Delete blog post
|
||||
// @access Private (Admin)
|
||||
router.delete('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const post = await BlogPost.findByIdAndDelete(id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Blog post not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Blog post deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Delete blog post error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting blog post'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/blog/categories/list
|
||||
// @desc Get list of all categories
|
||||
// @access Public
|
||||
router.get('/categories/list', async (req, res) => {
|
||||
try {
|
||||
const categories = await BlogPost.distinct('category');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { categories }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get categories error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/blog/tags/list
|
||||
// @desc Get list of all tags
|
||||
// @access Public
|
||||
router.get('/tags/list', async (req, res) => {
|
||||
try {
|
||||
const tags = await BlogPost.distinct('tags');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { tags }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get tags error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching tags'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
608
routes/bookings.js
Normal file
608
routes/bookings.js
Normal file
@@ -0,0 +1,608 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Booking = require('../models/Booking');
|
||||
const Room = require('../models/Room');
|
||||
const Guest = require('../models/Guest');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const auth = require('../middleware/auth');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
||||
const sendEmail = require('../utils/sendEmail');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// @route POST /api/bookings
|
||||
// @desc Create a new booking
|
||||
// @access Public
|
||||
router.post('/', [
|
||||
body('guestInfo.firstName').notEmpty().withMessage('First name is required'),
|
||||
body('guestInfo.lastName').notEmpty().withMessage('Last name is required'),
|
||||
body('guestInfo.email').isEmail().withMessage('Valid email is required'),
|
||||
body('guestInfo.phone').notEmpty().withMessage('Phone number is required'),
|
||||
body('roomId').isMongoId().withMessage('Valid room ID is required'),
|
||||
body('checkInDate').isISO8601().withMessage('Valid check-in date is required'),
|
||||
body('checkOutDate').isISO8601().withMessage('Valid check-out date is required'),
|
||||
body('numberOfGuests.adults').isInt({ min: 1 }).withMessage('At least 1 adult required'),
|
||||
body('numberOfGuests.children').optional().isInt({ min: 0 }),
|
||||
body('paymentMethodId').notEmpty().withMessage('Payment method is required')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
guestInfo,
|
||||
roomId,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
numberOfGuests,
|
||||
specialRequests,
|
||||
paymentMethodId
|
||||
} = req.body;
|
||||
|
||||
const checkIn = new Date(checkInDate);
|
||||
const checkOut = new Date(checkOutDate);
|
||||
|
||||
// Validate dates
|
||||
if (checkIn >= checkOut) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Check-out date must be after check-in date'
|
||||
});
|
||||
}
|
||||
|
||||
if (checkIn < new Date()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Check-in date cannot be in the past'
|
||||
});
|
||||
}
|
||||
|
||||
// Check room availability
|
||||
const room = await Room.findById(roomId);
|
||||
if (!room || !room.isActive) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
const totalGuests = numberOfGuests.adults + (numberOfGuests.children || 0);
|
||||
if (room.maxOccupancy < totalGuests) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Room can accommodate maximum ${room.maxOccupancy} guests`
|
||||
});
|
||||
}
|
||||
|
||||
const isAvailable = await room.isAvailable(checkIn, checkOut);
|
||||
if (!isAvailable) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Room is not available for the selected dates'
|
||||
});
|
||||
}
|
||||
|
||||
// Find or create guest
|
||||
let guest = await Guest.findOne({ email: guestInfo.email });
|
||||
if (!guest) {
|
||||
guest = new Guest({
|
||||
...guestInfo,
|
||||
isRegistered: false
|
||||
});
|
||||
await guest.save();
|
||||
} else {
|
||||
// Update guest information if provided
|
||||
Object.assign(guest, guestInfo);
|
||||
await guest.save();
|
||||
}
|
||||
|
||||
// Calculate pricing
|
||||
const numberOfNights = Math.ceil((checkOut - checkIn) / (1000 * 60 * 60 * 24));
|
||||
const roomRate = room.currentPrice;
|
||||
const subtotal = roomRate * numberOfNights;
|
||||
const taxes = subtotal * 0.12; // 12% tax
|
||||
const totalAmount = subtotal + taxes;
|
||||
|
||||
// Create Stripe payment intent
|
||||
let paymentIntent;
|
||||
try {
|
||||
paymentIntent = await stripe.paymentIntents.create({
|
||||
amount: Math.round(totalAmount * 100), // Stripe uses cents
|
||||
currency: 'usd',
|
||||
payment_method: paymentMethodId,
|
||||
confirmation_method: 'manual',
|
||||
confirm: true,
|
||||
metadata: {
|
||||
roomId: roomId,
|
||||
guestEmail: guestInfo.email,
|
||||
checkInDate: checkInDate,
|
||||
checkOutDate: checkOutDate
|
||||
}
|
||||
});
|
||||
} catch (stripeError) {
|
||||
logger.error('Stripe payment error:', stripeError);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Payment processing failed',
|
||||
error: stripeError.message
|
||||
});
|
||||
}
|
||||
|
||||
// Create booking
|
||||
const booking = new Booking({
|
||||
guest: guest._id,
|
||||
room: roomId,
|
||||
checkInDate: checkIn,
|
||||
checkOutDate: checkOut,
|
||||
numberOfGuests,
|
||||
roomRate,
|
||||
numberOfNights,
|
||||
subtotal,
|
||||
taxes,
|
||||
totalAmount,
|
||||
specialRequests,
|
||||
paymentStatus: paymentIntent.status === 'succeeded' ? 'Paid' : 'Pending',
|
||||
paymentMethod: 'Credit Card',
|
||||
stripePaymentIntentId: paymentIntent.id,
|
||||
status: paymentIntent.status === 'succeeded' ? 'Confirmed' : 'Pending',
|
||||
bookingSource: 'Direct'
|
||||
});
|
||||
|
||||
await booking.save();
|
||||
await booking.populate(['guest', 'room']);
|
||||
|
||||
// Update guest statistics if payment successful
|
||||
if (paymentIntent.status === 'succeeded') {
|
||||
await guest.updateStayStats(totalAmount);
|
||||
await guest.addLoyaltyPoints(Math.floor(totalAmount / 10)); // 1 point per $10
|
||||
}
|
||||
|
||||
// Send confirmation email
|
||||
if (paymentIntent.status === 'succeeded') {
|
||||
try {
|
||||
await sendEmail({
|
||||
to: guest.email,
|
||||
subject: 'Booking Confirmation - The Old Vine Hotel',
|
||||
template: 'bookingConfirmation',
|
||||
context: {
|
||||
guest: guest,
|
||||
booking: booking,
|
||||
room: room
|
||||
}
|
||||
});
|
||||
|
||||
booking.emailConfirmationSent = true;
|
||||
await booking.save();
|
||||
} catch (emailError) {
|
||||
logger.error('Email sending error:', emailError);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: paymentIntent.status === 'succeeded'
|
||||
? 'Booking confirmed successfully'
|
||||
: 'Booking created, payment processing',
|
||||
data: {
|
||||
booking,
|
||||
paymentIntent: {
|
||||
id: paymentIntent.id,
|
||||
status: paymentIntent.status,
|
||||
client_secret: paymentIntent.client_secret
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error creating booking:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while creating booking'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/bookings/:bookingNumber
|
||||
// @desc Get booking by booking number
|
||||
// @access Public (with confirmation code) / Private
|
||||
router.get('/:bookingNumber', async (req, res) => {
|
||||
try {
|
||||
const { bookingNumber } = req.params;
|
||||
const { confirmationCode } = req.query;
|
||||
|
||||
let booking;
|
||||
|
||||
// If confirmation code provided, allow public access
|
||||
if (confirmationCode) {
|
||||
booking = await Booking.findOne({
|
||||
bookingNumber,
|
||||
confirmationCode
|
||||
}).populate(['guest', 'room']);
|
||||
} else {
|
||||
// Otherwise require authentication (implement auth middleware check here)
|
||||
booking = await Booking.findOne({ bookingNumber })
|
||||
.populate(['guest', 'room']);
|
||||
}
|
||||
|
||||
if (!booking) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Booking not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: booking
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching booking:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching booking'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/bookings/:bookingNumber/cancel
|
||||
// @desc Cancel a booking
|
||||
// @access Public (with confirmation code)
|
||||
router.put('/:bookingNumber/cancel', [
|
||||
body('confirmationCode').notEmpty().withMessage('Confirmation code is required'),
|
||||
body('reason').optional().isLength({ max: 500 }).withMessage('Reason too long')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { bookingNumber } = req.params;
|
||||
const { confirmationCode, reason } = req.body;
|
||||
|
||||
const booking = await Booking.findOne({
|
||||
bookingNumber,
|
||||
confirmationCode
|
||||
}).populate(['guest', 'room']);
|
||||
|
||||
if (!booking) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Booking not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (!booking.canBeCancelled()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Booking cannot be cancelled at this time'
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate cancellation fee
|
||||
const cancellationFee = booking.calculateCancellationFee();
|
||||
const refundAmount = booking.totalAmount - cancellationFee;
|
||||
|
||||
// Process refund with Stripe
|
||||
if (booking.stripePaymentIntentId && refundAmount > 0) {
|
||||
try {
|
||||
await stripe.refunds.create({
|
||||
payment_intent: booking.stripePaymentIntentId,
|
||||
amount: Math.round(refundAmount * 100), // Stripe uses cents
|
||||
metadata: {
|
||||
bookingNumber: bookingNumber,
|
||||
reason: reason || 'Guest cancellation'
|
||||
}
|
||||
});
|
||||
} catch (stripeError) {
|
||||
logger.error('Stripe refund error:', stripeError);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Refund processing failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update booking
|
||||
booking.status = 'Cancelled';
|
||||
booking.cancellationReason = reason;
|
||||
booking.cancellationDate = new Date();
|
||||
booking.cancellationFee = cancellationFee;
|
||||
booking.refundAmount = refundAmount;
|
||||
booking.paymentStatus = refundAmount > 0 ? 'Refunded' : 'Paid';
|
||||
|
||||
await booking.save();
|
||||
|
||||
// Send cancellation email
|
||||
try {
|
||||
await sendEmail({
|
||||
to: booking.guest.email,
|
||||
subject: 'Booking Cancellation - The Old Vine Hotel',
|
||||
template: 'bookingCancellation',
|
||||
context: {
|
||||
guest: booking.guest,
|
||||
booking: booking,
|
||||
room: booking.room,
|
||||
cancellationFee,
|
||||
refundAmount
|
||||
}
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.error('Cancellation email error:', emailError);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Booking cancelled successfully',
|
||||
data: {
|
||||
bookingNumber,
|
||||
cancellationFee,
|
||||
refundAmount,
|
||||
status: booking.status
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error cancelling booking:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while cancelling booking'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/bookings
|
||||
// @desc Get all bookings (Admin only)
|
||||
// @access Private/Admin
|
||||
router.get('/', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
status,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
guestEmail,
|
||||
roomType,
|
||||
sortBy = 'createdAt',
|
||||
sortOrder = 'desc'
|
||||
} = req.query;
|
||||
|
||||
// Build filter
|
||||
let filter = {};
|
||||
|
||||
if (status) filter.status = status;
|
||||
if (guestEmail) {
|
||||
const guest = await Guest.findOne({ email: guestEmail });
|
||||
if (guest) filter.guest = guest._id;
|
||||
}
|
||||
|
||||
if (checkInDate || checkOutDate) {
|
||||
filter.checkInDate = {};
|
||||
if (checkInDate) filter.checkInDate.$gte = new Date(checkInDate);
|
||||
if (checkOutDate) filter.checkInDate.$lte = new Date(checkOutDate);
|
||||
}
|
||||
|
||||
// Build aggregation pipeline for room type filter
|
||||
let aggregationPipeline = [{ $match: filter }];
|
||||
|
||||
if (roomType) {
|
||||
aggregationPipeline.push(
|
||||
{
|
||||
$lookup: {
|
||||
from: 'rooms',
|
||||
localField: 'room',
|
||||
foreignField: '_id',
|
||||
as: 'roomData'
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
'roomData.type': roomType
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add pagination and sorting
|
||||
const sortOptions = {};
|
||||
sortOptions[sortBy] = sortOrder === 'desc' ? -1 : 1;
|
||||
|
||||
aggregationPipeline.push(
|
||||
{ $sort: sortOptions },
|
||||
{ $skip: (parseInt(page) - 1) * parseInt(limit) },
|
||||
{ $limit: parseInt(limit) },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'guests',
|
||||
localField: 'guest',
|
||||
foreignField: '_id',
|
||||
as: 'guest'
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'rooms',
|
||||
localField: 'room',
|
||||
foreignField: '_id',
|
||||
as: 'room'
|
||||
}
|
||||
},
|
||||
{
|
||||
$unwind: '$guest'
|
||||
},
|
||||
{
|
||||
$unwind: '$room'
|
||||
}
|
||||
);
|
||||
|
||||
const [bookings, totalCount] = await Promise.all([
|
||||
Booking.aggregate(aggregationPipeline),
|
||||
Booking.countDocuments(filter)
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(totalCount / parseInt(limit));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
bookings,
|
||||
pagination: {
|
||||
currentPage: parseInt(page),
|
||||
totalPages,
|
||||
totalCount,
|
||||
hasNextPage: parseInt(page) < totalPages,
|
||||
hasPrevPage: parseInt(page) > 1
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching bookings:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching bookings'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/bookings/:id/checkin
|
||||
// @desc Check in a guest (Admin only)
|
||||
// @access Private/Admin
|
||||
router.put('/:id/checkin', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const booking = await Booking.findById(req.params.id)
|
||||
.populate(['guest', 'room']);
|
||||
|
||||
if (!booking) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Booking not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (booking.status !== 'Confirmed') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Booking must be confirmed to check in'
|
||||
});
|
||||
}
|
||||
|
||||
// Update booking status
|
||||
booking.status = 'Checked In';
|
||||
booking.actualCheckInTime = new Date();
|
||||
await booking.save();
|
||||
|
||||
// Update room status
|
||||
const room = await Room.findById(booking.room._id);
|
||||
room.status = 'Occupied';
|
||||
await room.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Guest checked in successfully',
|
||||
data: booking
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error checking in guest:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during check-in'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/bookings/:id/checkout
|
||||
// @desc Check out a guest (Admin only)
|
||||
// @access Private/Admin
|
||||
router.put('/:id/checkout', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const booking = await Booking.findById(req.params.id)
|
||||
.populate(['guest', 'room']);
|
||||
|
||||
if (!booking) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Booking not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (booking.status !== 'Checked In') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Guest must be checked in to check out'
|
||||
});
|
||||
}
|
||||
|
||||
// Update booking status
|
||||
booking.status = 'Checked Out';
|
||||
booking.actualCheckOutTime = new Date();
|
||||
await booking.save();
|
||||
|
||||
// Update room status
|
||||
const room = await Room.findById(booking.room._id);
|
||||
room.status = 'Available';
|
||||
room.cleaningStatus = 'Dirty';
|
||||
room.lastCleaning = new Date();
|
||||
await room.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Guest checked out successfully',
|
||||
data: booking
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error checking out guest:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during check-out'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/bookings/analytics/revenue
|
||||
// @desc Get revenue analytics (Admin only)
|
||||
// @access Private/Admin
|
||||
router.get('/analytics/revenue', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate, groupBy = 'day' } = req.query;
|
||||
|
||||
const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const end = endDate ? new Date(endDate) : new Date();
|
||||
|
||||
const revenueData = await Booking.generateRevenueReport(start, end);
|
||||
|
||||
// Calculate total metrics
|
||||
const totalRevenue = revenueData.reduce((sum, day) => sum + day.totalRevenue, 0);
|
||||
const totalBookings = revenueData.reduce((sum, day) => sum + day.bookingsCount, 0);
|
||||
const averageBookingValue = totalBookings > 0 ? totalRevenue / totalBookings : 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
revenueData,
|
||||
summary: {
|
||||
totalRevenue,
|
||||
totalBookings,
|
||||
averageBookingValue,
|
||||
dateRange: { start, end }
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error generating revenue analytics:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while generating analytics'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
280
routes/contact.js
Normal file
280
routes/contact.js
Normal file
@@ -0,0 +1,280 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const sendEmail = require('../utils/sendEmail');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// @route POST /api/contact
|
||||
// @desc Send contact form message
|
||||
// @access Public
|
||||
router.post('/', [
|
||||
body('name').notEmpty().withMessage('Name is required').trim(),
|
||||
body('email').isEmail().withMessage('Valid email is required').normalizeEmail(),
|
||||
body('phone').optional().isMobilePhone().withMessage('Valid phone number required'),
|
||||
body('subject').optional().trim(),
|
||||
body('message').notEmpty().withMessage('Message is required').isLength({ min: 10, max: 1000 }).withMessage('Message must be between 10 and 1000 characters')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { name, email, phone, subject, message } = req.body;
|
||||
|
||||
// Log the contact form submission
|
||||
logger.info('Contact form submission received', {
|
||||
name,
|
||||
email,
|
||||
subject: subject || 'General Inquiry',
|
||||
ip: req.ip
|
||||
});
|
||||
|
||||
// Send email to hotel management
|
||||
try {
|
||||
await sendEmail({
|
||||
to: process.env.HOTEL_EMAIL || 'info@oldvinehotel.com',
|
||||
subject: `Contact Form: ${subject || 'General Inquiry'}`,
|
||||
template: 'contactForm',
|
||||
context: {
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
message,
|
||||
timestamp: new Date().toLocaleString()
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Contact form email sent successfully', { name, email });
|
||||
} catch (emailError) {
|
||||
logger.error('Failed to send contact form email:', emailError);
|
||||
// Don't fail the request if email fails
|
||||
}
|
||||
|
||||
// Send auto-reply to customer
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Thank you for contacting The Old Vine Hotel',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<p>Dear ${name},</p>
|
||||
|
||||
<p>Thank you for contacting The Old Vine Hotel. We have received your message and will respond within 24 hours.</p>
|
||||
|
||||
<div style="background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3>Your Message:</h3>
|
||||
<p><strong>Subject:</strong> ${subject || 'General Inquiry'}</p>
|
||||
<p><strong>Message:</strong> ${message}</p>
|
||||
</div>
|
||||
|
||||
<p>In the meantime, feel free to:</p>
|
||||
<ul>
|
||||
<li>Browse our rooms and amenities on our website</li>
|
||||
<li>Call us directly at +1 (555) 123-4567</li>
|
||||
<li>Follow us on social media for updates and special offers</li>
|
||||
</ul>
|
||||
|
||||
<p>We look forward to serving you!</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Old Vine Hotel Team</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f4f4f4; padding: 20px; text-align: center; font-size: 12px;">
|
||||
<p>© 2025 The Old Vine Hotel. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.error('Failed to send contact form auto-reply:', emailError);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Thank you for your message. We will get back to you within 24 hours.'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Contact form processing error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'An error occurred while processing your message. Please try again.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/contact/newsletter
|
||||
// @desc Subscribe to newsletter
|
||||
// @access Public
|
||||
router.post('/newsletter', [
|
||||
body('email').isEmail().withMessage('Valid email is required').normalizeEmail(),
|
||||
body('name').optional().trim()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { email, name } = req.body;
|
||||
|
||||
// Log newsletter subscription
|
||||
logger.info('Newsletter subscription', {
|
||||
email,
|
||||
name: name || 'Not provided',
|
||||
ip: req.ip
|
||||
});
|
||||
|
||||
// In a real application, you would save this to a newsletter database
|
||||
// For now, we'll just send a confirmation email
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Welcome to The Old Vine Hotel Newsletter',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
<h2>Newsletter Subscription Confirmed</h2>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<p>Dear ${name || 'Valued Guest'},</p>
|
||||
|
||||
<p>Thank you for subscribing to The Old Vine Hotel newsletter!</p>
|
||||
|
||||
<p>You'll now receive:</p>
|
||||
<ul>
|
||||
<li>Exclusive special offers and promotions</li>
|
||||
<li>Updates on hotel amenities and services</li>
|
||||
<li>Local event recommendations</li>
|
||||
<li>Seasonal packages and deals</li>
|
||||
</ul>
|
||||
|
||||
<p>We're excited to keep you informed about everything happening at The Old Vine Hotel.</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="https://oldvinehotel.com" style="background: #D4AF37; color: black; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">Visit Our Website</a>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Old Vine Hotel Team</p>
|
||||
|
||||
<p style="font-size: 12px; color: #666; margin-top: 30px;">
|
||||
You can unsubscribe from these emails at any time by clicking the unsubscribe link in any newsletter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f4f4f4; padding: 20px; text-align: center; font-size: 12px;">
|
||||
<p>© 2025 The Old Vine Hotel. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.error('Failed to send newsletter confirmation:', emailError);
|
||||
// Don't fail the request if email fails
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Thank you for subscribing! You will receive a confirmation email shortly.'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Newsletter subscription error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'An error occurred while processing your subscription. Please try again.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/contact/info
|
||||
// @desc Get hotel contact information
|
||||
// @access Public
|
||||
router.get('/info', (req, res) => {
|
||||
const contactInfo = {
|
||||
hotel: {
|
||||
name: process.env.HOTEL_NAME || 'The Old Vine Hotel',
|
||||
address: {
|
||||
street: 'Old Damascus City',
|
||||
city: 'Damascus',
|
||||
state: 'Damascus Governorate',
|
||||
zipCode: '',
|
||||
country: 'Syria',
|
||||
formatted: process.env.HOTEL_ADDRESS || 'Old Damascus City'
|
||||
},
|
||||
phone: process.env.HOTEL_PHONE || '+963 986 703 070',
|
||||
email: process.env.HOTEL_EMAIL || 'info@oldvinehotel.com',
|
||||
website: process.env.HOTEL_WEBSITE || 'https://oldvinehotel.com',
|
||||
whatsapp: process.env.WHATSAPP_PHONE_NUMBER || '+963 986 703 070'
|
||||
},
|
||||
departments: {
|
||||
reservations: {
|
||||
phone: '+963 986 703 070',
|
||||
email: 'reservations@oldvinehotel.com',
|
||||
hours: '24/7'
|
||||
},
|
||||
concierge: {
|
||||
phone: '+963 986 703 070',
|
||||
email: 'concierge@oldvinehotel.com',
|
||||
hours: '6:00 AM - 12:00 AM'
|
||||
},
|
||||
restaurant: {
|
||||
phone: '+963 986 703 070',
|
||||
email: 'restaurant@oldvinehotel.com',
|
||||
hours: '6:30 AM - 11:00 PM'
|
||||
},
|
||||
spa: {
|
||||
phone: '+963 986 703 070',
|
||||
email: 'spa@oldvinehotel.com',
|
||||
hours: '8:00 AM - 9:00 PM'
|
||||
},
|
||||
events: {
|
||||
phone: '+963 986 703 070',
|
||||
email: 'events@oldvinehotel.com',
|
||||
hours: '9:00 AM - 6:00 PM'
|
||||
}
|
||||
},
|
||||
socialMedia: {
|
||||
facebook: 'https://facebook.com/oldvinehotel',
|
||||
instagram: 'https://instagram.com/oldvinehotel',
|
||||
twitter: 'https://twitter.com/oldvinehotel',
|
||||
linkedin: 'https://linkedin.com/company/oldvinehotel'
|
||||
},
|
||||
checkInOut: {
|
||||
checkIn: '3:00 PM',
|
||||
checkOut: '11:00 AM',
|
||||
earlyCheckIn: 'Available upon request (additional fee may apply)',
|
||||
lateCheckOut: 'Available upon request (additional fee may apply)'
|
||||
},
|
||||
policies: {
|
||||
cancellation: '24 hours before arrival',
|
||||
petPolicy: 'Pets allowed with prior arrangement',
|
||||
smokingPolicy: 'Non-smoking hotel',
|
||||
childrenPolicy: 'Children of all ages welcome'
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: contactInfo
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
192
routes/content.js
Normal file
192
routes/content.js
Normal file
@@ -0,0 +1,192 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Content = require('../models/Content');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
|
||||
// @route GET /api/content/:page
|
||||
// @desc Get content for a specific page
|
||||
// @access Public
|
||||
router.get('/:page', async (req, res) => {
|
||||
try {
|
||||
const { page } = req.params;
|
||||
|
||||
let content = await Content.findOne({ page, isPublished: true });
|
||||
|
||||
// If content doesn't exist, create default content
|
||||
if (!content) {
|
||||
content = await Content.create({
|
||||
page,
|
||||
hero: {
|
||||
title: `Welcome to ${page.charAt(0).toUpperCase() + page.slice(1)}`,
|
||||
subtitle: 'Discover luxury and comfort',
|
||||
description: 'Experience the finest hospitality'
|
||||
},
|
||||
sections: [],
|
||||
seo: {
|
||||
title: `${page.charAt(0).toUpperCase() + page.slice(1)} - The Old Vine Hotel`,
|
||||
description: `Explore our ${page} page`
|
||||
},
|
||||
isPublished: true
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { content }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get content error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching content'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/content/:page
|
||||
// @desc Update content for a specific page
|
||||
// @access Private (Admin)
|
||||
router.put('/:page', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { page } = req.params;
|
||||
const { hero, sections, seo, isPublished } = req.body;
|
||||
|
||||
let content = await Content.findOne({ page });
|
||||
|
||||
if (!content) {
|
||||
// Create new content
|
||||
content = new Content({
|
||||
page,
|
||||
hero,
|
||||
sections,
|
||||
seo,
|
||||
isPublished,
|
||||
lastModifiedBy: req.admin.id
|
||||
});
|
||||
} else {
|
||||
// Update existing content
|
||||
if (hero) content.hero = hero;
|
||||
if (sections) content.sections = sections;
|
||||
if (seo) content.seo = seo;
|
||||
if (typeof isPublished !== 'undefined') content.isPublished = isPublished;
|
||||
content.lastModifiedBy = req.admin.id;
|
||||
}
|
||||
|
||||
await content.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Content updated successfully',
|
||||
data: { content }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update content error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating content'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/content
|
||||
// @desc Get all content pages (admin)
|
||||
// @access Private (Admin)
|
||||
router.get('/', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const contents = await Content.find()
|
||||
.populate('lastModifiedBy', 'firstName lastName')
|
||||
.sort({ page: 1 });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { contents }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get all content error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching content'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/content/:page/section
|
||||
// @desc Add a section to page content
|
||||
// @access Private (Admin)
|
||||
router.post('/:page/section', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { page } = req.params;
|
||||
const section = req.body;
|
||||
|
||||
const content = await Content.findOne({ page });
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Content page not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Set section order if not provided
|
||||
if (!section.order) {
|
||||
section.order = content.sections.length;
|
||||
}
|
||||
|
||||
content.sections.push(section);
|
||||
content.lastModifiedBy = req.admin.id;
|
||||
await content.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Section added successfully',
|
||||
data: { content }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Add section error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error adding section'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/content/:page/section/:sectionId
|
||||
// @desc Remove a section from page content
|
||||
// @access Private (Admin)
|
||||
router.delete('/:page/section/:sectionId', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { page, sectionId } = req.params;
|
||||
|
||||
const content = await Content.findOne({ page });
|
||||
|
||||
if (!content) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Content page not found'
|
||||
});
|
||||
}
|
||||
|
||||
content.sections = content.sections.filter(
|
||||
section => section.sectionId !== sectionId
|
||||
);
|
||||
|
||||
content.lastModifiedBy = req.admin.id;
|
||||
await content.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Section removed successfully',
|
||||
data: { content }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Remove section error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error removing section'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
11
routes/gallery.js
Normal file
11
routes/gallery.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Minimal gallery router stub to satisfy app.use
|
||||
router.get('/', (req, res) => {
|
||||
res.json({ success: true, images: [] });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
181
routes/galleryCategories.js
Normal file
181
routes/galleryCategories.js
Normal file
@@ -0,0 +1,181 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const GalleryCategory = require('../models/GalleryCategory');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
|
||||
// @route GET /api/gallery-categories
|
||||
// @desc Get all active gallery categories
|
||||
// @access Public
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const categories = await GalleryCategory.find({ isActive: true })
|
||||
.sort({ displayOrder: 1, name: 1 })
|
||||
.lean();
|
||||
|
||||
// Add virtual properties
|
||||
const categoriesWithStats = categories.map((category) => ({
|
||||
...category,
|
||||
primaryImage: category.images.find(img => img.isPrimary)?.url ||
|
||||
(category.images.length > 0 ? category.images[0].url : null),
|
||||
imageCount: category.images.length
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { categories: categoriesWithStats }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching gallery categories:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching gallery categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/gallery-categories/:slug
|
||||
// @desc Get single gallery category with all images
|
||||
// @access Public
|
||||
router.get('/:slug', async (req, res) => {
|
||||
try {
|
||||
const category = await GalleryCategory.findOne({
|
||||
slug: req.params.slug,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Gallery category not found'
|
||||
});
|
||||
}
|
||||
|
||||
const categoryData = category.toObject();
|
||||
categoryData.primaryImage = category.primaryImage;
|
||||
categoryData.imageCount = category.images.length;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { category: categoryData }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching gallery category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching gallery category'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== ADMIN ROUTES ====================
|
||||
|
||||
// @route GET /api/gallery-categories/admin/all
|
||||
// @desc Get all gallery categories (including inactive) - Admin only
|
||||
// @access Private/Admin
|
||||
router.get('/admin/all', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const categories = await GalleryCategory.find()
|
||||
.sort({ displayOrder: 1, name: 1 });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { categories }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching all gallery categories:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching gallery categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/gallery-categories
|
||||
// @desc Create a new gallery category - Admin only
|
||||
// @access Private/Admin
|
||||
router.post('/', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const category = new GalleryCategory(req.body);
|
||||
await category.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Gallery category created successfully',
|
||||
data: { category }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating gallery category:', error);
|
||||
if (error.code === 11000) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category with this name or slug already exists'
|
||||
});
|
||||
}
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while creating gallery category'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/gallery-categories/:id
|
||||
// @desc Update a gallery category - Admin only
|
||||
// @access Private/Admin
|
||||
router.put('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const category = await GalleryCategory.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
req.body,
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Gallery category not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Gallery category updated successfully',
|
||||
data: { category }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating gallery category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while updating gallery category'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/gallery-categories/:id
|
||||
// @desc Delete a gallery category - Admin only
|
||||
// @access Private/Admin
|
||||
router.delete('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const category = await GalleryCategory.findByIdAndDelete(req.params.id);
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Gallery category not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Gallery category deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting gallery category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while deleting gallery category'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
11
routes/guests.js
Normal file
11
routes/guests.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Minimal guests router stub to satisfy app.use
|
||||
router.get('/health', (req, res) => {
|
||||
res.json({ success: true, service: 'guests', status: 'ok' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
347
routes/integrations.js
Normal file
347
routes/integrations.js
Normal file
@@ -0,0 +1,347 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const OperaPMSService = require('../services/OperaPMSService');
|
||||
const BookingComService = require('../services/BookingComService');
|
||||
const TripComService = require('../services/TripComService');
|
||||
const ExpediaService = require('../services/ExpediaService');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Initialize services
|
||||
const operaPMS = new OperaPMSService();
|
||||
const bookingCom = new BookingComService();
|
||||
const tripCom = new TripComService();
|
||||
const expedia = new ExpediaService();
|
||||
|
||||
// @route GET /api/integrations/health
|
||||
// @desc Check health of all integrations
|
||||
// @access Private/Admin
|
||||
router.get('/health', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const healthChecks = await Promise.allSettled([
|
||||
operaPMS.healthCheck(),
|
||||
bookingCom.healthCheck(),
|
||||
tripCom.healthCheck(),
|
||||
expedia.healthCheck()
|
||||
]);
|
||||
|
||||
const results = {
|
||||
operaPMS: healthChecks[0].status === 'fulfilled' ? healthChecks[0].value : { status: 'error', error: healthChecks[0].reason.message },
|
||||
bookingCom: healthChecks[1].status === 'fulfilled' ? healthChecks[1].value : { status: 'error', error: healthChecks[1].reason.message },
|
||||
tripCom: healthChecks[2].status === 'fulfilled' ? healthChecks[2].value : { status: 'error', error: healthChecks[2].reason.message },
|
||||
expedia: healthChecks[3].status === 'fulfilled' ? healthChecks[3].value : { status: 'error', error: healthChecks[3].reason.message }
|
||||
};
|
||||
|
||||
const overallStatus = Object.values(results).every(result => result.status === 'connected') ? 'healthy' : 'partial';
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
overallStatus,
|
||||
services: results,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Integration health check failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to check integration health'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/integrations/opera/sync-rooms
|
||||
// @desc Sync room inventory with Opera PMS
|
||||
// @access Private/Admin
|
||||
router.post('/opera/sync-rooms', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await operaPMS.syncRoomInventory();
|
||||
|
||||
logger.integrationLog('Room inventory sync completed', {
|
||||
admin: req.admin.email,
|
||||
result
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Room inventory synchronized successfully',
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Room inventory sync failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to sync room inventory'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/integrations/opera/create-reservation
|
||||
// @desc Create reservation in Opera PMS
|
||||
// @access Private (called internally)
|
||||
router.post('/opera/create-reservation', async (req, res) => {
|
||||
try {
|
||||
const { booking } = req.body;
|
||||
const result = await operaPMS.createReservation(booking);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Opera PMS reservation creation failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to create reservation in Opera PMS'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/integrations/sync-rates
|
||||
// @desc Sync rates across all platforms
|
||||
// @access Private/Admin
|
||||
router.post('/sync-rates', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { roomType, rates, dates } = req.body;
|
||||
|
||||
// Sync rates to all platforms
|
||||
const syncPromises = [
|
||||
bookingCom.updateRates(roomType, rates, dates),
|
||||
tripCom.updateRates(roomType, rates, dates),
|
||||
expedia.updateRates(roomType, rates, dates)
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(syncPromises);
|
||||
|
||||
const syncResults = {
|
||||
bookingCom: results[0].status === 'fulfilled' ? results[0].value : { error: results[0].reason.message },
|
||||
tripCom: results[1].status === 'fulfilled' ? results[1].value : { error: results[1].reason.message },
|
||||
expedia: results[2].status === 'fulfilled' ? results[2].value : { error: results[2].reason.message }
|
||||
};
|
||||
|
||||
logger.integrationLog('Rate sync completed', {
|
||||
admin: req.admin.email,
|
||||
roomType,
|
||||
dates,
|
||||
results: syncResults
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Rates synchronized across platforms',
|
||||
data: syncResults
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Rate sync failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to sync rates'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/integrations/sync-availability
|
||||
// @desc Sync availability across all platforms
|
||||
// @access Private/Admin
|
||||
router.post('/sync-availability', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { roomType, availability, dates } = req.body;
|
||||
|
||||
// Sync availability to all platforms
|
||||
const syncPromises = [
|
||||
bookingCom.updateAvailability(roomType, availability, dates),
|
||||
tripCom.updateAvailability(roomType, availability, dates),
|
||||
expedia.updateAvailability(roomType, availability, dates)
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(syncPromises);
|
||||
|
||||
const syncResults = {
|
||||
bookingCom: results[0].status === 'fulfilled' ? results[0].value : { error: results[0].reason.message },
|
||||
tripCom: results[1].status === 'fulfilled' ? results[1].value : { error: results[1].reason.message },
|
||||
expedia: results[2].status === 'fulfilled' ? results[2].value : { error: results[2].reason.message }
|
||||
};
|
||||
|
||||
logger.integrationLog('Availability sync completed', {
|
||||
admin: req.admin.email,
|
||||
roomType,
|
||||
dates,
|
||||
results: syncResults
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Availability synchronized across platforms',
|
||||
data: syncResults
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Availability sync failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to sync availability'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/integrations/bookings/external
|
||||
// @desc Get bookings from external platforms
|
||||
// @access Private/Admin
|
||||
router.get('/bookings/external', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const start = new Date(startDate || Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
const end = new Date(endDate || Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Fetch bookings from all platforms
|
||||
const bookingPromises = [
|
||||
bookingCom.getBookings(start, end),
|
||||
tripCom.getBookings(start, end),
|
||||
expedia.getBookings(start, end)
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(bookingPromises);
|
||||
|
||||
const externalBookings = {
|
||||
bookingCom: results[0].status === 'fulfilled' ? results[0].value : [],
|
||||
tripCom: results[1].status === 'fulfilled' ? results[1].value : [],
|
||||
expedia: results[2].status === 'fulfilled' ? results[2].value : []
|
||||
};
|
||||
|
||||
// Combine all bookings
|
||||
const allBookings = [
|
||||
...externalBookings.bookingCom.map(b => ({ ...b, source: 'Booking.com' })),
|
||||
...externalBookings.tripCom.map(b => ({ ...b, source: 'Trip.com' })),
|
||||
...externalBookings.expedia.map(b => ({ ...b, source: 'Expedia' }))
|
||||
];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
bookings: allBookings,
|
||||
summary: {
|
||||
total: allBookings.length,
|
||||
byPlatform: {
|
||||
bookingCom: externalBookings.bookingCom.length,
|
||||
tripCom: externalBookings.tripCom.length,
|
||||
expedia: externalBookings.expedia.length
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('External bookings fetch failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch external bookings'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/integrations/webhook/booking-com
|
||||
// @desc Handle Booking.com webhooks
|
||||
// @access Public (webhook)
|
||||
router.post('/webhook/booking-com', async (req, res) => {
|
||||
try {
|
||||
const webhookData = req.body;
|
||||
|
||||
logger.integrationLog('Booking.com webhook received', {
|
||||
type: webhookData.event_type,
|
||||
bookingId: webhookData.booking_id
|
||||
});
|
||||
|
||||
// Process webhook based on event type
|
||||
switch (webhookData.event_type) {
|
||||
case 'booking_created':
|
||||
await bookingCom.processNewBooking(webhookData);
|
||||
break;
|
||||
case 'booking_modified':
|
||||
await bookingCom.processBookingModification(webhookData);
|
||||
break;
|
||||
case 'booking_cancelled':
|
||||
await bookingCom.processBookingCancellation(webhookData);
|
||||
break;
|
||||
default:
|
||||
logger.integrationLog('Unknown Booking.com webhook event', {
|
||||
type: webhookData.event_type
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Webhook processed successfully' });
|
||||
} catch (error) {
|
||||
logger.error('Booking.com webhook processing failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Webhook processing failed'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/integrations/webhook/expedia
|
||||
// @desc Handle Expedia webhooks
|
||||
// @access Public (webhook)
|
||||
router.post('/webhook/expedia', async (req, res) => {
|
||||
try {
|
||||
const webhookData = req.body;
|
||||
|
||||
logger.integrationLog('Expedia webhook received', {
|
||||
type: webhookData.event_type,
|
||||
bookingId: webhookData.booking_id
|
||||
});
|
||||
|
||||
// Process Expedia webhook
|
||||
await expedia.processWebhook(webhookData);
|
||||
|
||||
res.json({ success: true, message: 'Webhook processed successfully' });
|
||||
} catch (error) {
|
||||
logger.error('Expedia webhook processing failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Webhook processing failed'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/integrations/analytics/platform-performance
|
||||
// @desc Get performance analytics for all platforms
|
||||
// @access Private/Admin
|
||||
router.get('/analytics/platform-performance', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const start = new Date(startDate || Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const end = new Date(endDate || Date.now());
|
||||
|
||||
// Get performance data from all platforms
|
||||
const performancePromises = [
|
||||
bookingCom.getPerformanceData(start, end),
|
||||
tripCom.getPerformanceData(start, end),
|
||||
expedia.getPerformanceData(start, end)
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(performancePromises);
|
||||
|
||||
const performanceData = {
|
||||
bookingCom: results[0].status === 'fulfilled' ? results[0].value : null,
|
||||
tripCom: results[1].status === 'fulfilled' ? results[1].value : null,
|
||||
expedia: results[2].status === 'fulfilled' ? results[2].value : null
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
dateRange: { start, end },
|
||||
platforms: performanceData
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Platform performance analytics failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch platform performance data'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
350
routes/media.js
Normal file
350
routes/media.js
Normal file
@@ -0,0 +1,350 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const Media = require('../models/Media');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
|
||||
// Ensure upload directories exist
|
||||
const ensureUploadDir = async (dir) => {
|
||||
try {
|
||||
await fs.access(dir);
|
||||
} catch {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Configure multer storage
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
const folder = req.body.folder || 'general';
|
||||
const uploadPath = path.join(__dirname, '../../client/public/images', folder);
|
||||
await ensureUploadDir(uploadPath);
|
||||
cb(null, uploadPath);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
const basename = path.basename(file.originalname, ext)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
cb(null, basename + '-' + uniqueSuffix + ext);
|
||||
}
|
||||
});
|
||||
|
||||
// File filter
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowedMimeTypes = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
'video/mp4',
|
||||
'video/quicktime',
|
||||
'application/pdf'
|
||||
];
|
||||
|
||||
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only images, videos, and PDFs are allowed.'), false);
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/media/upload
|
||||
// @desc Upload media file
|
||||
// @access Private (Admin)
|
||||
router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No file uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
const { folder = 'general', alt, caption, description, tags } = req.body;
|
||||
|
||||
// Determine media type
|
||||
let mediaType = 'other';
|
||||
if (req.file.mimetype.startsWith('image/')) {
|
||||
mediaType = 'image';
|
||||
} else if (req.file.mimetype.startsWith('video/')) {
|
||||
mediaType = 'video';
|
||||
} else if (req.file.mimetype === 'application/pdf') {
|
||||
mediaType = 'document';
|
||||
}
|
||||
|
||||
// Create media record
|
||||
const media = new Media({
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
url: `/images/${folder}/${req.file.filename}`,
|
||||
mimeType: req.file.mimetype,
|
||||
size: req.file.size,
|
||||
type: mediaType,
|
||||
folder,
|
||||
alt,
|
||||
caption,
|
||||
description,
|
||||
tags: tags ? tags.split(',').map(t => t.trim()) : [],
|
||||
uploadedBy: req.admin.id
|
||||
});
|
||||
|
||||
await media.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'File uploaded successfully',
|
||||
data: { media }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || 'Error uploading file'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/media/upload-multiple
|
||||
// @desc Upload multiple media files
|
||||
// @access Private (Admin)
|
||||
router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req, res) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No files uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
const { folder = 'general' } = req.body;
|
||||
const mediaRecords = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
let mediaType = 'other';
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
mediaType = 'image';
|
||||
} else if (file.mimetype.startsWith('video/')) {
|
||||
mediaType = 'video';
|
||||
} else if (file.mimetype === 'application/pdf') {
|
||||
mediaType = 'document';
|
||||
}
|
||||
|
||||
const media = new Media({
|
||||
filename: file.filename,
|
||||
originalName: file.originalname,
|
||||
url: `/images/${folder}/${file.filename}`,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
type: mediaType,
|
||||
folder,
|
||||
uploadedBy: req.admin.id
|
||||
});
|
||||
|
||||
await media.save();
|
||||
mediaRecords.push(media);
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `${mediaRecords.length} files uploaded successfully`,
|
||||
data: { media: mediaRecords }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Multiple upload error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || 'Error uploading files'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/media
|
||||
// @desc Get all media
|
||||
// @access Private (Admin)
|
||||
router.get('/', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { folder, type, page = 1, limit = 50 } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const query = {};
|
||||
if (folder) query.folder = folder;
|
||||
if (type) query.type = type;
|
||||
|
||||
const media = await Media.find(query)
|
||||
.populate('uploadedBy', 'firstName lastName')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit));
|
||||
|
||||
const total = await Media.countDocuments(query);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
media,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get media error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching media'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/media/search
|
||||
// @desc Search media
|
||||
// @access Private (Admin)
|
||||
router.get('/search', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { q, folder, type, limit = 50 } = req.query;
|
||||
|
||||
if (!q) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Search query is required'
|
||||
});
|
||||
}
|
||||
|
||||
const media = await Media.search(q, { folder, type, limit: parseInt(limit) });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { media }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Search media error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error searching media'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/media/:id
|
||||
// @desc Update media metadata
|
||||
// @access Private (Admin)
|
||||
router.put('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { alt, caption, description, tags, folder } = req.body;
|
||||
|
||||
const updateFields = {};
|
||||
if (alt) updateFields.alt = alt;
|
||||
if (caption) updateFields.caption = caption;
|
||||
if (description) updateFields.description = description;
|
||||
if (tags) updateFields.tags = tags;
|
||||
if (folder) updateFields.folder = folder;
|
||||
|
||||
const media = await Media.findByIdAndUpdate(
|
||||
id,
|
||||
{ $set: updateFields },
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!media) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Media not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Media updated successfully',
|
||||
data: { media }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update media error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating media'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/media/:id
|
||||
// @desc Delete media file
|
||||
// @access Private (Admin)
|
||||
router.delete('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const media = await Media.findById(id);
|
||||
|
||||
if (!media) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Media not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Delete file from filesystem
|
||||
const filePath = path.join(__dirname, '../../client/public', media.url);
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (err) {
|
||||
console.error('Error deleting file:', err);
|
||||
}
|
||||
|
||||
// Delete media record
|
||||
await Media.findByIdAndDelete(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Media deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Delete media error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting media'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/media/folders/list
|
||||
// @desc Get list of all folders
|
||||
// @access Private (Admin)
|
||||
router.get('/folders/list', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const folders = await Media.distinct('folder');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { folders }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get folders error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching folders'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
11
routes/payments.js
Normal file
11
routes/payments.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Minimal payments router stub to satisfy app.use
|
||||
router.get('/health', (req, res) => {
|
||||
res.json({ success: true, service: 'payments', status: 'ok' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
226
routes/roomCategories.js
Normal file
226
routes/roomCategories.js
Normal file
@@ -0,0 +1,226 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const RoomCategory = require('../models/RoomCategory');
|
||||
const Room = require('../models/Room');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
|
||||
// @route GET /api/room-categories
|
||||
// @desc Get all active room categories
|
||||
// @access Public
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const categories = await RoomCategory.find({ isActive: true })
|
||||
.sort({ displayOrder: 1, name: 1 })
|
||||
.lean();
|
||||
|
||||
// Calculate room count and price range for each category
|
||||
const categoriesWithStats = await Promise.all(
|
||||
categories.map(async (category) => {
|
||||
const rooms = await Room.find({
|
||||
category: category._id,
|
||||
isActive: true
|
||||
}).lean();
|
||||
|
||||
const prices = rooms.map(r => r.basePrice).filter(p => p > 0);
|
||||
|
||||
return {
|
||||
...category,
|
||||
roomCount: rooms.length,
|
||||
priceRange: {
|
||||
min: prices.length > 0 ? Math.min(...prices) : 0,
|
||||
max: prices.length > 0 ? Math.max(...prices) : 0
|
||||
},
|
||||
primaryImage: category.images.find(img => img.isPrimary)?.url ||
|
||||
(category.images.length > 0 ? category.images[0].url : null),
|
||||
imageCount: category.images.length
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { categories: categoriesWithStats }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching room categories:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/room-categories/:slug
|
||||
// @desc Get single room category with all images and rooms
|
||||
// @access Public
|
||||
router.get('/:slug', async (req, res) => {
|
||||
try {
|
||||
const category = await RoomCategory.findOne({
|
||||
slug: req.params.slug,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room category not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Get all rooms in this category
|
||||
const rooms = await Room.find({
|
||||
category: category._id,
|
||||
isActive: true
|
||||
})
|
||||
.sort({ basePrice: 1 })
|
||||
.lean();
|
||||
|
||||
// Calculate price range
|
||||
const prices = rooms.map(r => r.basePrice).filter(p => p > 0);
|
||||
const priceRange = {
|
||||
min: prices.length > 0 ? Math.min(...prices) : 0,
|
||||
max: prices.length > 0 ? Math.max(...prices) : 0
|
||||
};
|
||||
|
||||
const categoryData = category.toObject();
|
||||
categoryData.rooms = rooms;
|
||||
categoryData.roomCount = rooms.length;
|
||||
categoryData.priceRange = priceRange;
|
||||
categoryData.primaryImage = category.primaryImage;
|
||||
categoryData.imageCount = category.images.length;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { category: categoryData }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching room category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room category'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== ADMIN ROUTES ====================
|
||||
|
||||
// @route GET /api/room-categories/admin/all
|
||||
// @desc Get all room categories (including inactive) - Admin only
|
||||
// @access Private/Admin
|
||||
router.get('/admin/all', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const categories = await RoomCategory.find()
|
||||
.sort({ displayOrder: 1, name: 1 });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { categories }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching all room categories:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/room-categories
|
||||
// @desc Create a new room category - Admin only
|
||||
// @access Private/Admin
|
||||
router.post('/', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const category = new RoomCategory(req.body);
|
||||
await category.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Room category created successfully',
|
||||
data: { category }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating room category:', error);
|
||||
if (error.code === 11000) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category with this name or slug already exists'
|
||||
});
|
||||
}
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while creating room category'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/room-categories/:id
|
||||
// @desc Update a room category - Admin only
|
||||
// @access Private/Admin
|
||||
router.put('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const category = await RoomCategory.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
req.body,
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room category not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Room category updated successfully',
|
||||
data: { category }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating room category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while updating room category'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/room-categories/:id
|
||||
// @desc Delete a room category - Admin only
|
||||
// @access Private/Admin
|
||||
router.delete('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
// Check if any rooms are using this category
|
||||
const roomsCount = await Room.countDocuments({ category: req.params.id });
|
||||
|
||||
if (roomsCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Cannot delete category. ${roomsCount} room(s) are using this category. Please reassign rooms first.`
|
||||
});
|
||||
}
|
||||
|
||||
const category = await RoomCategory.findByIdAndDelete(req.params.id);
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room category not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Room category deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting room category:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while deleting room category'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
521
routes/rooms.js
Normal file
521
routes/rooms.js
Normal file
@@ -0,0 +1,521 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Room = require('../models/Room');
|
||||
const RoomCategory = require('../models/RoomCategory');
|
||||
const { body, validationResult, query } = require('express-validator');
|
||||
const auth = require('../middleware/auth');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
|
||||
// @route GET /api/rooms
|
||||
// @desc Get all rooms with filtering and pagination
|
||||
// @access Public
|
||||
router.get('/', [
|
||||
query('page').optional().isInt({ min: 1 }),
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }),
|
||||
query('type').optional().isIn(['Standard', 'Deluxe', 'Suite', 'Executive Suite', 'Presidential Suite']),
|
||||
query('maxPrice').optional().isFloat({ min: 0 }),
|
||||
query('minPrice').optional().isFloat({ min: 0 }),
|
||||
query('guests').optional().isInt({ min: 1 }),
|
||||
query('checkIn').optional().isISO8601(),
|
||||
query('checkOut').optional().isISO8601(),
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
limit = 12,
|
||||
type,
|
||||
category, // category slug or ID
|
||||
maxPrice,
|
||||
minPrice,
|
||||
guests,
|
||||
checkIn,
|
||||
checkOut,
|
||||
sortBy = 'name',
|
||||
sortOrder = 'asc'
|
||||
} = req.query;
|
||||
|
||||
// Build filter object
|
||||
let filter = {
|
||||
isActive: true,
|
||||
status: 'Available'
|
||||
};
|
||||
|
||||
if (type) filter.type = type;
|
||||
if (guests) filter.maxOccupancy = { $gte: parseInt(guests) };
|
||||
|
||||
// Handle category filtering
|
||||
if (category) {
|
||||
// Try to find category by slug first, then by ID
|
||||
const categoryDoc = await RoomCategory.findOne({
|
||||
$or: [
|
||||
{ slug: category },
|
||||
{ _id: category }
|
||||
]
|
||||
});
|
||||
|
||||
if (categoryDoc) {
|
||||
filter.category = categoryDoc._id;
|
||||
} else {
|
||||
// If category not found, return empty results
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
rooms: [],
|
||||
pagination: {
|
||||
currentPage: parseInt(page),
|
||||
totalPages: 0,
|
||||
totalCount: 0,
|
||||
hasNextPage: false,
|
||||
hasPrevPage: false
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Price filtering (using virtual currentPrice would require aggregation)
|
||||
if (minPrice || maxPrice) {
|
||||
filter.basePrice = {};
|
||||
if (minPrice) filter.basePrice.$gte = parseFloat(minPrice);
|
||||
if (maxPrice) filter.basePrice.$lte = parseFloat(maxPrice);
|
||||
}
|
||||
|
||||
// If dates are provided, find available rooms
|
||||
let roomQuery;
|
||||
if (checkIn && checkOut) {
|
||||
const checkInDate = new Date(checkIn);
|
||||
const checkOutDate = new Date(checkOut);
|
||||
|
||||
if (checkInDate >= checkOutDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Check-out date must be after check-in date'
|
||||
});
|
||||
}
|
||||
|
||||
roomQuery = Room.findAvailable(checkInDate, checkOutDate, guests ? parseInt(guests) : 1);
|
||||
} else {
|
||||
roomQuery = Room.find(filter);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
const sortOptions = {};
|
||||
sortOptions[sortBy] = sortOrder === 'desc' ? -1 : 1;
|
||||
|
||||
// Execute query with pagination
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [rooms, totalCount] = await Promise.all([
|
||||
roomQuery
|
||||
.sort(sortOptions)
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
|
||||
checkIn && checkOut ?
|
||||
Room.findAvailable(new Date(checkIn), new Date(checkOut), guests ? parseInt(guests) : 1).countDocuments() :
|
||||
Room.countDocuments(filter)
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(totalCount / parseInt(limit));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
rooms,
|
||||
pagination: {
|
||||
currentPage: parseInt(page),
|
||||
totalPages,
|
||||
totalCount,
|
||||
hasNextPage: parseInt(page) < totalPages,
|
||||
hasPrevPage: parseInt(page) > 1
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching rooms:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching rooms'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/rooms/:id
|
||||
// @desc Get single room by ID
|
||||
// @access Public
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findById(req.params.id);
|
||||
|
||||
if (!room || !room.isActive) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: room
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching room:', error);
|
||||
|
||||
if (error.name === 'CastError') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid room ID format'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/rooms/slug/:slug
|
||||
// @desc Get single room by slug
|
||||
// @access Public
|
||||
router.get('/slug/:slug', async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findOne({
|
||||
slug: req.params.slug,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: room
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching room by slug:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/rooms/:id/availability
|
||||
// @desc Check room availability for specific dates
|
||||
// @access Public
|
||||
router.post('/:id/availability', [
|
||||
body('checkIn').isISO8601().withMessage('Valid check-in date is required'),
|
||||
body('checkOut').isISO8601().withMessage('Valid check-out date is required'),
|
||||
body('guests').optional().isInt({ min: 1 }).withMessage('Number of guests must be at least 1')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { checkIn, checkOut, guests = 1 } = req.body;
|
||||
const checkInDate = new Date(checkIn);
|
||||
const checkOutDate = new Date(checkOut);
|
||||
|
||||
if (checkInDate >= checkOutDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Check-out date must be after check-in date'
|
||||
});
|
||||
}
|
||||
|
||||
if (checkInDate < new Date()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Check-in date cannot be in the past'
|
||||
});
|
||||
}
|
||||
|
||||
const room = await Room.findById(req.params.id);
|
||||
|
||||
if (!room || !room.isActive) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (room.maxOccupancy < guests) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Room can accommodate maximum ${room.maxOccupancy} guests`
|
||||
});
|
||||
}
|
||||
|
||||
const isAvailable = await room.isAvailable(checkInDate, checkOutDate);
|
||||
|
||||
// Calculate pricing
|
||||
const numberOfNights = Math.ceil((checkOutDate - checkInDate) / (1000 * 60 * 60 * 24));
|
||||
const roomRate = room.currentPrice;
|
||||
const subtotal = roomRate * numberOfNights;
|
||||
const taxes = subtotal * 0.12; // 12% tax
|
||||
const total = subtotal + taxes;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
available: isAvailable,
|
||||
room: {
|
||||
id: room._id,
|
||||
name: room.name,
|
||||
type: room.type,
|
||||
maxOccupancy: room.maxOccupancy
|
||||
},
|
||||
pricing: {
|
||||
roomRate,
|
||||
numberOfNights,
|
||||
subtotal,
|
||||
taxes,
|
||||
total
|
||||
},
|
||||
dates: {
|
||||
checkIn: checkInDate,
|
||||
checkOut: checkOutDate
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking availability:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while checking availability'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/rooms/types/available
|
||||
// @desc Get available room types with counts
|
||||
// @access Public
|
||||
router.get('/types/available', [
|
||||
query('checkIn').optional().isISO8601(),
|
||||
query('checkOut').optional().isISO8601(),
|
||||
query('guests').optional().isInt({ min: 1 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const { checkIn, checkOut, guests = 1 } = req.query;
|
||||
|
||||
let aggregationPipeline = [
|
||||
{
|
||||
$match: {
|
||||
isActive: true,
|
||||
status: 'Available',
|
||||
maxOccupancy: { $gte: parseInt(guests) }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// If dates provided, filter by availability
|
||||
if (checkIn && checkOut) {
|
||||
const checkInDate = new Date(checkIn);
|
||||
const checkOutDate = new Date(checkOut);
|
||||
|
||||
aggregationPipeline.push(
|
||||
{
|
||||
$lookup: {
|
||||
from: 'bookings',
|
||||
let: { roomId: '$_id' },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ['$room', '$$roomId'] },
|
||||
status: { $in: ['Confirmed', 'Checked In'] },
|
||||
$or: [
|
||||
{
|
||||
checkInDate: { $lt: checkOutDate },
|
||||
checkOutDate: { $gt: checkInDate }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'conflictingBookings'
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
conflictingBookings: { $size: 0 }
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
aggregationPipeline.push(
|
||||
{
|
||||
$group: {
|
||||
_id: '$type',
|
||||
count: { $sum: 1 },
|
||||
minPrice: { $min: '$basePrice' },
|
||||
maxPrice: { $max: '$basePrice' },
|
||||
avgPrice: { $avg: '$basePrice' },
|
||||
rooms: {
|
||||
$push: {
|
||||
id: '$_id',
|
||||
name: '$name',
|
||||
price: '$basePrice',
|
||||
amenities: '$amenities',
|
||||
images: { $slice: ['$images', 1] }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { minPrice: 1 }
|
||||
}
|
||||
);
|
||||
|
||||
const roomTypes = await Room.aggregate(aggregationPipeline);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: roomTypes
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching room types:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room types'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Protected routes below (require authentication)
|
||||
|
||||
// @route POST /api/rooms
|
||||
// @desc Create a new room (Admin only)
|
||||
// @access Private/Admin
|
||||
router.post('/', adminAuth, [
|
||||
body('name').notEmpty().withMessage('Room name is required'),
|
||||
body('type').isIn(['Standard', 'Deluxe', 'Suite', 'Executive Suite', 'Presidential Suite']),
|
||||
body('description').notEmpty().withMessage('Description is required'),
|
||||
body('roomNumber').notEmpty().withMessage('Room number is required'),
|
||||
body('floor').isInt({ min: 1 }).withMessage('Floor must be a positive integer'),
|
||||
body('size').isFloat({ min: 1 }).withMessage('Size must be a positive number'),
|
||||
body('maxOccupancy').isInt({ min: 1, max: 8 }).withMessage('Max occupancy must be between 1 and 8'),
|
||||
body('basePrice').isFloat({ min: 0 }).withMessage('Base price must be a positive number'),
|
||||
body('bedType').isIn(['Single', 'Double', 'Queen', 'King', 'Twin', 'Sofa Bed']),
|
||||
body('bedCount').isInt({ min: 1 }).withMessage('Bed count must be at least 1')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
// Check if room number already exists
|
||||
const existingRoom = await Room.findOne({ roomNumber: req.body.roomNumber });
|
||||
if (existingRoom) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Room number already exists'
|
||||
});
|
||||
}
|
||||
|
||||
const room = new Room(req.body);
|
||||
await room.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Room created successfully',
|
||||
data: room
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating room:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while creating room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/rooms/:id
|
||||
// @desc Update room (Admin only)
|
||||
// @access Private/Admin
|
||||
router.put('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
req.body,
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!room) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Room updated successfully',
|
||||
data: room
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating room:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while updating room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/rooms/:id
|
||||
// @desc Delete room (Admin only)
|
||||
// @access Private/Admin
|
||||
router.delete('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
{ isActive: false },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!room) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Room deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting room:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while deleting room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
208
routes/settings.js
Normal file
208
routes/settings.js
Normal file
@@ -0,0 +1,208 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const SiteSettings = require('../models/SiteSettings');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
|
||||
// @route GET /api/settings
|
||||
// @desc Get site settings
|
||||
// @access Public
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.getSiteSettings();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { settings }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get settings error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching settings'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/settings
|
||||
// @desc Update site settings
|
||||
// @access Private (Admin)
|
||||
router.put('/', adminAuth, async (req, res) => {
|
||||
try {
|
||||
let settings = await SiteSettings.findOne();
|
||||
|
||||
if (!settings) {
|
||||
settings = new SiteSettings(req.body);
|
||||
} else {
|
||||
// Update all provided fields
|
||||
Object.keys(req.body).forEach(key => {
|
||||
if (key !== '_id' && key !== '__v') {
|
||||
if (typeof req.body[key] === 'object' && !Array.isArray(req.body[key]) && req.body[key] !== null) {
|
||||
// Deep merge for nested objects
|
||||
settings[key] = { ...settings[key], ...req.body[key] };
|
||||
} else {
|
||||
settings[key] = req.body[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (req.admin && req.admin.id) {
|
||||
settings.lastModifiedBy = req.admin.id;
|
||||
}
|
||||
await settings.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Settings updated successfully',
|
||||
data: { settings }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update settings error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating settings'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/settings/public
|
||||
// @desc Get public settings (subset for frontend)
|
||||
// @access Public
|
||||
router.get('/public', async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.getSiteSettings();
|
||||
|
||||
// Return only public-facing settings
|
||||
const publicSettings = {
|
||||
hotel: {
|
||||
name: settings.hotel.name,
|
||||
tagline: settings.hotel.tagline,
|
||||
phone: settings.hotel.phone,
|
||||
email: settings.hotel.email,
|
||||
whatsapp: settings.hotel.whatsapp,
|
||||
address: settings.hotel.address,
|
||||
socialMedia: settings.hotel.socialMedia,
|
||||
businessHours: settings.hotel.businessHours
|
||||
},
|
||||
theme: settings.theme,
|
||||
features: settings.features,
|
||||
languages: settings.languages,
|
||||
booking: {
|
||||
enabled: settings.booking.enabled,
|
||||
minNights: settings.booking.minNights,
|
||||
maxNights: settings.booking.maxNights,
|
||||
currency: settings.booking.currency,
|
||||
currencySymbol: settings.booking.currencySymbol
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { settings: publicSettings }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get public settings error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching settings'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/settings/hotel
|
||||
// @desc Update hotel information
|
||||
// @access Private (Admin)
|
||||
router.put('/hotel', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.getSiteSettings();
|
||||
|
||||
settings.hotel = { ...settings.hotel, ...req.body };
|
||||
settings.lastModifiedBy = req.admin.id;
|
||||
await settings.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Hotel information updated successfully',
|
||||
data: { hotel: settings.hotel }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update hotel info error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating hotel information'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/settings/theme
|
||||
// @desc Update theme settings
|
||||
// @access Private (Admin)
|
||||
router.put('/theme', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.getSiteSettings();
|
||||
|
||||
settings.theme = { ...settings.theme, ...req.body };
|
||||
if (req.body.colors) {
|
||||
settings.theme.colors = { ...settings.theme.colors, ...req.body.colors };
|
||||
}
|
||||
if (req.body.fonts) {
|
||||
settings.theme.fonts = { ...settings.theme.fonts, ...req.body.fonts };
|
||||
}
|
||||
if (req.body.layout) {
|
||||
settings.theme.layout = { ...settings.theme.layout, ...req.body.layout };
|
||||
}
|
||||
|
||||
settings.lastModifiedBy = req.admin.id;
|
||||
await settings.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Theme updated successfully',
|
||||
data: { theme: settings.theme }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update theme error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating theme'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/settings/maintenance
|
||||
// @desc Toggle maintenance mode
|
||||
// @access Private (Admin)
|
||||
router.put('/maintenance', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.getSiteSettings();
|
||||
const { enabled, message, allowedIPs } = req.body;
|
||||
|
||||
if (typeof enabled !== 'undefined') {
|
||||
settings.maintenance.enabled = enabled;
|
||||
}
|
||||
if (message) {
|
||||
settings.maintenance.message = message;
|
||||
}
|
||||
if (allowedIPs) {
|
||||
settings.maintenance.allowedIPs = allowedIPs;
|
||||
}
|
||||
|
||||
settings.lastModifiedBy = req.admin.id;
|
||||
await settings.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Maintenance mode updated successfully',
|
||||
data: { maintenance: settings.maintenance }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update maintenance mode error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating maintenance mode'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
188
routes/upload.js
Normal file
188
routes/upload.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const sharp = require('sharp');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
|
||||
// Ensure uploads directory exists
|
||||
const uploadsDir = path.join(__dirname, '../../client/public/uploads');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Configure multer for file upload
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadsDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// Generate unique filename
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
const name = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
||||
cb(null, `${name}-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileFilter = (req, file, cb) => {
|
||||
// Accept images only
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only image files are allowed'), false);
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/upload
|
||||
// @desc Upload single or multiple images
|
||||
// @access Private (Admin)
|
||||
router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No files uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
// Process uploaded files
|
||||
const uploadedFiles = await Promise.all(
|
||||
req.files.map(async (file) => {
|
||||
try {
|
||||
// Optimize image with sharp
|
||||
const optimizedPath = path.join(uploadsDir, `optimized-${file.filename}`);
|
||||
|
||||
await sharp(file.path)
|
||||
.resize(1920, 1080, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.jpeg({ quality: 85 })
|
||||
.toFile(optimizedPath);
|
||||
|
||||
// Replace original with optimized
|
||||
fs.unlinkSync(file.path);
|
||||
fs.renameSync(optimizedPath, file.path);
|
||||
|
||||
return {
|
||||
filename: file.filename,
|
||||
originalName: file.originalname,
|
||||
url: `/uploads/${file.filename}`,
|
||||
size: file.size,
|
||||
mimeType: file.mimetype,
|
||||
uploadedAt: new Date()
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error);
|
||||
return {
|
||||
filename: file.filename,
|
||||
originalName: file.originalname,
|
||||
url: `/uploads/${file.filename}`,
|
||||
size: file.size,
|
||||
mimeType: file.mimetype,
|
||||
uploadedAt: new Date(),
|
||||
error: 'Optimization failed'
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Files uploaded successfully',
|
||||
data: { files: uploadedFiles }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || 'Error uploading files'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/upload/list
|
||||
// @desc Get list of uploaded files
|
||||
// @access Private (Admin)
|
||||
router.get('/list', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const files = fs.readdirSync(uploadsDir);
|
||||
|
||||
const fileList = files
|
||||
.filter(file => !file.startsWith('.')) // Ignore hidden files
|
||||
.map(filename => {
|
||||
const filePath = path.join(uploadsDir, filename);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
return {
|
||||
filename,
|
||||
url: `/uploads/${filename}`,
|
||||
size: stats.size,
|
||||
uploadedAt: stats.mtime
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.uploadedAt - a.uploadedAt); // Sort by newest first
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { files: fileList, total: fileList.length }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('List files error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error listing files'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/upload/:filename
|
||||
// @desc Delete an uploaded file
|
||||
// @access Private (Admin)
|
||||
router.delete('/:filename', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const filePath = path.join(uploadsDir, filename);
|
||||
|
||||
// Security check: ensure filename doesn't contain path traversal
|
||||
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid filename'
|
||||
});
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'File not found'
|
||||
});
|
||||
}
|
||||
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'File deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Delete file error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting file'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user