Merge pull request #1 from andwati/dev

Dev
This commit is contained in:
2026-01-14 11:28:50 +03:00
committed by GitHub
15 changed files with 243 additions and 16 deletions
+2 -1
View File
@@ -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": "",
+7 -1
View File
@@ -4,9 +4,11 @@ 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";
import userRoutes from "./routes/user.routes.js";
import docsRoutes from "./routes/docs.routes.js";
const app: Application = express();
@@ -14,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) => {
@@ -25,6 +30,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);
+2 -2
View File
@@ -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) {
+13
View File
@@ -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);
}
}
}
+4 -2
View File
@@ -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",
+15 -2
View File
@@ -1,16 +1,29 @@
import type { ZodSchema } from "zod";
import type { Request, Response, NextFunction } from "express";
interface ParsedRequest {
body?: Record<string, unknown>;
params?: Record<string, string>;
query?: Record<string, unknown>;
}
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<string, string>;
next();
} catch (err) {
next(err);
}
};
+4
View File
@@ -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<UserDocument[]> {
return UserModel.find().select("-passwordHash");
}
static findByEmail(email: string): Promise<UserDocument | null> {
return UserModel.findOne({ email });
}
+31 -1
View File
@@ -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",
+1 -1
View File
@@ -44,7 +44,7 @@ const router: Router = Router();
router.post(
"/",
authMiddleware,
requireRole("customer"),
requireRole(["customer", "admin"]),
validate(createOrderSchema),
OrderController.create,
);
+53
View File
@@ -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;
+57
View File
@@ -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();
+3 -2
View File
@@ -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 };
+14
View File
@@ -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,
}));
}
}
+36 -4
View File
@@ -1,13 +1,33 @@
type LogMeta = Record<string, unknown>;
function format(level: string, message: string, meta?: LogMeta) {
type LogLevel = "debug" | "info" | "warn" | "error" | "http";
const LOG_COLORS: Record<LogLevel, string> = {
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());
},
},
};
+1
View File
@@ -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"),
}),
});