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:
132
utils/logger.js
Normal file
132
utils/logger.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
// Define custom log format
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss'
|
||||
}),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
const fs = require('fs');
|
||||
const logDir = 'logs';
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir);
|
||||
}
|
||||
|
||||
// Create Winston logger
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: logFormat,
|
||||
defaultMeta: { service: 'old-vine-hotel-api' },
|
||||
transports: [
|
||||
// Write all logs with level 'error' and below to 'error.log'
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'error.log'),
|
||||
level: 'error',
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5,
|
||||
}),
|
||||
|
||||
// Write all logs with level 'info' and below to 'combined.log'
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'combined.log'),
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5,
|
||||
}),
|
||||
|
||||
// Write only booking-related logs
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'bookings.log'),
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json(),
|
||||
winston.format((info) => {
|
||||
return info.type === 'booking' ? info : false;
|
||||
})()
|
||||
),
|
||||
}),
|
||||
|
||||
// Write only payment-related logs
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'payments.log'),
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json(),
|
||||
winston.format((info) => {
|
||||
return info.type === 'payment' ? info : false;
|
||||
})()
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// If we're not in production, log to the console as well
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple(),
|
||||
winston.format.printf(info => {
|
||||
return `${info.timestamp} [${info.level}]: ${info.message} ${info.stack ? '\n' + info.stack : ''}`;
|
||||
})
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
// Custom logging methods for specific contexts
|
||||
logger.bookingLog = (message, meta = {}) => {
|
||||
logger.info(message, { ...meta, type: 'booking' });
|
||||
};
|
||||
|
||||
logger.paymentLog = (message, meta = {}) => {
|
||||
logger.info(message, { ...meta, type: 'payment' });
|
||||
};
|
||||
|
||||
logger.integrationLog = (message, meta = {}) => {
|
||||
logger.info(message, { ...meta, type: 'integration' });
|
||||
};
|
||||
|
||||
logger.securityLog = (message, meta = {}) => {
|
||||
logger.warn(message, { ...meta, type: 'security' });
|
||||
};
|
||||
|
||||
// Error logging with context
|
||||
logger.logError = (error, context = {}) => {
|
||||
logger.error(error.message, {
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
},
|
||||
context
|
||||
});
|
||||
};
|
||||
|
||||
// Request logging middleware
|
||||
logger.requestLogger = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
|
||||
logger.info('HTTP Request', {
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
statusCode: res.statusCode,
|
||||
duration: `${duration}ms`,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip || req.connection.remoteAddress,
|
||||
type: 'http'
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = logger;
|
||||
284
utils/sendEmail.js
Normal file
284
utils/sendEmail.js
Normal file
@@ -0,0 +1,284 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const logger = require('./logger');
|
||||
|
||||
// Create transporter
|
||||
const createTransporter = () => {
|
||||
return nodemailer.createTransporter({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT,
|
||||
secure: false, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Email templates
|
||||
const generateBookingConfirmationHTML = (context) => {
|
||||
const { guest, booking, room } = context;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.header { background: #8B4513; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; }
|
||||
.booking-details { background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||
.footer { background: #f4f4f4; padding: 20px; text-align: center; font-size: 12px; }
|
||||
.btn { background: #D4AF37; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
<h2>Booking Confirmation</h2>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Dear ${guest.firstName} ${guest.lastName},</p>
|
||||
|
||||
<p>Thank you for choosing The Old Vine Hotel! We're delighted to confirm your reservation.</p>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Booking Details</h3>
|
||||
<p><strong>Booking Number:</strong> ${booking.bookingNumber}</p>
|
||||
<p><strong>Confirmation Code:</strong> ${booking.confirmationCode}</p>
|
||||
<p><strong>Room:</strong> ${room.name} (${room.type})</p>
|
||||
<p><strong>Check-in:</strong> ${booking.checkInDate.toLocaleDateString()} (3:00 PM)</p>
|
||||
<p><strong>Check-out:</strong> ${booking.checkOutDate.toLocaleDateString()} (11:00 AM)</p>
|
||||
<p><strong>Guests:</strong> ${booking.numberOfGuests.adults} Adult(s)${booking.numberOfGuests.children ? `, ${booking.numberOfGuests.children} Child(ren)` : ''}</p>
|
||||
<p><strong>Nights:</strong> ${booking.numberOfNights}</p>
|
||||
<p><strong>Total Amount:</strong> $${booking.totalAmount.toFixed(2)}</p>
|
||||
</div>
|
||||
|
||||
${booking.specialRequests ? `
|
||||
<div class="booking-details">
|
||||
<h3>Special Requests</h3>
|
||||
<p>${booking.specialRequests}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<h3>What to Expect</h3>
|
||||
<ul>
|
||||
<li>Luxury accommodations with premium amenities</li>
|
||||
<li>24/7 concierge service</li>
|
||||
<li>Complimentary WiFi throughout the hotel</li>
|
||||
<li>Fine dining restaurant and bar</li>
|
||||
<li>Spa and fitness center access</li>
|
||||
</ul>
|
||||
|
||||
<h3>Hotel Information</h3>
|
||||
<p>
|
||||
<strong>Address:</strong> 123 Luxury Avenue, Downtown District, City, State 12345<br>
|
||||
<strong>Phone:</strong> +1 (555) 123-4567<br>
|
||||
<strong>Email:</strong> info@oldvinehotel.com
|
||||
</p>
|
||||
|
||||
<p>If you need to modify or cancel your reservation, please contact us at least 24 hours in advance.</p>
|
||||
|
||||
<p>We look forward to welcoming you to The Old Vine Hotel!</p>
|
||||
|
||||
<p>Warm regards,<br>
|
||||
The Old Vine Hotel Team</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>© 2025 The Old Vine Hotel. All rights reserved.</p>
|
||||
<p>This is an automated message. Please do not reply to this email.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
const generateBookingCancellationHTML = (context) => {
|
||||
const { guest, booking, room, cancellationFee, refundAmount } = context;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.header { background: #8B4513; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; }
|
||||
.booking-details { background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||
.footer { background: #f4f4f4; padding: 20px; text-align: center; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
<h2>Booking Cancellation</h2>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Dear ${guest.firstName} ${guest.lastName},</p>
|
||||
|
||||
<p>We have processed your cancellation request for the following booking:</p>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Cancelled Booking Details</h3>
|
||||
<p><strong>Booking Number:</strong> ${booking.bookingNumber}</p>
|
||||
<p><strong>Room:</strong> ${room.name}</p>
|
||||
<p><strong>Check-in Date:</strong> ${booking.checkInDate.toLocaleDateString()}</p>
|
||||
<p><strong>Check-out Date:</strong> ${booking.checkOutDate.toLocaleDateString()}</p>
|
||||
<p><strong>Original Amount:</strong> $${booking.totalAmount.toFixed(2)}</p>
|
||||
${cancellationFee > 0 ? `<p><strong>Cancellation Fee:</strong> $${cancellationFee.toFixed(2)}</p>` : ''}
|
||||
<p><strong>Refund Amount:</strong> $${refundAmount.toFixed(2)}</p>
|
||||
</div>
|
||||
|
||||
${refundAmount > 0 ? `
|
||||
<p>Your refund of $${refundAmount.toFixed(2)} will be processed within 5-7 business days and will appear on your original payment method.</p>
|
||||
` : ''}
|
||||
|
||||
<p>We're sorry to see you cancel your stay with us. We hope to welcome you to The Old Vine Hotel in the future.</p>
|
||||
|
||||
<p>If you have any questions about this cancellation, please don't hesitate to contact us.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Old Vine Hotel Team</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>© 2025 The Old Vine Hotel. All rights reserved.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
const generateContactFormHTML = (context) => {
|
||||
const { name, email, phone, message } = context;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.header { background: #8B4513; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; }
|
||||
.details { background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>The Old Vine Hotel</h1>
|
||||
<h2>New Contact Form Submission</h2>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h3>Contact Details</h3>
|
||||
<div class="details">
|
||||
<p><strong>Name:</strong> ${name}</p>
|
||||
<p><strong>Email:</strong> ${email}</p>
|
||||
<p><strong>Phone:</strong> ${phone || 'Not provided'}</p>
|
||||
</div>
|
||||
|
||||
<h3>Message</h3>
|
||||
<div class="details">
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
|
||||
<p><em>This message was sent from the hotel website contact form.</em></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
// Main send email function
|
||||
const sendEmail = async ({ to, subject, template, context, html, text }) => {
|
||||
try {
|
||||
const transporter = createTransporter();
|
||||
|
||||
let emailHTML = html;
|
||||
|
||||
// Generate HTML based on template
|
||||
if (template && context) {
|
||||
switch (template) {
|
||||
case 'bookingConfirmation':
|
||||
emailHTML = generateBookingConfirmationHTML(context);
|
||||
break;
|
||||
case 'bookingCancellation':
|
||||
emailHTML = generateBookingCancellationHTML(context);
|
||||
break;
|
||||
case 'contactForm':
|
||||
emailHTML = generateContactFormHTML(context);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown email template: ${template}`);
|
||||
}
|
||||
}
|
||||
|
||||
const mailOptions = {
|
||||
from: `"The Old Vine Hotel" <${process.env.EMAIL_FROM}>`,
|
||||
to,
|
||||
subject,
|
||||
html: emailHTML,
|
||||
text: text || '', // Plain text version
|
||||
};
|
||||
|
||||
const result = await transporter.sendMail(mailOptions);
|
||||
|
||||
logger.info(`Email sent successfully to ${to}`, {
|
||||
messageId: result.messageId,
|
||||
subject
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Email sending error:', {
|
||||
error: error.message,
|
||||
to,
|
||||
subject,
|
||||
template
|
||||
});
|
||||
|
||||
throw new Error(`Failed to send email: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Send bulk emails
|
||||
const sendBulkEmails = async (emails) => {
|
||||
const results = [];
|
||||
|
||||
for (const emailData of emails) {
|
||||
try {
|
||||
const result = await sendEmail(emailData);
|
||||
results.push({ success: true, to: emailData.to, messageId: result.messageId });
|
||||
} catch (error) {
|
||||
results.push({ success: false, to: emailData.to, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// Send newsletter
|
||||
const sendNewsletter = async (subscribers, subject, content) => {
|
||||
const emails = subscribers.map(subscriber => ({
|
||||
to: subscriber.email,
|
||||
subject,
|
||||
html: content,
|
||||
template: null
|
||||
}));
|
||||
|
||||
return sendBulkEmails(emails);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sendEmail,
|
||||
sendBulkEmails,
|
||||
sendNewsletter
|
||||
};
|
||||
Reference in New Issue
Block a user