diff --git a/backend/package-lock.json b/backend/package-lock.json index 89efb5b..d03e198 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1512,6 +1512,7 @@ "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -1580,6 +1581,7 @@ "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -3652,6 +3654,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/backend/src/app.ts b/backend/src/app.ts index 87d04a0..1961eae 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,21 +1,17 @@ -import express, { Application, Request, Response } from 'express'; +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"; - - -// Consolidated Imports import productRoutes from "./routes/product.routes"; -import cartRoutes from "./routes/cartRoutes"; +import cartRoutes from "./routes/cartRoutes"; import mongoose from "mongoose"; -import bcrypt from "bcryptjs"; + import cookieParser from "cookie-parser"; -import { signAccessToken, signRefreshToken, verifyRefreshToken } from "./controllers/auth"; -import { authenticate, AuthRequest } from "./middleware/authMiddleware"; -const app: Application = express(); +const app = express(); + import dotenv from "dotenv"; dotenv.config(); @@ -28,112 +24,19 @@ app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser()); -// 3. User Interface and Mock Data -interface User { - id: string; - username: string; - passwordHash: string; - role: string; -} - -const users: User[] = [ - { id: "1", username: "alice", passwordHash: bcrypt.hashSync("password", 8), role: "admin" }, -]; - -const refreshTokens = new Map() - - -// 4. Authentication API 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); - refreshTokens.set(user.id, refreshToken); - res.cookie("refreshToken", refreshToken, { - httpOnly: true, - sameSite: "strict", - secure: process.env.NODE_ENV === "production", - maxAge: 7 * 24 * 60 * 60 * 1000, - }); +// 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, - }); - - 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) => { - // req.user is guaranteed to exist by the 'authenticate' middleware - 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 }); -}); // Existing API Routes app.use('/api/health', healthRoutes); app.use("/api/products", productRoutes); app.use("/api/users", userRoutes); app.use("/api/cart", cartRoutes); - -// Default -app.get("/", (_req, res) => { - res.send("API is running "); -}); +app.use("/api/users", userRoutes); // MongoDB connect (optional) const mongoUri = process.env.MONGO_URI; diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts new file mode 100644 index 0000000..f6c88df --- /dev/null +++ b/backend/src/controllers/auth.controller.ts @@ -0,0 +1,102 @@ +// src/controllers/authController.ts +import { Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import crypto from "crypto"; +import nodemailer from "nodemailer"; +import { User } from "../models/user.model"; + +const JWT_SECRET = process.env.JWT_SECRET || "secretkey"; +const CLIENT_URL = process.env.CLIENT_URL || "http://localhost:3000"; + +const transporter = nodemailer.createTransport({ + // service: "gmail", + // auth: { + // user: process.env.EMAIL_USER, + // pass: process.env.EMAIL_PASS, + // }, + host: 'smtp.ethereal.email', + port: 587, + auth: { + user: 'kiarra.dietrich11@ethereal.email', + pass: 'Du8rSm184HzwwHsHYm' + } +}); + +const generateToken = (id: string) => { + return jwt.sign({ id }, JWT_SECRET, { expiresIn: "7d" }); +}; + +// Register user +export const register = async (req: Request, res: Response) => { + try { + const { name, email, password } = req.body; + const existingUser = await User.findOne({ email }); + if (existingUser) return res.status(400).json({ message: "User already exists" }); + + const verificationToken = crypto.randomBytes(20).toString("hex"); + const user = await User.create({ name, email, password, verificationToken }); + + const verificationLink = `${CLIENT_URL}/verify/${verificationToken}`; + await transporter.sendMail({ + to: email, + subject: "Verify your email", + html: `

Click here to verify your account.

