Skip to content
Merged
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
17 changes: 17 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

# --- JWT Configuration ---
# Secret key for signing access tokens. Should be a long, random string.
JWT_SECRET=your_jwt_secret_key_here

# Secret key for signing refresh tokens. Should be a long, random string, different from JWT_SECRET.
JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here

# Access token expiration time (e.g., 10m, 1h, 1d)
ACCESS_TOKEN_EXPIRY=10m

# Refresh token expiration time (e.g., 7d, 30d)
REFRESH_TOKEN_EXPIRY=7d

# --- Server Configuration ---
# Port for the server to run on
PORT=4000
69 changes: 61 additions & 8 deletions backend/package-lock.json

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

17 changes: 11 additions & 6 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,27 @@
"author": "",
"license": "MIT",
"dependencies": {
"bcryptjs": "^3.0.2",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"dotenv": "^16.6.1",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.19.1",
"mongoose": "^8.19.2",
"morgan": "^1.10.0",
"nodemon": "^3.1.10"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/body-parser": "^1.19.2",
"@types/cookie-parser": "^1.4.9",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.21",
"@types/express": "^4.17.23",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.7",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.2.2"
"typescript": "^5.9.3"
}
}
}
112 changes: 109 additions & 3 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,132 @@
import express, { Application } from 'express';
import express, { Application, Request, Response } from 'express';
import cors from 'cors';
import morgan from 'morgan';
import bodyParser from 'body-parser';
import healthRoutes from './routes/healthRoutes';

// Consolidated Imports
import productRoutes from "./routes/product.routes";
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();
import dotenv from "dotenv";
dotenv.config();

// Middleware setup
app.use(cors());
app.use(morgan('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Middleware needed for Auth
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,
});

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 });
});

// API Routes
// Existing API Routes
app.use('/api/health', healthRoutes);
app.use("/api/products", productRoutes);
// Default
app.get("/", (_req, res) => {
res.send("API is running 🚀");
res.send("API is running ");
});

// MongoDB connect (optional)
Expand Down
50 changes: 50 additions & 0 deletions backend/src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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;
}
Loading
Loading