From b563fe256cfe0449b40237b1d0942240c315e7c3 Mon Sep 17 00:00:00 2001 From: PriyanshuValiya <147643182+PriyanshuValiya@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:06:00 +0530 Subject: [PATCH 1/2] add feat/contactus-page branch --- backend/controller/contact.controller.js | 74 ++++++++ backend/routes/contactUsRouter.js | 7 + backend/routes/index.js | 60 +++---- frontend/src/components/Pages/ContactUs.jsx | 185 ++++++++++++++++++++ frontend/src/components/Shared/Navbar.jsx | 1 + frontend/src/router/index.jsx | 4 +- 6 files changed, 294 insertions(+), 37 deletions(-) create mode 100644 backend/controller/contact.controller.js create mode 100644 backend/routes/contactUsRouter.js create mode 100644 frontend/src/components/Pages/ContactUs.jsx diff --git a/backend/controller/contact.controller.js b/backend/controller/contact.controller.js new file mode 100644 index 00000000..4c81946f --- /dev/null +++ b/backend/controller/contact.controller.js @@ -0,0 +1,74 @@ +const { z } = require("zod"); +const nodemailer = require("nodemailer"); +require("dotenv").config(); + +// data require form .env file : EMAIL_USER, EMAIL_PASS + +// Define the Zod schema for contact form validation +const contactSchema = z.object({ + mail: z.string().email(), + subject: z.string().min(5, "Subject must be at least 5 characters long."), + message: z.string().min(5, "Message must be at least 5 characters long."), +}); + +const createContactUs = async (req, res) => { + const validation = contactSchema.safeParse(req.body); + + if (!validation.success) { + console.error("Error at validation"); + return res.status(400).json({ + status: "error", + errors: validation.error.errors, + }); + } + + const { mail, subject, message } = req.body; + + try { + const transporter = nodemailer.createTransport({ + service: "gmail", + host: "smtp.gmail.com", + port: 587, + secure: false, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + tls: { + rejectUnauthorized: false, // Disable strict SSL verification + }, + }); + + const mailOptions = { + from: mail, + to: process.env.EMAIL_USER, + subject: subject, + text: 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); + }); + }); + + 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({ + status: "error", + message: + "There was an error sending your message. Please try again later.", + }); + } +}; + +module.exports = { createContactUs }; diff --git a/backend/routes/contactUsRouter.js b/backend/routes/contactUsRouter.js new file mode 100644 index 00000000..9871cf8f --- /dev/null +++ b/backend/routes/contactUsRouter.js @@ -0,0 +1,7 @@ +const express = require("express"); +const router = express.Router(); +const { createContactUs } = require("../controller/contact.controller"); + +router.post("/contactus", createContactUs); + +module.exports = router; diff --git a/backend/routes/index.js b/backend/routes/index.js index 27a2b8bd..096551dd 100644 --- a/backend/routes/index.js +++ b/backend/routes/index.js @@ -1,39 +1,25 @@ const express = require("express"); -const logger = require("../config/logger"); // Import your Winston logger +const logger = require("../config/logger"); // Import Winston logger require("dotenv").config(); -const config = { - JWT_SECRET: process.env.JWT_SECRET, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, -}; - const router = express.Router(); -let feedbackRouter; - -try { - feedbackRouter = require("./feedbackRouter"); -} catch (error) { - logger.error("Error loading feedbackRouter:", error); // Log the error with Winston - feedbackRouter = (req, res) => { - res - .status(500) - .json({ error: "Feedback functionality is currently unavailable" }); - }; -} +// Utility function to safely load modules and handle errors +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 }); + }; + } +}; -let eventRouter; -try { - eventRouter = require("./eventRouter"); -} catch (error) { - logger.error("Error loading eventRouter:", error); // Log the error with Winston - eventRouter = (req, res) => { - res - .status(500) - .json({ error: "Event functionality is currently unavailable" }); - }; -} +// 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"); router.get("/", (req, res) => { return res.json({ @@ -41,17 +27,19 @@ router.get("/", (req, res) => { version: "1.0.0", endpoints: { Reservation: "/reservation", - Feedback: "/feedback", // Added feedback endpoint documentation + Feedback: "/feedback", }, documentation: "https://api-docs-url.com", }); }); router.use("/event", eventRouter); -router.use("/admin", require("./adminRouter")); +router.use("/admin", safeRequire("./adminRouter", "Admin functionality is currently unavailable")); router.use("/feedback", feedbackRouter); -router.use("/user", require("./customerRouter")); -router.use("/reservation", require("./reservationRouter")); -router.use("/newsletter", require("./newsletterRoute")); -router.use("/forgot", require("./forgotRouter")); +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); + module.exports = router; diff --git a/frontend/src/components/Pages/ContactUs.jsx b/frontend/src/components/Pages/ContactUs.jsx new file mode 100644 index 00000000..50c9e6e5 --- /dev/null +++ b/frontend/src/components/Pages/ContactUs.jsx @@ -0,0 +1,185 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { useInView } from 'react-intersection-observer'; +import chess from '../../assets/img/chess.gif'; + +const ContactUs = () => { + const { ref, inView } = useInView({ + threshold: 0.2, + triggerOnce: true, + }); + + const animationVariants = { + hidden: { opacity: 0, y: 50 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5 } }, + }; + + // Use an environment variable for backend URL + const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000'; + const [mail, setMail] = useState(''); + const [subject, setSubject] = useState(''); + const [message, setMessage] = useState(''); + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Basic client-side validation for security + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!mail || !subject || !message) { + setError('All fields are required.'); + return; + } + + if (!emailRegex.test(mail)) { + setError('Please enter a valid email address.'); + return; + } + + if (subject.length > 100) { + setError('Subject must be less than 100 characters.'); + return; + } + + if (message.length > 1000) { + setError('Message must be less than 1000 characters.'); + return; + } + + setError(null); + setIsLoading(true); + + try { + const response = await fetch(`${API_URL}/api/contact/contactus`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ mail, subject, message }), + }); + + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.status}`); + } + + setSubmitted(true); + setTimeout(() => { + setMail(''); + setSubject(''); + setMessage(''); + setSubmitted(false); + }, 4000); + } catch (error) { + setError('An error occurred while sending Mail...'); + console.error('Mail sending failed : ', error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ +
+