`, + }); + + res.status(201).json({ message: "User registered. Check your email for verification link." }); + } catch (error) { + res.status(500).json({ message: "Registration failed", error }); + } +}; + +// Verify email +export const verifyEmail = async (req: Request, res: Response) => { + try { + const { token } = req.params; + const user = await User.findOne({ verificationToken: token }); + + if (!user) return res.status(400).json({ message: "Invalid or expired token" }); + + user.isVerified = true; + user.verificationToken = undefined; + await user.save(); + + res.status(200).json({ message: "Email verified successfully" }); + } catch (error) { + res.status(500).json({ message: "Verification failed", error }); + } +}; + +// Login +export const login = async (req: Request, res: Response) => { + try { + const { email, password } = req.body; + const user = await User.findOne({ email }); + + if (!user) return res.status(400).json({ message: "Invalid credentials" }); + if (!user.isVerified) return res.status(403).json({ message: "Please verify your email" }); + + const isMatch = await user.comparePassword(password); + if (!isMatch) return res.status(400).json({ message: "Invalid credentials" }); + + const token = generateToken(String(user._id)); + res.status(200).json({ token, user: { name: user.name, email: user.email } }); + } catch (error) { + res.status(500).json({ message: "Login failed", error }); + } +}; + +// Get current user +export const getMe = async (req: Request, res: Response) => { + try { + const userId = (req as any).user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const user = await User.findById(userId).select("-password"); + if (!user) return res.status(404).json({ message: "User not found" }); + + res.status(200).json(user); + } catch (error) { + res.status(500).json({ message: "Error fetching user", error }); + } +}; diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts deleted file mode 100644 index bd6f4de..0000000 --- a/backend/src/controllers/auth.ts +++ /dev/null @@ -1,50 +0,0 @@ -import jwt, { SignOptions, JwtPayload } from "jsonwebtoken"; -import dotenv from "dotenv"; -dotenv.config(); - -if (!process.env.JWT_SECRET || !process.env.JWT_REFRESH_SECRET) { - throw new Error("JWT secrets missing in environment variables"); -} - -const accessSecret = process.env.JWT_SECRET as string; -const refreshSecret = process.env.JWT_REFRESH_SECRET as string; - -// Define your payload type -export interface TokenPayload extends JwtPayload { - sub: string; - username?: string; - role?: string; - //role?:"user"|"admin"; // Example roles -} - -export function signAccessToken(payload: TokenPayload) { - const options: SignOptions = { expiresIn: process.env.ACCESS_TOKEN_EXPIRY as any }; - - // --- CRITICAL FIX: Create a clean payload copy --- - const cleanPayload = { ...payload }; - delete cleanPayload.iat; // Remove "Issued At" claim - delete cleanPayload.exp; // Remove "Expiration" claim to allow options.expiresIn to work - // --- END FIX --- - - return jwt.sign(cleanPayload, accessSecret, options); -} - -export function signRefreshToken(payload: TokenPayload) { - const options: SignOptions = { expiresIn: process.env.REFRESH_TOKEN_EXPIRY as any }; - - // --- CRITICAL FIX: Create a clean payload copy --- - const cleanPayload = { ...payload }; - delete cleanPayload.iat; // Remove "Issued At" claim - delete cleanPayload.exp; // Remove "Expiration" claim to allow options.expiresIn to work - // --- END FIX --- - - return jwt.sign(cleanPayload, refreshSecret, options); -} - -export function verifyAccessToken(token: string): TokenPayload { - return jwt.verify(token, accessSecret) as TokenPayload; -} - -export function verifyRefreshToken(token: string): TokenPayload { - return jwt.verify(token, refreshSecret) as TokenPayload; -} \ No newline at end of file diff --git a/backend/src/middleware/authMiddleware.ts b/backend/src/middleware/authMiddleware.ts index 9368ec5..7daa66b 100644 --- a/backend/src/middleware/authMiddleware.ts +++ b/backend/src/middleware/authMiddleware.ts @@ -1,41 +1,28 @@ -import { Request, Response, NextFunction } from 'express'; -import { verifyAccessToken } from '../controllers/auth'; +// src/middlewares/authMiddleware.ts +import { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; +import { User } from "../models/user.model"; -export interface AuthenticatedRequest extends Request { +const JWT_SECRET = process.env.JWT_SECRET || "secretkey"; + +export interface AuthRequest extends Request { user?: any; } -export type AuthRequest = AuthenticatedRequest; - -// (Middleware file - authMiddleware.ts) +export const protect = async (req: AuthRequest, res: Response, next: NextFunction) => { + let token; -export function authenticate(req: AuthenticatedRequest, res: Response, next: NextFunction) { - const header = req.headers.authorization; - const token = header?.startsWith('Bearer ') ? header.slice(7) : undefined; - - if (!token) { - // Use 401 for NO credentials (token missing entirely) - return res.status(401).json({ error: 'No token provided' }); + if (req.headers.authorization && req.headers.authorization.startsWith("Bearer")) { + token = req.headers.authorization.split(" ")[1]; } + if (!token) return res.status(401).json({ message: "Not authorized, no token" }); + try { - const payload = verifyAccessToken(token); - req.user = payload; + const decoded = jwt.verify(token, JWT_SECRET) as { id: string }; + req.user = await User.findById(decoded.id).select("-password"); next(); - } catch { - // Use 403 for INVALID credentials (token present but invalid/expired/rejected) - // This is often seen as a better status for expired tokens. - return res.status(403).json({ error: 'Invalid or expired token' }); + } catch (error) { + res.status(401).json({ message: "Not authorized, token failed" }); } -} -export function authorizeRole(role: string) { - return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - if (!req.user) { - return res.status(401).json({ error: 'Not authenticated' }); - } - if (req.user.role !== role) { - return res.status(403).json({ error: 'Forbidden' }); - } - next(); - }; -} +}; diff --git a/backend/src/models/user.model.ts b/backend/src/models/user.model.ts new file mode 100644 index 0000000..06ef157 --- /dev/null +++ b/backend/src/models/user.model.ts @@ -0,0 +1,36 @@ +// src/models/userModel.ts +import mongoose, { Document, Schema } from "mongoose"; +import bcrypt from "bcryptjs"; + +export interface IUser extends Document { + name: string; + email: string; + password: string; + isVerified: boolean; + verificationToken?: string; + comparePassword(candidatePassword: string): Promise; +} + +const userSchema = new Schema( + { + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + isVerified: { type: Boolean, default: false }, + verificationToken: { type: String }, + }, + { timestamps: true } +); + +userSchema.pre("save", async function (next) { + if (!this.isModified("password")) return next(); + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); + next(); +}); + +userSchema.methods.comparePassword = async function (candidatePassword: string) { + return bcrypt.compare(candidatePassword, this.password); +}; + +export const User = mongoose.model("User", userSchema); diff --git a/backend/src/routes/product.routes.ts b/backend/src/routes/product.routes.ts index 98e69ca..bdbdcc0 100644 --- a/backend/src/routes/product.routes.ts +++ b/backend/src/routes/product.routes.ts @@ -6,7 +6,6 @@ import { updateProduct, deleteProduct, } from "../controllers/product.controller"; -// import { verifyAuth } from "../middleware/auth.middleware"; const router = Router(); diff --git a/backend/src/routes/user.routes.ts b/backend/src/routes/user.routes.ts index dacb028..361e465 100644 --- a/backend/src/routes/user.routes.ts +++ b/backend/src/routes/user.routes.ts @@ -1,111 +1,13 @@ -import express, { Request, Response } from "express"; -import bcrypt from "bcryptjs"; -import nodemailer from "nodemailer"; -const { v4: uuidv4 } = require("uuid"); - -interface User { - id: string; - username: string; - email: string; - passwordHash: string; - verified: boolean; - verificationToken?: string; -} - -const users: User[] = []; - -const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST || "smtp.ethereal.email", - port: Number(process.env.SMTP_PORT) || 587, - auth: { - user: process.env.SMTP_USER || "test@ethereal.email", - pass: process.env.SMTP_PASS || "password", - }, -}); +// src/routes/userRoutes.ts +import express from "express"; +import { register, verifyEmail, login, getMe } from "../controllers/auth.controller"; +import { protect } from "../middleware/authMiddleware"; const router = express.Router(); -router.post("/register", async (req: Request, res: Response) => { - const { username, email, password } = req.body; - if (!username || !email || !password) - return res.status(400).json({ error: "All fields are required" }); - - const existingUser = users.find( - (u) => u.email.toLowerCase() === email.toLowerCase() - ); - if (existingUser) - return res.status(400).json({ error: "User already exists" }); - - const passwordHash = await bcrypt.hash(password, 10); - const verificationToken = uuidv4(); - - const newUser: User = { - id: uuidv4(), - username, - email, - passwordHash, - verified: false, - verificationToken, - }; - - users.push(newUser); - - const verificationUrl = `${process.env.FRONTEND_URL || "http://localhost:4000"}/api/users/verify-email?token=${verificationToken}`; - - try { - await transporter.sendMail({ - from: `"Support" `, - to: email, - subject: "Verify your email", - text: `Click the following link to verify your email: ${verificationUrl}`, - html: `

