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:
Talal Sharabi
2026-01-06 12:21:56 +04:00
commit a3308a26e2
48 changed files with 15294 additions and 0 deletions

379
routes/admin.js Normal file
View 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
View 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>&copy; 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>&copy; 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
View 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
View 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
View 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>&copy; 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>&copy; 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;