diff --git a/backend/package-lock.json b/backend/package-lock.json index e515c19..2f0fb9d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,6 +23,7 @@ "nodemailer": "^7.0.10", "nodemon": "^3.1.10", "uuid": "^13.0.0", + "zod": "^4.1.12" "winston": "^3.18.3" }, "devDependencies": { @@ -4068,6 +4069,15 @@ "engines": { "node": ">=6" } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/backend/package.json b/backend/package.json index e4301a7..aa49157 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,7 @@ "nodemailer": "^7.0.10", "nodemon": "^3.1.10", "uuid": "^13.0.0", + "zod": "^4.1.12" "winston": "^3.18.3" }, "devDependencies": { diff --git a/backend/src/middleware/auth.middleware.ts b/backend/src/middleware/auth.middleware.ts deleted file mode 100644 index 3dfa244..0000000 --- a/backend/src/middleware/auth.middleware.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import jwt from "jsonwebtoken"; - -const SECRET_KEY = process.env.JWT_SECRET || "your_secret_key"; - -export const verifyAuth = (req: Request, res: Response, next: NextFunction) => { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const token = authHeader.split(" ")[1]; - try { - const decoded = jwt.verify(token, SECRET_KEY); - (req as any).user = decoded; - next(); - } catch (error) { - return res.status(401).json({ message: "Invalid token" }); - } -}; diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts new file mode 100644 index 0000000..40fdd3b --- /dev/null +++ b/backend/src/middleware/validateRequest.ts @@ -0,0 +1,24 @@ +// src/middleware/validateRequest.ts +import { ZodObject } from "zod"; +import { Request, Response, NextFunction } from "express"; + +const validateRequest = + (schema: ZodObject) => + (req: Request, res: Response, next: NextFunction) => { + try { + schema.parse({ + body: req.body, + query: req.query, + params: req.params, + }); + next(); + } catch (err) { + return res.status(400).json({ + message: "Validation failed", + err + // errors: err.errors.map((e: any) => e.message), + }); + } + }; + +export default validateRequest; diff --git a/backend/src/routes/cartRoutes.ts b/backend/src/routes/cartRoutes.ts index cadbe92..7742644 100644 --- a/backend/src/routes/cartRoutes.ts +++ b/backend/src/routes/cartRoutes.ts @@ -6,15 +6,23 @@ import { getCart, checkout, } from "../controllers/cart.controller"; -import { authenticate } from "../middleware/authMiddleware"; +import { protect } from "../middleware/authMiddleware"; +import validateRequest from "../middleware/validateRequest"; +import { + addItemSchema, + removeItemSchema, + updateQuantitySchema, + getCartSchema, + checkoutSchema, +} from "../validations/cartValidation"; const router: Router = express.Router(); //Protected routes -router.post("/", authenticate, addItem); -router.patch("/:itemId", authenticate, updateQuantity); -router.delete("/:itemId", authenticate, removeItem); -router.get("/", authenticate, getCart); -router.post("/checkout", authenticate, checkout); +router.post("/add", protect, validateRequest(addItemSchema), addItem); +router.patch("/:itemId", protect, validateRequest(updateQuantitySchema), updateQuantity); +router.delete("/:itemId", protect, removeItem); +router.get("/", protect, getCart); +router.post("/checkout", protect, validateRequest(checkoutSchema), checkout); export default router; \ No newline at end of file diff --git a/backend/src/routes/product.routes.ts b/backend/src/routes/product.routes.ts index bdbdcc0..a0be9f5 100644 --- a/backend/src/routes/product.routes.ts +++ b/backend/src/routes/product.routes.ts @@ -7,16 +7,22 @@ import { deleteProduct, } from "../controllers/product.controller"; +import { + createProductSchema, + updateProductSchema, + getProductByIdSchema, + deleteProductSchema, + getProductsSchema, +} from "../validations/product.validation"; +import validateRequest from "../middleware/validateRequest"; const router = Router(); -// router.post("/", verifyAuth, createProduct); -router.post("/", createProduct); -router.get("/", getProducts); -router.get("/:id", getProductById); -// router.put("/:id", verifyAuth, updateProduct); -router.put("/:id", updateProduct); -// router.delete("/:id", verifyAuth, deleteProduct); -router.delete("/:id", deleteProduct); + +router.post("/", validateRequest(createProductSchema), createProduct); +router.get("/", validateRequest(getProductsSchema), getProducts); +router.get("/:id", validateRequest(getProductByIdSchema), getProductById); +router.put("/:id", validateRequest(updateProductSchema), updateProduct); +router.delete("/:id", validateRequest(deleteProductSchema), deleteProduct); export default router; diff --git a/backend/src/routes/user.routes.ts b/backend/src/routes/user.routes.ts index 361e465..2c27a59 100644 --- a/backend/src/routes/user.routes.ts +++ b/backend/src/routes/user.routes.ts @@ -2,12 +2,17 @@ import express from "express"; import { register, verifyEmail, login, getMe } from "../controllers/auth.controller"; import { protect } from "../middleware/authMiddleware"; - +import validateRequest from "../middleware/validateRequest"; +import { + registerSchema, + loginSchema, + verifyEmailSchema, +} from "../validations/userValidation"; const router = express.Router(); -router.post("/register", register); -router.get("/verify/:token", verifyEmail); -router.post("/login", login); +router.post("/register", validateRequest(registerSchema), register); +router.get("/verify/:token", validateRequest(verifyEmailSchema), verifyEmail); +router.post("/login", validateRequest(loginSchema), login); router.get("/me", protect, getMe); export default router; diff --git a/backend/src/validations/cartValidation.ts b/backend/src/validations/cartValidation.ts new file mode 100644 index 0000000..0d5e7c6 --- /dev/null +++ b/backend/src/validations/cartValidation.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; + +export const addItemSchema = z.object({ + body: z.object({ + productId: z.string().min(1, "Product ID is required"), + name: z.string().min(2, "Product name is required"), + price: z.number().positive("Price must be positive"), + quantity: z.number().int().positive("Quantity must be a positive integer"), + }), +}); + +export const removeItemSchema = z.object({ + params: z.object({ + itemId: z.string().min(1, "Item ID is required"), + }), +}); + +export const updateQuantitySchema = z.object({ + params: z.object({ + itemId: z.string().min(1, "Item ID is required"), + }), + body: z.object({ + quantity: z.number().int().positive("Quantity must be a positive integer"), + }), +}); + +export const getCartSchema = z.object({}); + +export const checkoutSchema = z.object({}); diff --git a/backend/src/validations/product.validation.ts b/backend/src/validations/product.validation.ts new file mode 100644 index 0000000..33ab6bd --- /dev/null +++ b/backend/src/validations/product.validation.ts @@ -0,0 +1,47 @@ +// src/validations/product.validation.ts +import { z } from "zod"; + +export const createProductSchema = z.object({ + body: z.object({ + name: z.string().min(1, "Name is required"), + description: z.string().min(1, "Description is required"), + price: z.number().positive(), + category: z.string().optional(), + stock: z.number().optional(), + }), +}); + +export const updateProductSchema = z.object({ + params: z.object({ + id: z.string(), + }), + body: z.object({ + name: z.string().optional(), + description: z.string().optional(), + price: z.number().optional(), + category: z.string().optional(), + stock: z.number().optional(), + }), +}); + +export const getProductByIdSchema = z.object({ + params: z.object({ + id: z.string(), + }), +}); + +export const deleteProductSchema = z.object({ + params: z.object({ + id: z.string(), + }), +}); + +export const getProductsSchema = z.object({ + query: z.object({ + search: z.string().optional(), + category: z.string().optional(), + minPrice: z.string().optional(), + maxPrice: z.string().optional(), + sort: z.enum(["asc", "desc", "lowToHigh", "highToLow"]).optional(), + }), +}); diff --git a/backend/src/validations/userValidation.ts b/backend/src/validations/userValidation.ts new file mode 100644 index 0000000..c76df24 --- /dev/null +++ b/backend/src/validations/userValidation.ts @@ -0,0 +1,32 @@ +// src/validation/userValidation.ts +import { z } from "zod"; + +export const registerSchema = z.object({ + body: z.object({ + name: z + .string() + .min(2, "Name must be at least 2 characters long") + .max(50, "Name too long"), + email: z + .string() + .email("Invalid email address") + .regex(/\.edu$/, "Email must be a college email (.edu)"), + password: z + .string() + .min(6, "Password must be at least 6 characters long") + .max(100, "Password too long"), + }), +}); + +export const loginSchema = z.object({ + body: z.object({ + email: z.string().email("Invalid email"), + password: z.string().min(6, "Password must be at least 6 characters long"), + }), +}); + +export const verifyEmailSchema = z.object({ + params: z.object({ + token: z.string().min(10, "Invalid verification token"), + }), +}); diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 1477e1f..00cde20 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -1,12 +1,19 @@ { "compilerOptions": { + "ignoreDeprecations": "6.0", "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "module": "ESNext", "skipLibCheck": true, - "types": ["vitest", "node"], - + "types": [ + "vitest", + "node" + ], /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, @@ -14,18 +21,20 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": false, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitAny": false, "noFallthroughCasesInSwitch": false, - "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file