diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..26ec636 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,4 @@ +MONGO_URI=mongodb://localhost:27017/password-reset +EMAIL_USER=your_email@gmail.com +EMAIL_PASS=your_app_password +PORT=5000 diff --git a/backend/package-lock.json b/backend/package-lock.json index d03e198..b0404a3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,16 +9,18 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@tanstack/react-query": "^5.90.5", "bcryptjs": "^3.0.2", "body-parser": "^1.20.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.6.1", "express": "^4.21.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.2", "morgan": "^1.10.0", - "nodemailer": "^7.0.9", + "nodemailer": "^7.0.10", "nodemon": "^3.1.10", "uuid": "^13.0.0" }, @@ -27,10 +29,11 @@ "@types/body-parser": "^1.19.2", "@types/cookie-parser": "^1.4.9", "@types/cors": "^2.8.13", - "@types/express": "^4.17.23", + "@types/express": "^4.17.25", "@types/jsonwebtoken": "^9.0.10", "@types/morgan": "^1.9.7", - "@types/nodemailer": "^7.0.2", + "@types/node": "^24.9.2", + "@types/nodemailer": "^7.0.3", "@types/uuid": "^10.0.0", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", @@ -1430,6 +1433,32 @@ "node": ">=18.0.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz", + "integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz", + "integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1507,9 +1536,9 @@ } }, "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", "peer": true, @@ -1517,7 +1546,7 @@ "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { @@ -1576,20 +1605,20 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", - "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/nodemailer": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.2.tgz", - "integrity": "sha512-Zo6uOA9157WRgBk/ZhMpTQ/iCWLMk7OIs/Q9jvHarMvrzUUP/MDdPHL2U1zpf57HrrWGv4nYQn5uIxna0xY3xw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.3.tgz", + "integrity": "sha512-fC8w49YQ868IuPWRXqPfLf+MuTRex5Z1qxMoG8rr70riqqbOp2F5xgOKE9fODEBPzpnvjkJXFgK6IL2xgMSTnA==", "dev": true, "license": "MIT", "dependencies": { @@ -2011,6 +2040,13 @@ "dev": true, "license": "MIT" }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2921,9 +2957,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", - "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", + "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -3152,6 +3188,16 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3670,9 +3716,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, diff --git a/backend/package.json b/backend/package.json index 8f41037..0dd174f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,16 +11,18 @@ "author": "", "license": "MIT", "dependencies": { + "@tanstack/react-query": "^5.90.5", "bcryptjs": "^3.0.2", "body-parser": "^1.20.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.6.1", "express": "^4.21.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.2", "morgan": "^1.10.0", - "nodemailer": "^7.0.9", + "nodemailer": "^7.0.10", "nodemon": "^3.1.10", "uuid": "^13.0.0" }, @@ -29,10 +31,11 @@ "@types/body-parser": "^1.19.2", "@types/cookie-parser": "^1.4.9", "@types/cors": "^2.8.13", - "@types/express": "^4.17.23", + "@types/express": "^4.17.25", "@types/jsonwebtoken": "^9.0.10", "@types/morgan": "^1.9.7", - "@types/nodemailer": "^7.0.2", + "@types/node": "^24.9.2", + "@types/nodemailer": "^7.0.3", "@types/uuid": "^10.0.0", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", diff --git a/backend/src/app.ts b/backend/src/app.ts index 1961eae..6f84fb1 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,44 +1,211 @@ -import express from 'express'; -import cors from 'cors'; -import morgan from 'morgan'; -import bodyParser from 'body-parser'; -import healthRoutes from './routes/healthRoutes'; -import userRoutes from "./routes/user.routes"; -import productRoutes from "./routes/product.routes"; -import cartRoutes from "./routes/cartRoutes"; +import express, { Application, Request, Response } from "express"; +import cors from "cors"; +import morgan from "morgan"; +import bodyParser from "body-parser"; +import cookieParser from "cookie-parser"; +import dotenv from "dotenv"; import mongoose from "mongoose"; +import bcrypt from "bcryptjs"; +import crypto from "crypto"; +import nodemailer from "nodemailer"; -import cookieParser from "cookie-parser"; +import healthRoutes from "./routes/healthRoutes.js"; +import userRoutes from "./routes/user.routes.js"; +import productRoutes from "./routes/product.routes.js"; +import cartRoutes from "./routes/cartRoutes.js"; +import authRoutes from "./routes/auth.js"; -const app = express(); +import { signAccessToken, signRefreshToken, verifyRefreshToken } from "./controllers/auth.js"; +import { authenticate, AuthRequest } from "./middleware/authMiddleware.js"; -import dotenv from "dotenv"; dotenv.config(); -// Middleware setup +const app: Application = express(); + +// Middleware app.use(cors()); -app.use(morgan('dev')); +app.use(morgan("dev")); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); -// Middleware needed for Auth app.use(cookieParser()); +// Interface and mock data (temporary) +interface User { + id: string; + username: string; + passwordHash: string; + role: string; + email?: string; + resetPasswordToken?: string; + resetPasswordExpires?: Date; +} + +const users: User[] = [ + { id: "1", username: "alice", passwordHash: bcrypt.hashSync("password", 8), role: "admin", email: "alice@example.com" }, +]; + +const refreshTokens = new Map(); + +// ------------------- AUTHENTICATION ROUTES ------------------- + +app.post("/login", async (req: Request, res: Response) => { + const { username, password } = req.body; + const user = users.find((u) => u.username === username); + if (!user) return res.status(401).json({ error: "Invalid credentials" }); + const match = await bcrypt.compare(password, user.passwordHash); + if (!match) return res.status(401).json({ error: "Invalid credentials" }); + + const payload = { sub: user.id, username: user.username, role: user.role }; + const accessToken = signAccessToken(payload); + const refreshToken = signRefreshToken(payload); // app.get("/protected", authenticate, (req: AuthRequest, res: Response) => { // res.json({ message: "Protected route accessed", user: req.user }); // }); + res.json({ accessToken }); +}); + +app.post("/refresh", (req: Request, res: Response) => { + const token = req.cookies?.refreshToken || req.body.refreshToken; + + if (!token) { + return res.status(401).json({ error: "Refresh token is missing" }); + } + + try { + const payload = verifyRefreshToken(token); + const storedToken = refreshTokens.get(payload.sub); + + if (!storedToken) { + return res.status(401).json({ error: "Session not found or already logged out" }); + } + + if (storedToken !== token) { + return res.status(401).json({ error: "Token used is not the latest valid token" }); + } + + const cleanPayload = { + sub: payload.sub, + username: payload.username, + role: payload.role, + }; + + const newAccess = signAccessToken(cleanPayload); + const newRefresh = signRefreshToken(cleanPayload); + + refreshTokens.set(cleanPayload.sub, newRefresh); + + res.cookie("refreshToken", newRefresh, { + httpOnly: true, + sameSite: "strict", + secure: process.env.NODE_ENV === "production", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); -// Existing API Routes -app.use('/api/health', healthRoutes); + res.json({ accessToken: newAccess }); + } catch (error) { + console.error("Refresh token verification failed:", error); + res.status(401).json({ error: "Refresh token is expired or invalid" }); + } +}); + +app.post("/logout", authenticate, (req: AuthRequest, res: Response) => { + refreshTokens.delete(req.user!.sub); + res.clearCookie("refreshToken"); + res.status(204).send(); +}); + +app.get("/protected", authenticate, (req: AuthRequest, res: Response) => { + res.json({ message: "Protected route accessed", user: req.user }); +}); + +// ------------------- PASSWORD RESET ROUTES ------------------- + +/** + * Step 1: Request password reset + * Generates token, sets expiry, and emails reset link. + */ +app.post("/api/request-reset", async (req: Request, res: Response) => { + const { email } = req.body; + const user = users.find((u) => u.email === email); + + if (!user) { + return res.status(404).json({ msg: "User not found" }); + } + + const token = crypto.randomBytes(32).toString("hex"); + user.resetPasswordToken = token; + user.resetPasswordExpires = new Date(Date.now() + 3600000); // 1 hour + + const resetLink = `${process.env.FRONTEND_URL || "http://localhost:5173"}/reset-password/${token}`; + + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + const mailOptions = { + to: user.email, + from: process.env.EMAIL_USER, + subject: "Password Reset", + html: `

