From e6b2d03c5dfd397b5458b29ba11eede5a8d4227e Mon Sep 17 00:00:00 2001 From: haseebzaki-07 Date: Wed, 30 Oct 2024 11:49:10 +0530 Subject: [PATCH] add dashboard --- backend/.env.example | 3 +- backend/controller/customer.controller.js | 33 ++---- backend/controller/reservation.controller.js | 76 +++++++++++-- backend/index.js | 3 +- backend/middlewares/sessionMiddleware.js | 21 ++-- backend/models/customer.model.js | 16 +-- backend/models/reservation.model.js | 5 + backend/routes/reservationRouter.js | 6 +- frontend/package.json | 1 + frontend/src/components/Pages/Dashboard.tsx | 112 +++++++++++++++++++ frontend/src/components/Pages/Login.jsx | 19 ++-- frontend/src/components/Shared/Navbar.jsx | 1 + frontend/src/router/index.jsx | 2 + 13 files changed, 239 insertions(+), 59 deletions(-) create mode 100644 frontend/src/components/Pages/Dashboard.tsx diff --git a/backend/.env.example b/backend/.env.example index 8c71fad6..da4e6036 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,4 +8,5 @@ GOOGLE_CLIENT_SECRET=your_google_client_secret FRONTEND_URL=your_frontend_url CALLBACK_URL=http://localhost:3000/auth/google/callback PROD_CALLBACK_URL=https://play-cafe.vercel.app/auth/google/callback -NODE_ENV=development \ No newline at end of file +NODE_ENV=development +SECRET_KEY=your_secret_key diff --git a/backend/controller/customer.controller.js b/backend/controller/customer.controller.js index b6195ece..92242da7 100644 --- a/backend/controller/customer.controller.js +++ b/backend/controller/customer.controller.js @@ -89,7 +89,6 @@ async function loginCustomer(req, res) { password: z.string().min(6, "Password must be at least 6 characters long"), }); - const validation = customerLoginSchema.safeParse(req.body); if (!validation.success) { return res.status(400).json({ error: validation.error.errors }); @@ -97,7 +96,7 @@ async function loginCustomer(req, res) { try { const customer = await Customer.findOne({ email: req.body.email }); - + if (!customer) { return res.status(401).json({ error: "Invalid email or password" }); } @@ -106,35 +105,26 @@ async function loginCustomer(req, res) { } const validPassword = await bcrypt.compare(req.body.password, customer.password); - if (!validPassword) { return res.status(401).json({ error: "Invalid email or password" }); } const payload = { - sub: customer._id, - name: customer.name, // Optional - role: "customer", // Optional - email: customer.email, // Optional - }; - - const token = jwt.sign( - payload, - process.env.JWT_SECRET, - { expiresIn: "1h" } // Expires in 1 hour - ); - - req.session.user = { - id: customer._id, + sub: customer._id, // Use `sub` as this is a standard JWT claim for subject (user ID) name: customer.name, + role: "customer", + email: customer.email, }; + const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "1h" }); + res.cookie("authToken", token, { - maxAge: 1000 * 60 * 60, - httpOnly: true, - secure: true, + maxAge: 60 * 60 * 1000, // 1 hour + httpOnly: false, // Set to false if you need access on the frontend + secure: process.env.NODE_ENV === "production", // Set `secure: true` only in production with HTTPS + sameSite: "strict", // Use `strict` to avoid CSRF in most cases }); - + return res.json({ message: "Login successful", token, @@ -147,7 +137,6 @@ async function loginCustomer(req, res) { }); } catch (error) { console.error("Error during login:", error); - res.status(500).json({ error: "Internal server error" }); } } diff --git a/backend/controller/reservation.controller.js b/backend/controller/reservation.controller.js index 43019db8..74643d99 100644 --- a/backend/controller/reservation.controller.js +++ b/backend/controller/reservation.controller.js @@ -1,7 +1,8 @@ const { z } = require("zod"); const Reservation = require("../models/reservation.model"); +const Customer = require("../models/customer.model"); // Import Customer model const logger = require("../config/logger"); -const { sendReservationConfirmation } = require("../config/nodemailer"); // Import your email function +const { sendReservationConfirmation } = require("../config/nodemailer"); // Import email function // Define the Zod schema for reservation validation const reservationSchema = z @@ -9,10 +10,12 @@ const reservationSchema = z guests: z.string(), date: z.string(), time: z.string(), - email: z.string().email(), // Include email validation in the schema + email: z.string().email(), // Include email validation + userId: z.string().optional(), // Make userId optional for validation }) .strict(); // Disallow unknown keys +// Controller to create a reservation async function createReservation(req, res) { try { const validationResult = reservationSchema.safeParse(req.body); @@ -28,13 +31,29 @@ async function createReservation(req, res) { errors: validationResult.error.errors, }); } + const userId = req.params.id - // Create the reservation in the database - const reservation = await Reservation.create(validationResult.data); + const { email, date, guests, time } = validationResult.data; + + // Find the customer by userId if provided + const customer = await Customer.findById(userId); + if (!customer) { + return res.status(404).json({ + success: false, + message: "Customer not found.", + }); + } + + // Create the reservation in the database with userId reference + const reservation = await Reservation.create({ + guests, + date, + time, + customer: customer._id, // Associate with customer + }); // Send a confirmation email try { - const { email, date, guests, time } = validationResult.data; await sendReservationConfirmation(email, { reservationDate: date, guests, @@ -45,10 +64,10 @@ async function createReservation(req, res) { logger.error("Error sending reservation confirmation email:", { message: emailError.message, }); - // Email error should not block the main reservation process, so no need to return a failure response + // Email error does not block reservation creation } - // Send the success response + // Respond with success res.status(201).json({ success: true, message: "Reservation created successfully", @@ -68,6 +87,49 @@ async function createReservation(req, res) { } } +// Controller to fetch all reservations for a specific user +async function fetchUserReservations(req, res) { + try { + const userId = req.params.id; // Extract user ID from route parameters + + if (!userId) { + return res.status(400).json({ + success: false, + message: "User ID is required.", + }); + } + + // Find all reservations associated with the customer + const reservations = await Reservation.find({ customer: userId }).populate("customer", "name email"); + + if (reservations.length === 0) { + logger.info(`No reservations found for user ID: ${userId}`); + return res.status(404).json({ + success: false, + message: "No reservations found for this user.", + }); + } + + res.status(200).json({ + success: true, + message: "Reservations retrieved successfully.", + data: reservations, + }); + } catch (error) { + logger.error("Error fetching user reservations:", { + message: error.message, + stack: error.stack, + userId: req.params.id, + }); + + res.status(500).json({ + success: false, + message: "An error occurred while fetching reservations.", + }); + } +} + module.exports = { createReservation, + fetchUserReservations }; diff --git a/backend/index.js b/backend/index.js index 5dfeab5a..3c597f17 100644 --- a/backend/index.js +++ b/backend/index.js @@ -11,15 +11,16 @@ const app = express(); const port = process.env.PORT || 3000; const session = require("express-session"); const MongoStore = require("connect-mongo"); - // CORS configuration const corsOptions = { origin: ["http://localhost:5173", "https://play-cafe.vercel.app"], + credentials: true, optionsSuccessStatus: 200, }; app.use(cors(corsOptions)); + app.use(express.json()); app.use('/api', newsletterRoute); diff --git a/backend/middlewares/sessionMiddleware.js b/backend/middlewares/sessionMiddleware.js index aefe6d89..1d6f0281 100644 --- a/backend/middlewares/sessionMiddleware.js +++ b/backend/middlewares/sessionMiddleware.js @@ -1,12 +1,13 @@ -const sessionMiddleware = async (req, res, next)=>{ - console.log(req.session.user); - - - if (req.session.user !== undefined) { - next(); +const sessionMiddleware = (req, res, next) => { + if (req.session.user && req.session.user.id) { + next(); // Continue if session is valid and has user ID } else { - res.status(401).send("Invalid session. Please log in again."); + res.status(401).json({ + success: false, + message: "Invalid session. Please log in again.", + }); } -} - -module.exports = sessionMiddleware; \ No newline at end of file + }; + + module.exports = sessionMiddleware; + \ No newline at end of file diff --git a/backend/models/customer.model.js b/backend/models/customer.model.js index ecc17807..d0bc27bb 100644 --- a/backend/models/customer.model.js +++ b/backend/models/customer.model.js @@ -1,4 +1,3 @@ - const mongoose = require("mongoose"); const Schema = mongoose.Schema; @@ -20,18 +19,16 @@ const customerSchema = new Schema( verificationCode: { type: String, default: "", - }, otp: { - type: String, + type: String, }, otpExpiry: { - type: Date, + type: Date, }, isVerified: { - type: Boolean, + type: Boolean, default: false, - }, role: { type: String, @@ -52,7 +49,12 @@ const customerSchema = new Schema( ref: "Order", }, ], - + reservations: [ + { + type: Schema.Types.ObjectId, + ref: "Reservation", // Link to Reservation schema + }, + ], }, { timestamps: true } ); diff --git a/backend/models/reservation.model.js b/backend/models/reservation.model.js index c3eef159..a9f9c716 100644 --- a/backend/models/reservation.model.js +++ b/backend/models/reservation.model.js @@ -14,6 +14,11 @@ const reservationSchema = new Schema({ type: String, required: true, }, + customer: { + type: Schema.Types.ObjectId, + ref: "Customer", // Link back to Customer schema + required: true, + }, }); const Reservation = mongoose.model("Reservation", reservationSchema); diff --git a/backend/routes/reservationRouter.js b/backend/routes/reservationRouter.js index 3e6dcfb2..dd4eef57 100644 --- a/backend/routes/reservationRouter.js +++ b/backend/routes/reservationRouter.js @@ -1,9 +1,11 @@ const express = require("express"); -const { createReservation } = require("../controller/reservation.controller"); +const { createReservation, fetchUserReservations } = require("../controller/reservation.controller"); const sessionMiddleware = require("../middlewares/sessionMiddleware"); +const authenticateCustomer = require("../middlewares/authCustomer"); const router = express.Router(); -router.post("/create", sessionMiddleware, createReservation); +router.post("/create/:id", authenticateCustomer, createReservation); +router.get("/get/:id", authenticateCustomer, fetchUserReservations); router.get("/", (req, res) => { res.json({ message: "Welcome to the restaurant reservation API!", diff --git a/frontend/package.json b/frontend/package.json index 47cee436..c0c4b9c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "framer-motion": "^11.5.6", "gsap": "^3.12.5", "js-cookie": "^3.0.5", + "jwt-decode": "^4.0.0", "lucide-react": "^0.454.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/components/Pages/Dashboard.tsx b/frontend/src/components/Pages/Dashboard.tsx new file mode 100644 index 00000000..2e81c6b5 --- /dev/null +++ b/frontend/src/components/Pages/Dashboard.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import Cookies from 'js-cookie'; +import { jwtDecode } from 'jwt-decode'; + +function Profile() { + const [reservations, setReservations] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000'; + + // Fetch reservation data from API + useEffect(() => { + const fetchReservations = async () => { + const authToken = Cookies.get('authToken'); // Retrieve the authToken from cookies + + if (!authToken) { + alert("Please sign in to view your reservations."); + navigate('/login'); + return; + } + + // Decode the token to get the user ID + let userId; + try { + const decodedToken = jwtDecode(authToken); + userId = decodedToken.sub; // Use `sub` based on the backend token payload + console.log("Decoded userId:", userId); // Debugging line + } catch (decodeError) { + console.error("Error decoding token:", decodeError); + alert("Invalid token. Please log in again."); + navigate('/login'); + return; + } + + try { + const response = await fetch( + `${API_URL}/api/reservation/get/${userId}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}`, // Pass the token in headers + }, + credentials: 'include', + } + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Failed to fetch reservations'); + } + + const data = await response.json(); + setReservations(data.data); + } catch (error) { + setError(error.message); + console.error('Error fetching reservations:', error); + } finally { + setLoading(false); // Set loading to false after the fetch operation + } + }; + + fetchReservations(); + }, [navigate]); + + return ( +
+
+

