Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 9 additions & 106 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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<string, string>()


// 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;
Expand Down
102 changes: 102 additions & 0 deletions backend/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
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: `<p>Click <a href="${verificationLink}">here</a> to verify your account.</p>`,
});

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 });
}
};
50 changes: 0 additions & 50 deletions backend/src/controllers/auth.ts

This file was deleted.

49 changes: 18 additions & 31 deletions backend/src/middleware/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -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();
};
}
};
Loading
Loading