Click below to reset your password:

${resetLink}`, + }; + + try { + await transporter.sendMail(mailOptions); + res.json({ msg: "Reset link sent to email" }); + } catch (error) { + console.error("Email send failed:", error); + res.status(500).json({ msg: "Failed to send reset email" }); + } +}); + +/** + * Step 2: Reset password with token + * Validates token, hashes new password, updates it. + */ +app.post("/api/reset-password/:token", async (req: Request, res: Response) => { + const { token } = req.params; + const { password } = req.body; + + const user = users.find( + (u) => u.resetPasswordToken === token && u.resetPasswordExpires && u.resetPasswordExpires > new Date() + ); + + if (!user) { + return res.status(400).json({ msg: "Invalid or expired token" }); + } + + user.passwordHash = await bcrypt.hash(password, 10); + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + + res.json({ msg: "Password updated successfully" }); +}); + +// ------------------- ROUTES ------------------- + +app.use("/api/health", healthRoutes); app.use("/api/products", productRoutes); app.use("/api/users", userRoutes); app.use("/api/cart", cartRoutes); +app.use("/api", authRoutes); + +// ------------------- DEFAULT ------------------- + +app.get("/", (_req, res) => { + res.send("API is running"); +}); app.use("/api/users", userRoutes); -// MongoDB connect (optional) +// ------------------- DATABASE ------------------- + const mongoUri = process.env.MONGO_URI; if (mongoUri && (mongoUri.startsWith("mongodb://") || mongoUri.startsWith("mongodb+srv://"))) { mongoose @@ -49,4 +216,4 @@ if (mongoUri && (mongoUri.startsWith("mongodb://") || mongoUri.startsWith("mongo console.warn("MONGO_URI not set or invalid. Skipping MongoDB connection. Set MONGO_URI in .env to enable DB."); } -export default app; \ No newline at end of file +export default app; diff --git a/backend/src/models/user.model.ts b/backend/src/models/user.model.ts index 06ef157..c95e03f 100644 --- a/backend/src/models/user.model.ts +++ b/backend/src/models/user.model.ts @@ -1,4 +1,3 @@ -// src/models/userModel.ts import mongoose, { Document, Schema } from "mongoose"; import bcrypt from "bcryptjs"; @@ -8,6 +7,8 @@ export interface IUser extends Document { password: string; isVerified: boolean; verificationToken?: string; + resetPasswordToken?: string; + resetPasswordExpires?: Date; comparePassword(candidatePassword: string): Promise; } @@ -18,6 +19,8 @@ const userSchema = new Schema( password: { type: String, required: true }, isVerified: { type: Boolean, default: false }, verificationToken: { type: String }, + resetPasswordToken: { type: String }, + resetPasswordExpires: { type: Date }, }, { timestamps: true } ); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts new file mode 100644 index 0000000..c839cf2 --- /dev/null +++ b/backend/src/routes/auth.ts @@ -0,0 +1,74 @@ +import express, { Request, Response } from "express"; +import crypto from "crypto"; +import bcrypt from "bcryptjs"; +import nodemailer from "nodemailer"; +import User from "../models/user.model.js"; + +const router = express.Router(); + +// Request password reset +router.post("/request-reset", async (req: Request, res: Response) => { + const { email } = req.body; + + try { + const user = await User.findOne({ email }); + if (!user) return res.status(404).json({ msg: "User not found" }); + + const token = crypto.randomBytes(32).toString("hex"); + user.resetPasswordToken = token; + user.resetPasswordExpires = new Date(Date.now() + 3600000); // 1 hour + await user.save(); + + const resetLink = `http://localhost:5173/reset-password/${token}`; + + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + const mailOptions = { + to: user.email, + from: process.env.EMAIL_USER, + subject: "Password Reset", + html: `

