From d2334181a06b924fbdb43f5ea6f083e2f9aa4449 Mon Sep 17 00:00:00 2001 From: andwati Date: Tue, 13 Jan 2026 17:56:43 +0300 Subject: [PATCH 1/4] added role to register --- src/controllers/auth.controller.ts | 4 ++-- src/services/auth.service.ts | 5 +++-- src/validation/auth.schema.ts | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index e90670a..01fb326 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -4,9 +4,9 @@ import { AuthService } from "../services/auth.service.js"; export class AuthController { static async register(req: Request, res: Response, next: NextFunction) { try { - const { email, password } = req.body; + const { email, password, role } = req.body; - const user = await AuthService.register(email, password); + const user = await AuthService.register(email, password, role); res.status(201).json(user); } catch (err) { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index cd1fe68..730e791 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,9 +1,10 @@ import { UserRepository } from "../repositories/user.repo.js"; import { hashPassword, verifyPassword } from "../utils/password.js"; import { signToken } from "../utils/jwt.js"; +import type { UserRole } from "../models/user.model.js"; export class AuthService { - static async register(email: string, password: string) { + static async register(email: string, password: string, role: UserRole = "customer") { const existing = await UserRepository.findByEmail(email); if (existing) { throw { statusCode: 409, message: "Email already registered" }; @@ -14,7 +15,7 @@ export class AuthService { const user = await UserRepository.create({ email, passwordHash, - role: "customer", + role, }); return { id: user._id, email: user.email, role: user.role }; diff --git a/src/validation/auth.schema.ts b/src/validation/auth.schema.ts index 433bdee..3ea8d0c 100644 --- a/src/validation/auth.schema.ts +++ b/src/validation/auth.schema.ts @@ -4,6 +4,7 @@ export const registerSchema = z.object({ body: z.object({ email: z.string().email(), password: z.string().min(8), + role: z.enum(["customer", "admin"]).default("customer"), }), }); From 8e385887de973d48f3073b373330935ef82016c6 Mon Sep 17 00:00:00 2001 From: andwati Date: Tue, 13 Jan 2026 18:29:46 +0300 Subject: [PATCH 2/4] added get users admin only endpoint --- src/app.ts | 2 + src/controllers/user.controller.ts | 13 +++++++ src/middleware/validate.middleware.ts | 17 ++++++++- src/repositories/user.repo.ts | 4 ++ src/routes/auth.routes.ts | 32 +++++++++++++++- src/routes/user.routes.ts | 53 +++++++++++++++++++++++++++ src/services/user.service.ts | 14 +++++++ 7 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/controllers/user.controller.ts create mode 100644 src/routes/user.routes.ts create mode 100644 src/services/user.service.ts diff --git a/src/app.ts b/src/app.ts index a02c0ea..ae6e069 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,6 +7,7 @@ import { errorHandler } from "./middleware/error.middleware.js"; import authRoutes from "./routes/auth.routes.js"; import productRoutes from "./routes/product.routes.js"; import orderRoutes from "./routes/order.routes.js"; +import userRoutes from "./routes/user.routes.js"; import docsRoutes from "./routes/docs.routes.js"; const app: Application = express(); @@ -25,6 +26,7 @@ app.get("/health", (req, res) => { app.use("/auth", authRoutes); app.use("/products", productRoutes); app.use("/orders", orderRoutes); +app.use("/users", userRoutes); // swagger ui app.use("/docs", docsRoutes); diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 0000000..8a75307 --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,13 @@ +import type { Request, Response, NextFunction } from "express"; +import { UserService } from "../services/user.service.js"; + +export class UserController { + static async getAll(_req: Request, res: Response, next: NextFunction) { + try { + const users = await UserService.getAll(); + res.status(200).json(users); + } catch (err) { + next(err); + } + } +} diff --git a/src/middleware/validate.middleware.ts b/src/middleware/validate.middleware.ts index a7b5a78..4c38d00 100644 --- a/src/middleware/validate.middleware.ts +++ b/src/middleware/validate.middleware.ts @@ -1,16 +1,29 @@ import type { ZodSchema } from "zod"; import type { Request, Response, NextFunction } from "express"; +interface ParsedRequest { + body?: Record; + params?: Record; + query?: Record; +} + export const validate = (schema: ZodSchema) => (req: Request, _res: Response, next: NextFunction) => { try { - schema.parse({ + const parsed = schema.parse({ body: req.body, params: req.params, query: req.query, - }); + }) as ParsedRequest; + + // Update request with parsed values (including defaults) + if (parsed.body) req.body = parsed.body; + if (parsed.params) req.params = parsed.params; + if (parsed.query) req.query = parsed.query as Record; + next(); } catch (err) { next(err); } }; + diff --git a/src/repositories/user.repo.ts b/src/repositories/user.repo.ts index 25cd646..f0f9da0 100644 --- a/src/repositories/user.repo.ts +++ b/src/repositories/user.repo.ts @@ -3,6 +3,10 @@ import { UserModel } from "../models/user.model.js"; import type { UserDocument } from "../models/user.model.js"; export class UserRepository { + static findAll(): Promise { + return UserModel.find().select("-passwordHash"); + } + static findByEmail(email: string): Promise { return UserModel.findOne({ email }); } diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index 26d3e37..38406d5 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -13,22 +13,52 @@ const router: Router = Router(); * tags: * - Auth * summary: Register a new user + * security: [] * requestBody: * required: true * content: * application/json: * schema: * type: object + * required: + * - email + * - password * properties: * email: * type: string + * format: email + * description: User's email address * password: * type: string + * minLength: 8 + * description: User's password (minimum 8 characters) + * role: + * type: string + * enum: [customer, admin] + * default: customer + * description: Account type (defaults to customer) * responses: * 201: - * description: User created + * description: User created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * description: The user ID + * email: + * type: string + * description: The user's email + * role: + * type: string + * enum: [customer, admin] + * description: The user's role * 400: * description: Invalid input + * 409: + * description: Email already registered */ router.post( "/register", diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts new file mode 100644 index 0000000..d841bb2 --- /dev/null +++ b/src/routes/user.routes.ts @@ -0,0 +1,53 @@ +import { Router } from "express"; +import { UserController } from "../controllers/user.controller.js"; +import { authMiddleware } from "../middleware/auth.middleware.js"; +import { requireRole } from "../middleware/role.middleware.js"; + +const router: Router = Router(); + +/** + * @openapi + * /users: + * get: + * tags: + * - Users + * summary: Get all users (Admin only) + * description: Retrieve a list of all registered users. This endpoint is restricted to administrators only. + * security: + * - BearerAuth: [] + * responses: + * 200: + * description: List of users retrieved successfully + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * description: The user ID + * email: + * type: string + * description: The user's email + * role: + * type: string + * enum: [customer, admin] + * description: The user's role + * createdAt: + * type: string + * format: date-time + * description: Account creation date + * updatedAt: + * type: string + * format: date-time + * description: Last update date + * 401: + * description: Unauthorized - Missing or invalid token + * 403: + * description: Forbidden - Admin access required + */ +router.get("/", authMiddleware, requireRole("admin"), UserController.getAll); + +export default router; diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000..1d7836c --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,14 @@ +import { UserRepository } from "../repositories/user.repo.js"; + +export class UserService { + static async getAll() { + const users = await UserRepository.findAll(); + return users.map((user) => ({ + id: user._id, + email: user.email, + role: user.role, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + })); + } +} From b74197f60fd0404ee93fba332d78702280fede36 Mon Sep 17 00:00:00 2001 From: andwati Date: Tue, 13 Jan 2026 18:39:10 +0300 Subject: [PATCH 3/4] logging with morgan setup --- src/app.ts | 6 +++++- src/utils/logger.ts | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index ae6e069..05e134b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,6 +4,7 @@ import cors from "cors"; import morgan from "morgan"; import { errorHandler } from "./middleware/error.middleware.js"; +import { logger } from "./utils/logger.js"; import authRoutes from "./routes/auth.routes.js"; import productRoutes from "./routes/product.routes.js"; import orderRoutes from "./routes/order.routes.js"; @@ -15,7 +16,10 @@ const app: Application = express(); //global middleware app.use(cors()); app.use(express.json()); -app.use(morgan("dev")); + +// HTTP request logging with morgan +const morganFormat = ":method :url :status :res[content-length] - :response-time ms"; +app.use(morgan(morganFormat, { stream: logger.stream })); // health check route app.get("/health", (req, res) => { diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 91894b4..c67156f 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,13 +1,33 @@ type LogMeta = Record; -function format(level: string, message: string, meta?: LogMeta) { +type LogLevel = "debug" | "info" | "warn" | "error" | "http"; + +const LOG_COLORS: Record = { + debug: "\x1b[36m", // cyan + info: "\x1b[32m", // green + warn: "\x1b[33m", // yellow + error: "\x1b[31m", // red + http: "\x1b[35m", // magenta +}; + +const RESET = "\x1b[0m"; + +function format(level: LogLevel, message: string, meta?: LogMeta): string { const timestamp = new Date().toISOString(); - return meta - ? `[${timestamp}] ${level.toUpperCase()} ${message} ${JSON.stringify(meta)}` - : `[${timestamp}] ${level.toUpperCase()} ${message}`; + const color = LOG_COLORS[level]; + const levelStr = `${color}${level.toUpperCase().padEnd(5)}${RESET}`; + + const baseLog = `[${timestamp}] ${levelStr} ${message}`; + return meta ? `${baseLog} ${JSON.stringify(meta)}` : baseLog; } export const logger = { + debug(message: string, meta?: LogMeta) { + if (process.env.NODE_ENV !== "production") { + console.log(format("debug", message, meta)); + } + }, + info(message: string, meta?: LogMeta) { console.log(format("info", message, meta)); }, @@ -19,4 +39,16 @@ export const logger = { error(message: string, meta?: LogMeta) { console.error(format("error", message, meta)); }, + + http(message: string, meta?: LogMeta) { + console.log(format("http", message, meta)); + }, + + // Stream for morgan integration + stream: { + write(message: string) { + // Remove trailing newline from morgan + logger.http(message.trim()); + }, + }, }; From ba21363eae45a9cb4d247949cacffe92737ae004 Mon Sep 17 00:00:00 2001 From: andwati Date: Tue, 13 Jan 2026 19:21:20 +0300 Subject: [PATCH 4/4] fixed 403 errror on order creation --- package.json | 3 +- src/middleware/role.middleware.ts | 6 ++-- src/routes/order.routes.ts | 2 +- src/scripts/seed.ts | 57 +++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 src/scripts/seed.ts diff --git a/package.json b/package.json index 5cbc578..0d7c67d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "prettier": "prettier --check .", - "prettier:fix": "prettier --write ." + "prettier:fix": "prettier --write .", + "seed": "tsc && node dist/scripts/seed.js" }, "keywords": [], "author": "", diff --git a/src/middleware/role.middleware.ts b/src/middleware/role.middleware.ts index 47ac54e..791e0e4 100644 --- a/src/middleware/role.middleware.ts +++ b/src/middleware/role.middleware.ts @@ -1,6 +1,8 @@ import type { Request, Response, NextFunction } from "express"; -export function requireRole(role: "admin" | "customer") { +export function requireRole(roles: "admin" | "customer" | ("admin" | "customer")[]) { + const allowedRoles = Array.isArray(roles) ? roles : [roles]; + return (req: Request, _res: Response, next: NextFunction) => { if (!req.user) { return next({ @@ -9,7 +11,7 @@ export function requireRole(role: "admin" | "customer") { }); } - if (req.user.role !== role) { + if (!allowedRoles.includes(req.user.role as "admin" | "customer")) { return next({ statusCode: 403, message: "Forbidden", diff --git a/src/routes/order.routes.ts b/src/routes/order.routes.ts index 9ee1db6..7d5dd8e 100644 --- a/src/routes/order.routes.ts +++ b/src/routes/order.routes.ts @@ -44,7 +44,7 @@ const router: Router = Router(); router.post( "/", authMiddleware, - requireRole("customer"), + requireRole(["customer", "admin"]), validate(createOrderSchema), OrderController.create, ); diff --git a/src/scripts/seed.ts b/src/scripts/seed.ts new file mode 100644 index 0000000..ece79fb --- /dev/null +++ b/src/scripts/seed.ts @@ -0,0 +1,57 @@ +import { connectDB } from "../config/db.js"; +import { ProductModel } from "../models/product.model.js"; +import mongoose from "mongoose"; +import { logger } from "../utils/logger.js"; + +const PRODUCTS = [ + { + name: "Wireless Mouse", + price: 2500, // $25.00 + stock: 100, + }, + { + name: "Mechanical Keyboard", + price: 12000, // $120.00 + stock: 50, + }, + { + name: "USB-C Hub", + price: 4500, // $45.00 + stock: 75, + }, + { + name: "Monitor Stand", + price: 3500, // $35.00 + stock: 200, + }, + { + name: "Gaming Headset", + price: 8000, // $80.00 + stock: 30, + } +]; + +async function seed() { + try { + logger.info("Connecting to database..."); + await connectDB(); + logger.info("Connected to database."); + + logger.info("Clearing products..."); + await ProductModel.deleteMany({}); + logger.info("Products cleared."); + + logger.info("Seeding products..."); + await ProductModel.insertMany(PRODUCTS); + logger.info("Products seeded successfully."); + + process.exit(0); + } catch (error) { + logger.error("Error seeding database:", { error }); + process.exit(1); + } finally { + await mongoose.disconnect(); + } +} + +seed();