Skip to content

Commit a49a649

Browse files
committed
feat: image upload via firebase
1 parent d4f6a09 commit a49a649

File tree

15 files changed

+1353
-164
lines changed

15 files changed

+1353
-164
lines changed

backend/.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
PORT=5000
2+
MONGO_URI=mongodb://localhost:27017/uniloot
3+
4+
# JWT configuration
5+
JWT_SECRET=supersecretkey123
6+
JWT_REFRESH_SECRET=anothersecretkey456
7+
8+
# Optional
9+
NODE_ENV=development
10+
FIREBASE_STORAGE_BUCKET=get-from-firebaseconsole
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"get it from your firebase console to use store image feature"
3+
}

backend/src/controllers/auth.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import jwt, { SignOptions, JwtPayload } from "jsonwebtoken";
2+
import dotenv from "dotenv";
3+
dotenv.config();
4+
5+
if (!process.env.JWT_SECRET || !process.env.JWT_REFRESH_SECRET) {
6+
throw new Error("JWT secrets missing in environment variables");
7+
}
8+
const accessSecret = process.env.JWT_SECRET as string;
9+
const refreshSecret = process.env.JWT_REFRESH_SECRET as string;
10+
11+
export interface TokenPayload extends JwtPayload {
12+
sub: string;
13+
username?: string;
14+
role?: string;
15+
}
16+
export function signAccessToken(payload: TokenPayload) {
17+
const options: SignOptions = { expiresIn: process.env.ACCESS_TOKEN_EXPIRY as any };
18+
19+
const cleanPayload = { ...payload };
20+
delete cleanPayload.iat;
21+
delete cleanPayload.exp;
22+
23+
return jwt.sign(cleanPayload, accessSecret, options);
24+
}
25+
export function signRefreshToken(payload: TokenPayload) {
26+
const options: SignOptions = { expiresIn: process.env.REFRESH_TOKEN_EXPIRY as any };
27+
28+
const cleanPayload = { ...payload };
29+
delete cleanPayload.iat;
30+
delete cleanPayload.exp;
31+
32+
return jwt.sign(cleanPayload, refreshSecret, options);
33+
}
34+
export function verifyAccessToken(token: string): TokenPayload {
35+
return jwt.verify(token, accessSecret) as TokenPayload;
36+
}
37+
export function verifyRefreshToken(token: string): TokenPayload {
38+
return jwt.verify(token, refreshSecret) as TokenPayload;
39+
}

backend/src/controllers/product.controller.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,26 @@ import Product from "../models/product.model";
33

44
export const createProduct = async (req: Request, res: Response) => {
55
try {
6-
const { name, description, price, category, stock } = req.body;
6+
const { name, description, price, category, stock, imageUrl, condition } = req.body;
77

8-
if (!name || !description || !price) {
9-
return res.status(400).json({ message: "Missing required fields" });
8+
if (!name || !description || !price || !category || !stock) {
9+
return res.status(400).json({ error: "Missing required field(s)" });
1010
}
1111

12-
const product = await Product.create({
12+
const product = new Product({
1313
name,
1414
description,
1515
price,
1616
category,
1717
stock,
18+
imageUrl,
19+
condition,
1820
});
19-
return res.status(201).json(product);
20-
} catch (error) {
21-
console.error("Create Product Error:", error);
22-
res.status(500).json({ message: "Server error" });
21+
22+
await product.save();
23+
res.status(201).json(product);
24+
} catch (err: any) {
25+
res.status(500).json({ error: err.message || "Server error" });
2326
}
2427
};
2528

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
1-
// src/middlewares/authMiddleware.ts
21
import { Request, Response, NextFunction } from "express";
32
import jwt from "jsonwebtoken";
4-
import { User } from "../models/user.model";
3+
const SECRET_KEY = process.env.JWT_SECRET || "your_secret_key";
54

6-
const JWT_SECRET = process.env.JWT_SECRET || "secretkey";
7-
8-
export interface AuthRequest extends Request {
5+
export interface AuthenticatedRequest extends Request {
96
user?: any;
107
}
118

12-
export const protect = async (req: AuthRequest, res: Response, next: NextFunction) => {
13-
let token;
14-
15-
if (req.headers.authorization && req.headers.authorization.startsWith("Bearer")) {
16-
token = req.headers.authorization.split(" ")[1];
9+
export const authenticate = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
10+
const authHeader = req.headers.authorization;
11+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
12+
return res.status(401).json({ message: "Unauthorized" });
1713
}
18-
19-
if (!token) return res.status(401).json({ message: "Not authorized, no token" });
20-
14+
const token = authHeader.split(" ")[1];
2115
try {
22-
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
23-
req.user = await User.findById(decoded.id).select("-password");
16+
const decoded = jwt.verify(token, SECRET_KEY);
17+
req.user = decoded;
2418
next();
2519
} catch (error) {
26-
res.status(401).json({ message: "Not authorized, token failed" });
20+
return res.status(401).json({ message: "Invalid token" });
2721
}
2822
};
23+
24+
// For legacy support in /api/users route expecting 'protect'
25+
export const protect = authenticate;
26+
27+
// Role-based guard
28+
export const authorizeRole = (role: string) => {
29+
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
30+
if (!req.user) {
31+
return res.status(401).json({ error: 'Not authenticated' });
32+
}
33+
if (req.user.role !== role) {
34+
return res.status(403).json({ error: 'Forbidden' });
35+
}
36+
next();
37+
};
38+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import multer from "multer";
2+
3+
const upload = multer({
4+
storage: multer.memoryStorage(),
5+
limits: { fileSize: 2 * 1024 * 1024 },
6+
fileFilter: (_req: any, file: { mimetype: string; }, cb: (arg0: Error | null, arg1: boolean | undefined) => void) => {
7+
if (
8+
file.mimetype === "image/jpeg" ||
9+
file.mimetype === "image/png" ||
10+
file.mimetype === "image/webp"
11+
) {
12+
cb(null, true);
13+
} else {
14+
cb(new Error("Invalid file type. Only JPEG, PNG, WEBP allowed."));
15+
}
16+
},
17+
});
18+
19+
export default upload;

backend/src/models/product.model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface IProduct extends Document {
66
price: number;
77
category?: string;
88
stock: number;
9+
imageUrl?: string;
910
createdAt: Date;
1011
updatedAt: Date;
1112
}
@@ -17,8 +18,9 @@ const productSchema = new Schema<IProduct>(
1718
price: { type: Number, required: true },
1819
category: { type: String },
1920
stock: { type: Number, default: 0 },
21+
imageUrl: { type: String },
2022
},
2123
{ timestamps: true }
2224
);
2325

24-
export default mongoose.model<IProduct>("Product", productSchema);
26+
export default mongoose.model<IProduct>("Product", productSchema);

frontend/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ yarn-debug.log*
66
yarn-error.log*
77
pnpm-debug.log*
88
lerna-debug.log*
9-
9+
.env
1010
node_modules
1111
dist
1212
dist-ssr

0 commit comments

Comments
 (0)