mirror of
https://github.com/andwati/order-service.git
synced 2026-10-26 05:42:17 +00:00
+2
-1
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -44,7 +44,7 @@ const router: Router = Router();
|
||||
router.post(
|
||||
"/",
|
||||
authMiddleware,
|
||||
requireRole("customer"),
|
||||
requireRole(["customer", "admin"]),
|
||||
validate(createOrderSchema),
|
||||
OrderController.create,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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());
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user