Click here to verify your email.

`, - }); - - res.status(201).json({ - message: "User registered. Verification email sent.", - }); - } catch (err) { - console.error(err); - res.status(500).json({ error: "Error sending verification email" }); - } -}); - -router.get("/verify-email", (req: Request, res: Response) => { - const token = req.query.token as string; - if (!token) return res.status(400).json({ error: "Token missing" }); - - const user = users.find((u) => u.verificationToken === token); - if (!user) return res.status(400).json({ error: "Invalid or expired token" }); - - user.verified = true; - delete user.verificationToken; - - res.json({ message: "Email verified successfully. You can now log in." }); -}); - -router.post("/resend-verification", async (req: Request, res: Response) => { - const { email } = req.body; - const user = users.find((u) => u.email.toLowerCase() === email.toLowerCase()); - - if (!user) return res.status(404).json({ error: "User not found" }); - if (user.verified) - return res.status(400).json({ error: "User already verified" }); - - user.verificationToken = uuidv4(); - const verificationUrl = `${process.env.FRONTEND_URL || "http://localhost:4000"}/api/users/verify-email?token=${user.verificationToken}`; - - try { - await transporter.sendMail({ - from: `"Support" `, - to: user.email, - subject: "Verify your email", - html: `

Click here to verify your email.

`, - }); - res.json({ message: "Verification email resent." }); - } catch (err) { - console.error(err); - res.status(500).json({ error: "Failed to resend email" }); - } -}); +router.post("/register", register); +router.get("/verify/:token", verifyEmail); +router.post("/login", login); +router.get("/me", protect, getMe); export default router; diff --git a/backend/src/server.ts b/backend/src/server.ts index e359b33..b01cd61 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -2,9 +2,9 @@ import dotenv from "dotenv"; dotenv.config(); -import app from './app'; +import app from './app'; -const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; +const PORT = process.env.PORT ? Number(process.env.PORT) : 5000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`);