diff --git a/backend/controller/contact.controller.js b/backend/controller/contact.controller.js index 4c81946..8171fca 100644 --- a/backend/controller/contact.controller.js +++ b/backend/controller/contact.controller.js @@ -1,8 +1,16 @@ const { z } = require("zod"); const nodemailer = require("nodemailer"); +const logger = require("../utils/logger"); require("dotenv").config(); -// data require form .env file : EMAIL_USER, EMAIL_PASS +const requiredEnvVars = ["EMAIL_USER", "EMAIL_PASS"]; +const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); + +if (missingEnvVars.length > 0) { + throw new Error( + `Missing required environment variables: ${missingEnvVars.join(", ")}` + ); +} // Define the Zod schema for contact form validation const contactSchema = z.object({ @@ -34,39 +42,41 @@ const createContactUs = async (req, res) => { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, }, - tls: { - rejectUnauthorized: false, // Disable strict SSL verification - }, + tls: { + rejectUnauthorized: false, // Disable strict SSL verification + }, }); + const sanitizeInput = (str) => { + return str.replace(/[<>]/g, "").trim(); + }; + const mailOptions = { - from: mail, + from: `"Contact Form" <${process.env.EMAIL_USER}>`, + replyTo: sanitizeInput(mail), to: process.env.EMAIL_USER, - subject: subject, - text: message, + subject: sanitizeInput(subject), + text: sanitizeInput(message), }; - // Send mail with defined transport object - await new Promise((resolve, reject) => { - transporter.sendMail(mailOptions, (error, info) => { - if (error) { - console.error("Error occurred: " + error.message); - reject(error); - } - resolve(info); - }); + // Use built-in promise interface + await transporter.sendMail(mailOptions); + } catch (err) { + logger.error("Validation failed", { + errors: validation.error.errors, + requestId: req.id, }); - res.status(200).json({ - status: "success", - message: "Your contact request has been successfully received.", - }); - } catch (err) { - console.error(`Error at transport: ${err}`); - res.status(500).json({ + const statusCode = err.code === "EAUTH" ? 401 : 500; + const errorMessage = + process.env.NODE_ENV === "production" + ? "There was an error sending your message. Please try again later." + : err.message; + + res.status(statusCode).json({ status: "error", - message: - "There was an error sending your message. Please try again later.", + message: errorMessage, + requestId: req.id, }); } }; diff --git a/backend/routes/contactUsRouter.js b/backend/routes/contactUsRouter.js index 9871cf8..9e43f74 100644 --- a/backend/routes/contactUsRouter.js +++ b/backend/routes/contactUsRouter.js @@ -1,7 +1,46 @@ const express = require("express"); const router = express.Router(); -const { createContactUs } = require("../controller/contact.controller"); +const { createContactUs } = require("../controller/contact.controller"); +const rateLimit = require("express-rate-limit"); +const { body } = require("express-validator"); -router.post("/contactus", createContactUs); +// Error handling middleware +router.use((err, req, res, next) => { + console.error(err.stack); + const statusCode = err.statusCode || 500; + const errorType = err.type || "InternalServerError"; + res.status(500).json({ + status: "error", + message: err.message || "An error occurred processing your request", + type: errorType, + ...(process.env.NODE_ENV === "development" && { stack: err.stack }), + }); +}); + +const contactFormLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 requests per window + message: { + status: "error", + message: "Too many requests, please try again later", + }, +}); + +router.post( + "/", + contactFormLimiter, + [ + body("email").isEmail().normalizeEmail(), + body("name").trim().isLength({ min: 2 }).escape(), + body("message").trim().isLength({ min: 10, max: 1000 }).escape(), + ], + async (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ status: "error", errors: errors.array() }); + } + await createContactUs(req, res, next); + } +); module.exports = router; diff --git a/backend/routes/index.js b/backend/routes/index.js index 096551d..b27298f 100644 --- a/backend/routes/index.js +++ b/backend/routes/index.js @@ -9,17 +9,42 @@ const safeRequire = (modulePath, fallbackMessage) => { try { return require(modulePath); } catch (error) { - logger.error(`Error loading ${modulePath}:`, error); - return (req, res) => { - res.status(500).json({ error: fallbackMessage }); + const errorDetails = { + module: modulePath.split("/").pop(), + message: error.message, + stack: process.env.NODE_ENV === "development" ? error.stack : undefined, }; + logger.error("Module loading error:", errorDetails); + + // Return a pre-defined handler to avoid creating closures + return safeRequire.errorHandler(fallbackMessage); } }; +// Pre-defined error handler to avoid creating closures +safeRequire.errorHandler = (message) => (req, res) => { + res.status(503).json({ + status: "error", + message: + process.env.NODE_ENV === "production" + ? message + : `Service unavailable: ${message}`, + }); +}; + // Safely load routers with error handling -const feedbackRouter = safeRequire("./feedbackRouter", "Feedback functionality is currently unavailable"); -const contactUsRouter = safeRequire("./contactUsRouter", "Contact Us functionality is currently unavailable"); -const eventRouter = safeRequire("./eventRouter", "Event functionality is currently unavailable"); +const feedbackRouter = safeRequire( + "./feedbackRouter", + "Feedback functionality is currently unavailable" +); +const contactUsRouter = safeRequire( + "./contactUsRouter", + "Contact Us functionality is currently unavailable" +); +const eventRouter = safeRequire( + "./eventRouter", + "Event functionality is currently unavailable" +); router.get("/", (req, res) => { return res.json({ @@ -33,13 +58,42 @@ router.get("/", (req, res) => { }); }); +// Authentication routes +router.use( + "/admin", + safeRequire("./adminRouter", "Admin functionality is currently unavailable") +); +router.use( + "/user", + safeRequire("./customerRouter", "User functionality is currently unavailable") +); +router.use( + "/forgot", + safeRequire( + "./forgotRouter", + "Forgot password functionality is currently unavailable" + ) +); + +// Core feature routes +router.use( + "/reservation", + safeRequire( + "./reservationRouter", + "Reservation functionality is currently unavailable" + ) +); router.use("/event", eventRouter); -router.use("/admin", safeRequire("./adminRouter", "Admin functionality is currently unavailable")); + +// Feedback and communication routes router.use("/feedback", feedbackRouter); -router.use("/user", safeRequire("./customerRouter", "User functionality is currently unavailable")); -router.use("/reservation", safeRequire("./reservationRouter", "Reservation functionality is currently unavailable")); -router.use("/newsletter", safeRequire("./newsletterRoute", "Newsletter functionality is currently unavailable")); -router.use("/forgot", safeRequire("./forgotRouter", "Forgot password functionality is currently unavailable")); -router.use("/contact", contactUsRouter); +router.use("/contact", contactUsRouter); +router.use( + "/newsletter", + safeRequire( + "./newsletterRoute", + "Newsletter functionality is currently unavailable" + ) +); module.exports = router; diff --git a/frontend/src/components/Pages/ContactUs.jsx b/frontend/src/components/Pages/ContactUs.jsx index 50c9e6e..afac23e 100644 --- a/frontend/src/components/Pages/ContactUs.jsx +++ b/frontend/src/components/Pages/ContactUs.jsx @@ -15,7 +15,21 @@ const ContactUs = () => { }; // Use an environment variable for backend URL - const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000'; + const DEFAULT_URL = 'http://localhost:3000'; + + const API_URL = (() => { + try { + const url = new URL(import.meta.env.VITE_BACKEND_URL || DEFAULT_URL); + if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error('Invalid protocol'); + } + return url.toString(); + } catch (e) { + console.warn('Invalid VITE_BACKEND_URL, using default:', DEFAULT_URL); + return DEFAULT_URL; + } + })(); + const [mail, setMail] = useState(''); const [subject, setSubject] = useState(''); const [message, setMessage] = useState(''); @@ -27,7 +41,32 @@ const ContactUs = () => { e.preventDefault(); // Basic client-side validation for security - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + + const sanitizeInput = (input) => { + return input.replace(/[<>]/g, ''); + }; + + // Add to state + const [lastSubmissionTime, setLastSubmissionTime] = useState(0); + + if (!emailRegex.test(mail)) { + setError('Please enter a valid email address.'); + return; + } + + // Rate limiting + const now = Date.now(); + if (now - lastSubmissionTime < 60000) { + // 1 minute + setError('Please wait before submitting again.'); + return; + } + + // Sanitize inputs before sending + const sanitizedSubject = sanitizeInput(subject); + const sanitizedMessage = sanitizeInput(message); if (!mail || !subject || !message) { setError('All fields are required.'); @@ -43,7 +82,7 @@ const ContactUs = () => { setError('Subject must be less than 100 characters.'); return; } - + if (message.length > 1000) { setError('Message must be less than 1000 characters.'); return; @@ -52,14 +91,45 @@ const ContactUs = () => { setError(null); setIsLoading(true); + const fetchWithTimeout = async (url, options, timeout = 5000) => { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }); + clearTimeout(id); + return response; + } catch (error) { + clearTimeout(id); + throw error; + } + }; + const retryWithBackoff = async (fn, retries = 3, backoff = 300) => { + try { + return await fn(); + } catch (error) { + if (retries === 0) throw error; + await new Promise((resolve) => setTimeout(resolve, backoff)); + return retryWithBackoff(fn, retries - 1, backoff * 2); + } + }; + try { - const response = await fetch(`${API_URL}/api/contact/contactus`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ mail, subject, message }), - }); + const response = await retryWithBackoff(() => + fetchWithTimeout(`${API_URL}/api/contact/contactus`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + mail, + subject: sanitizedSubject, + message: sanitizedMessage, + }), + }) + ); if (!response.ok) { throw new Error(`Network response was not ok: ${response.status}`);