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:
509
routes/auth.js
Normal file
509
routes/auth.js
Normal file
@@ -0,0 +1,509 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Guest = require('../models/Guest');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const auth = require('../middleware/auth');
|
||||
const sendEmail = require('../utils/sendEmail');
|
||||
const logger = require('../utils/logger');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Generate JWT token
|
||||
const generateToken = (payload) => {
|
||||
return jwt.sign(payload, process.env.JWT_SECRET, {
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
|
||||
});
|
||||
};
|
||||
|
||||
// @route POST /api/auth/register
|
||||
// @desc Register a new guest
|
||||
// @access Public
|
||||
router.post('/register', [
|
||||
body('firstName').notEmpty().withMessage('First name is required').trim(),
|
||||
body('lastName').notEmpty().withMessage('Last name is required').trim(),
|
||||
body('email').isEmail().withMessage('Valid email is required').normalizeEmail(),
|
||||
body('phone').notEmpty().withMessage('Phone number is required').trim(),
|
||||
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
|
||||
body('confirmPassword').custom((value, { req }) => {
|
||||
if (value !== req.body.password) {
|
||||
throw new Error('Password confirmation does not match password');
|
||||
}
|
||||
return value;
|
||||
})
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { firstName, lastName, email, phone, password } = req.body;
|
||||
|
||||
// Check if guest already exists
|
||||
let guest = await Guest.findOne({ email });
|
||||
if (guest && guest.isRegistered) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Guest already registered with this email'
|
||||
});
|
||||
}
|
||||
|
||||
// Create or update guest
|
||||
if (guest) {
|
||||
// Update existing guest profile
|
||||
guest.firstName = firstName;
|
||||
guest.lastName = lastName;
|
||||
guest.phone = phone;
|
||||
guest.password = password;
|
||||
guest.isRegistered = true;
|
||||
guest.emailVerified = false;
|
||||
} else {
|
||||
// Create new guest
|
||||
guest = new Guest({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
phone,
|
||||
password,
|
||||
isRegistered: true,
|
||||
emailVerified: false
|
||||
});
|
||||
}
|
||||
|
||||
await guest.save();
|
||||
|
||||
// Generate email verification token
|
||||
const verificationToken = crypto.randomBytes(32).toString('hex');
|
||||
guest.emailVerificationToken = verificationToken;
|
||||
await guest.save();
|
||||
|
||||
// Send verification email
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Verify Your Email - The Old Vine Hotel',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
<h2>Email Verification</h2>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<p>Dear ${firstName} ${lastName},</p>
|
||||
|
||||
<p>Thank you for registering with The Old Vine Hotel! Please verify your email address to complete your registration.</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${process.env.CLIENT_URL}/verify-email?token=${verificationToken}"
|
||||
style="background: #D4AF37; color: black; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">
|
||||
Verify Email Address
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>If the button doesn't work, copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666;">${process.env.CLIENT_URL}/verify-email?token=${verificationToken}</p>
|
||||
|
||||
<p>This verification link will expire in 24 hours.</p>
|
||||
|
||||
<p>If you didn't create an account with us, please ignore this email.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Old Vine Hotel Team</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f4f4f4; padding: 20px; text-align: center; font-size: 12px;">
|
||||
<p>© 2025 The Old Vine Hotel. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.error('Failed to send verification email:', emailError);
|
||||
// Don't fail registration if email fails
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken({
|
||||
id: guest._id,
|
||||
email: guest.email,
|
||||
isAdmin: false
|
||||
});
|
||||
|
||||
logger.info('Guest registered successfully', {
|
||||
guestId: guest._id,
|
||||
email: guest.email
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Registration successful. Please check your email to verify your account.',
|
||||
data: {
|
||||
token,
|
||||
guest: {
|
||||
id: guest._id,
|
||||
firstName: guest.firstName,
|
||||
lastName: guest.lastName,
|
||||
email: guest.email,
|
||||
phone: guest.phone,
|
||||
emailVerified: guest.emailVerified,
|
||||
loyaltyProgram: guest.loyaltyProgram
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Registration error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during registration'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/auth/login
|
||||
// @desc Login guest
|
||||
// @access Public
|
||||
router.post('/login', [
|
||||
body('email').isEmail().withMessage('Valid email is required').normalizeEmail(),
|
||||
body('password').notEmpty().withMessage('Password is required')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Find guest with password field
|
||||
const guest = await Guest.findOne({ email, isRegistered: true }).select('+password');
|
||||
|
||||
if (!guest) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid email or password'
|
||||
});
|
||||
}
|
||||
|
||||
if (!guest.isActive) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Account is deactivated. Please contact support.'
|
||||
});
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isMatch = await guest.comparePassword(password);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid email or password'
|
||||
});
|
||||
}
|
||||
|
||||
// Update last login
|
||||
guest.lastLogin = new Date();
|
||||
guest.lastActivity = new Date();
|
||||
await guest.save();
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken({
|
||||
id: guest._id,
|
||||
email: guest.email,
|
||||
isAdmin: false
|
||||
});
|
||||
|
||||
logger.info('Guest login successful', {
|
||||
guestId: guest._id,
|
||||
email: guest.email
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
data: {
|
||||
token,
|
||||
guest: {
|
||||
id: guest._id,
|
||||
firstName: guest.firstName,
|
||||
lastName: guest.lastName,
|
||||
email: guest.email,
|
||||
phone: guest.phone,
|
||||
emailVerified: guest.emailVerified,
|
||||
loyaltyProgram: guest.loyaltyProgram,
|
||||
preferences: guest.preferences,
|
||||
isVIP: guest.isVIP
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during login'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/auth/me
|
||||
// @desc Get current guest
|
||||
// @access Private
|
||||
router.get('/me', auth, async (req, res) => {
|
||||
try {
|
||||
// Update last activity
|
||||
req.guest.lastActivity = new Date();
|
||||
await req.guest.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
guest: {
|
||||
id: req.guest._id,
|
||||
firstName: req.guest.firstName,
|
||||
lastName: req.guest.lastName,
|
||||
email: req.guest.email,
|
||||
phone: req.guest.phone,
|
||||
emailVerified: req.guest.emailVerified,
|
||||
loyaltyProgram: req.guest.loyaltyProgram,
|
||||
preferences: req.guest.preferences,
|
||||
isVIP: req.guest.isVIP,
|
||||
totalStays: req.guest.totalStays,
|
||||
totalSpent: req.guest.totalSpent
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get current guest error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/auth/forgot-password
|
||||
// @desc Send password reset email
|
||||
// @access Public
|
||||
router.post('/forgot-password', [
|
||||
body('email').isEmail().withMessage('Valid email is required').normalizeEmail()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { email } = req.body;
|
||||
|
||||
const guest = await Guest.findOne({ email, isRegistered: true });
|
||||
if (!guest) {
|
||||
// Don't reveal if email exists
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'If an account with that email exists, we have sent a password reset link.'
|
||||
});
|
||||
}
|
||||
|
||||
// Generate reset token
|
||||
const resetToken = guest.createPasswordResetToken();
|
||||
await guest.save();
|
||||
|
||||
// Send reset email
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Password Reset - The Old Vine Hotel',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
<h2>Password Reset</h2>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<p>Dear ${guest.firstName} ${guest.lastName},</p>
|
||||
|
||||
<p>You have requested to reset your password. Click the button below to reset it:</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${process.env.CLIENT_URL}/reset-password?token=${resetToken}"
|
||||
style="background: #D4AF37; color: black; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">
|
||||
Reset Password
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>If the button doesn't work, copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666;">${process.env.CLIENT_URL}/reset-password?token=${resetToken}</p>
|
||||
|
||||
<p>This reset link will expire in 10 minutes.</p>
|
||||
|
||||
<p>If you didn't request this password reset, please ignore this email.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Old Vine Hotel Team</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f4f4f4; padding: 20px; text-align: center; font-size: 12px;">
|
||||
<p>© 2025 The Old Vine Hotel. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.error('Failed to send password reset email:', emailError);
|
||||
guest.passwordResetToken = undefined;
|
||||
guest.passwordResetExpires = undefined;
|
||||
await guest.save();
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error sending password reset email'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'If an account with that email exists, we have sent a password reset link.'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Forgot password error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/auth/reset-password
|
||||
// @desc Reset password with token
|
||||
// @access Public
|
||||
router.post('/reset-password', [
|
||||
body('token').notEmpty().withMessage('Reset token is required'),
|
||||
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
|
||||
body('confirmPassword').custom((value, { req }) => {
|
||||
if (value !== req.body.password) {
|
||||
throw new Error('Password confirmation does not match password');
|
||||
}
|
||||
return value;
|
||||
})
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { token, password } = req.body;
|
||||
|
||||
// Hash the token
|
||||
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
|
||||
|
||||
// Find guest by token and check if token is still valid
|
||||
const guest = await Guest.findOne({
|
||||
passwordResetToken: hashedToken,
|
||||
passwordResetExpires: { $gt: Date.now() },
|
||||
isRegistered: true
|
||||
});
|
||||
|
||||
if (!guest) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Token is invalid or has expired'
|
||||
});
|
||||
}
|
||||
|
||||
// Set new password
|
||||
guest.password = password;
|
||||
guest.passwordResetToken = undefined;
|
||||
guest.passwordResetExpires = undefined;
|
||||
await guest.save();
|
||||
|
||||
logger.info('Password reset successful', {
|
||||
guestId: guest._id,
|
||||
email: guest.email
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Password reset successful. You can now log in with your new password.'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Reset password error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during password reset'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/auth/verify-email
|
||||
// @desc Verify email with token
|
||||
// @access Public
|
||||
router.post('/verify-email', [
|
||||
body('token').notEmpty().withMessage('Verification token is required')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { token } = req.body;
|
||||
|
||||
const guest = await Guest.findOne({
|
||||
emailVerificationToken: token,
|
||||
isRegistered: true
|
||||
});
|
||||
|
||||
if (!guest) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid verification token'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify email
|
||||
guest.emailVerified = true;
|
||||
guest.emailVerificationToken = undefined;
|
||||
await guest.save();
|
||||
|
||||
logger.info('Email verification successful', {
|
||||
guestId: guest._id,
|
||||
email: guest.email
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Email verified successfully!'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Email verification error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during email verification'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user