Click the link to reset your password:

+ ${resetLink}`, + }; + + await transporter.sendMail(mailOptions); + res.json({ msg: "Reset link sent to email" }); + } catch (error) { + console.error(error); + res.status(500).json({ msg: "Server error" }); + } +}); + +// Reset password with token +router.post("/reset-password/:token", async (req: Request, res: Response) => { + const { token } = req.params; + const { password } = req.body; + + try { + const user = await User.findOne({ + resetPasswordToken: token, + resetPasswordExpires: { $gt: new Date() }, + }); + + if (!user) return res.status(400).json({ msg: "Invalid or expired token" }); + + const hashed = await bcrypt.hash(password, 10); + user.password = hashed; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + await user.save(); + + res.json({ msg: "Password updated successfully" }); + } catch (error) { + console.error(error); + res.status(500).json({ msg: "Server error" }); + } +}); + +export default router; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8ab2fb0..2d7fe2e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -38,6 +38,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-query": "^5.90.5", + "axios": "^1.13.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -68,8 +69,9 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react-swc": "^4.1.0", "eslint": "^9.36.0", @@ -4169,6 +4171,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4209,6 +4218,29 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", @@ -4779,6 +4811,23 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4885,6 +4934,19 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5030,6 +5092,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5289,6 +5363,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5352,6 +5435,20 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.237", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", @@ -5427,6 +5524,24 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -5434,6 +5549,33 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5811,6 +5953,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/framer-motion": { "version": "12.23.24", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", @@ -5866,6 +6044,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5876,6 +6063,30 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -5885,6 +6096,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5931,6 +6155,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5954,6 +6190,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -6632,6 +6907,15 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", @@ -6661,6 +6945,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -7075,6 +7380,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index eca8891..9713c5d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-query": "^5.90.5", + "axios": "^1.13.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -71,8 +72,9 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react-swc": "^4.1.0", "eslint": "^9.36.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d43d4b1..0dda5c7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,8 @@ import { CartProvider } from "./components/CartContext"; import Cart from "./pages/Cart"; import Payment from "./pages/Payment"; import Dashboard from "./pages/Dashboard"; +import RequestReset from "./pages/RequestReset"; +import ResetPassword from "./pages/ResetPassword"; const queryClient = new QueryClient(); @@ -20,24 +22,29 @@ const App = () => ( - - - -
+ + +
+ } /> } /> } /> } /> } /> + } /> } /> } /> } /> } /> + + {/* Added password reset routes */} + } /> + } /> -
-
- + +
+
); diff --git a/frontend/src/pages/RequestReset.tsx b/frontend/src/pages/RequestReset.tsx new file mode 100644 index 0000000..f9fe8f0 --- /dev/null +++ b/frontend/src/pages/RequestReset.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import axios from "axios"; + +export default function RequestReset() { + const [email, setEmail] = useState(""); + const [msg, setMsg] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const res = await axios.post("http://localhost:5000/api/request-reset", { email }); + setMsg(res.data.msg); + } catch { + setMsg("Error sending reset link"); + } + }; + + return ( +
+
+

Forgot Password?

+ setEmail(e.target.value)} + className="border p-2 w-full mb-3" + required + /> + +

{msg}

+
+
+ ); +} diff --git a/frontend/src/pages/ResetPassword.tsx b/frontend/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..c84da8a --- /dev/null +++ b/frontend/src/pages/ResetPassword.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import axios from "axios"; + +export default function ResetPassword() { + const [password, setPassword] = useState(""); + const [msg, setMsg] = useState(""); + const { token } = useParams<{ token: string }>(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const res = await axios.post(`http://localhost:5000/api/reset-password/${token}`, { password }); + setMsg(res.data.msg); + } catch { + setMsg("Invalid or expired token"); + } + }; + + return ( +
+
+

Set New Password

+ setPassword(e.target.value)} + className="border p-2 w-full mb-3" + required + /> + +

{msg}

+
+
+ ); +} diff --git a/frontend/src/pages/Signin.tsx b/frontend/src/pages/Signin.tsx index a64f636..a108767 100644 --- a/frontend/src/pages/Signin.tsx +++ b/frontend/src/pages/Signin.tsx @@ -2,7 +2,14 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { Link } from "react-router-dom"; import { Button } from "../components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "../components/ui/card"; import { InputField } from "../components/InputField"; import { toast } from "../hooks/use-toast"; import { mockLogin, SignInData } from "../lib/api"; @@ -40,7 +47,6 @@ const SignIn = () => { return (
- {/* Background floating blobs */}
@@ -91,10 +97,20 @@ const SignIn = () => { error={errors.password?.message} /> + {/* Forgot password link */} +
+ + Forgot password? + +
+