Your Reservations

+ + {error && ( +

+ {error} +

+ )} + + {loading ? ( +

Loading your reservations...

+ ) : ( +
+ {reservations.length > 0 ? ( + reservations.map((reservation, index) => ( + + )) + ) : ( +

No reservations found.

+ )} +
+ )} +
+
+ ); +} + +// Separate ReservationCard component for better readability +const ReservationCard = ({ reservation }) => ( +
+

Reservation Details

+

Guests: {reservation.guests}

+

Date: {new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(new Date(reservation.date))}

+

Time: {reservation.time}

+

Email: {reservation.email}

+
+); + +export default Profile; diff --git a/frontend/src/components/Pages/Login.jsx b/frontend/src/components/Pages/Login.jsx index e39743d4..d6529595 100644 --- a/frontend/src/components/Pages/Login.jsx +++ b/frontend/src/components/Pages/Login.jsx @@ -8,14 +8,10 @@ import { FaEyeSlash } from 'react-icons/fa6'; const Login = () => { const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000'; - const [data, setData] = useState({ - email: '', - password: '', - }); + const [data, setData] = useState({ email: '', password: '' }); const [hidden, setHidden] = useState(true); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const navigate = useNavigate(); const handleChange = (e) => { @@ -26,6 +22,7 @@ const Login = () => { e.preventDefault(); setIsLoading(true); setError(null); + try { const response = await fetch(`${API_URL}/api/user/login`, { method: 'POST', @@ -35,10 +32,14 @@ const Login = () => { body: JSON.stringify(data), }); const result = await response.json(); - if (!response.ok) { - throw new Error(result.message || 'Login failed'); - } - Cookies.set('authToken', result.token, { expire: '1h', secure: true }); + if (!response.ok) throw new Error(result.message || 'Login failed'); + + Cookies.set('authToken', result.token, { + expires: 1 / 24, // 1 hour + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }); + message.success('Login successful'); navigate('/'); } catch (err) { diff --git a/frontend/src/components/Shared/Navbar.jsx b/frontend/src/components/Shared/Navbar.jsx index e01dd381..ff33ddcb 100644 --- a/frontend/src/components/Shared/Navbar.jsx +++ b/frontend/src/components/Shared/Navbar.jsx @@ -22,6 +22,7 @@ const Navbar = () => { { name: 'RESERVATION', path: '/reservation' }, { name: 'BOARDGAMES', path: '/boardgame' }, { name: 'MEMBERSHIP', path: '/membership' }, // Add Membership here + { name: 'PROFILE', path: '/dashboard' }, // Add Membership here ]; useEffect(() => { diff --git a/frontend/src/router/index.jsx b/frontend/src/router/index.jsx index 83ca6a13..2d8d0585 100644 --- a/frontend/src/router/index.jsx +++ b/frontend/src/router/index.jsx @@ -22,6 +22,7 @@ import VerifyOtp from '../components/Pages/VerifyOtp'; import EmailVerify from '../components/Pages/EmailVerify'; import Membership from '../components/Membership'; import OtpRegisterVerify from '../components/Pages/verifyRegisterOtp'; +import Profile from '../components/Pages/Dashboard'; const router = createBrowserRouter( createRoutesFromElements( }> @@ -41,6 +42,7 @@ const router = createBrowserRouter( } /> } /> } /> + } /> )