+ Feel Free To Mail Us.. +

+

+ Have questions or need assistance ? Reach out to us, and we'll be + happy to help !! +

+
+ Chess +
+
+ +
+
+
+ + setMail(e.target.value)} + required + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-[#004D43] focus:border-[#004D43]" + /> +
+
+ setSubject(e.target.value)} + required + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-[#004D43] focus:border-[#004D43]" + /> +
+
+ +
+
+ +
+
+ {submitted && ( + + Thank you, We will reply you soon... + + )} + {error && ( + + {error} + + )} +
+
+
+
+ ); +}; + +export default ContactUs; diff --git a/frontend/src/components/Shared/Navbar.jsx b/frontend/src/components/Shared/Navbar.jsx index 260a1386..da3c47b0 100644 --- a/frontend/src/components/Shared/Navbar.jsx +++ b/frontend/src/components/Shared/Navbar.jsx @@ -20,6 +20,7 @@ const Navbar = () => { { name: 'RESERVATION', path: '/reservation' }, { name: 'BOARDGAMES', path: '/boardgame' }, { name: 'MEMBERSHIP', path: '/membership' }, // Add Membership here + { name: 'CONTACTUS', path: '/contactus'} ]; useEffect(() => { diff --git a/frontend/src/router/index.jsx b/frontend/src/router/index.jsx index 964706ca..5f4b7a74 100644 --- a/frontend/src/router/index.jsx +++ b/frontend/src/router/index.jsx @@ -22,6 +22,8 @@ import VerifyOtp from '../components/Pages/VerifyOtp'; import EmailVerify from '../components/Pages/EmailVerify'; import Membership from '../components/Membership'; import HelpAndSupport from '../components/Pages/HelpAndSupport'; +import ContactUs from '../components/Pages/ContactUs'; + const router = createBrowserRouter( createRoutesFromElements( }> @@ -41,7 +43,7 @@ const router = createBrowserRouter( } /> } /> } /> - + } /> ) ); From 3eddb5117623d6328093ee58a0ca68b75fed02aa Mon Sep 17 00:00:00 2001 From: PriyanshuValiya <147643182+PriyanshuValiya@users.noreply.github.com> Date: Sat, 26 Oct 2024 20:13:24 +0530 Subject: [PATCH 2/2] Some File Changes --- backend/controller/contact.controller.js | 60 ++++++++------ backend/routes/contactUsRouter.js | 43 +++++++++- backend/routes/index.js | 78 +++++++++++++++--- frontend/src/components/Pages/ContactUs.jsx | 90 ++++++++++++++++++--- 4 files changed, 222 insertions(+), 49 deletions(-) diff --git a/backend/controller/contact.controller.js b/backend/controller/contact.controller.js index 4c81946f..8171fca1 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 9871cf8f..9e43f748 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 096551dd..b27298f4 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 50c9e6e5..afac23ee 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}`);