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 !!
+
+
+
+
+
+
+
+
+ {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}`);