mirror of
https://github.com/andwati/order-service.git
synced 2026-11-10 20:25:11 +00:00
feat: add bcrypt for password hashing and implement logger utility
This commit is contained in:
+4
-1
@@ -18,18 +18,21 @@
|
||||
"license": "ISC",
|
||||
"packageManager": "[email protected]",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"mongoose": "^9.1.3",
|
||||
"morgan": "^1.10.1",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@eslint/json": "^0.14.0",
|
||||
"@eslint/markdown": "^7.5.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
|
||||
Generated
+42
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
bcrypt:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
@@ -29,6 +32,9 @@ importers:
|
||||
uuid:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
zod:
|
||||
specifier: ^4.3.5
|
||||
version: 4.3.5
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.39.2
|
||||
@@ -39,6 +45,9 @@ importers:
|
||||
'@eslint/markdown':
|
||||
specifier: ^7.5.1
|
||||
version: 7.5.1
|
||||
'@types/bcrypt':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@types/cors':
|
||||
specifier: ^2.8.19
|
||||
version: 2.8.19
|
||||
@@ -176,6 +185,9 @@ packages:
|
||||
'@tsconfig/[email protected]':
|
||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
@@ -340,6 +352,10 @@ packages:
|
||||
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1022,6 +1038,14 @@ packages:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1336,6 +1360,9 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
@@ -1446,6 +1473,10 @@ snapshots:
|
||||
|
||||
'@tsconfig/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/node': 25.0.6
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
@@ -1654,6 +1685,11 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
node-addon-api: 8.5.0
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -2514,6 +2550,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
@@ -2829,4 +2869,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- bcrypt
|
||||
+1
-1
@@ -4,7 +4,7 @@ import cors from "cors";
|
||||
import morgan from "morgan";
|
||||
|
||||
import { errorHandler } from "./middleware/error.middleware.js";
|
||||
import { authRoutes } from "./routes/auth.routes.js";
|
||||
import authRoutes from "./routes/auth.routes.js";
|
||||
import productRoutes from "./routes/product.routes.js";
|
||||
import orderRoutes from "./routes/order.routes.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AuthService } from "../services/auth.service";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { AuthService } from "../services/auth.service.js";
|
||||
|
||||
export class AuthController {
|
||||
static async register(req: Request, res: Response, next: NextFunction) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { OrderService } from "../services/order.service";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { OrderService } from "../services/order.service.js";
|
||||
|
||||
export class OrderController {
|
||||
static async create(req: Request, res: Response, next: NextFunction) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { ProductService } from "../services/product.service";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { ProductService } from "../services/product.service.js";
|
||||
|
||||
export class ProductController {
|
||||
static async create(req: Request, res: Response, next: NextFunction) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ZodSchema } from "zod";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const validate =
|
||||
(schema: ZodSchema) => (req: Request, _res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import mongoose from "mongoose";
|
||||
import { ProductModel } from "../models/product.model";
|
||||
import { OrderModel } from "../models/order.model";
|
||||
import { ProductRepository } from "../repositories/product.repo";
|
||||
import { OrderRepository } from "../repositories/order.repo";
|
||||
|
||||
export class OrderService {
|
||||
static async createOrder(
|
||||
@@ -12,28 +12,18 @@ export class OrderService {
|
||||
|
||||
try {
|
||||
const productIds = items.map((i) => i.productId);
|
||||
|
||||
const products = await ProductModel.find({
|
||||
_id: { $in: productIds },
|
||||
}).session(session);
|
||||
const products = await ProductRepository.findByIds(productIds, session);
|
||||
|
||||
if (products.length !== productIds.length) {
|
||||
throw { statusCode: 400, message: "One or more products not found" };
|
||||
}
|
||||
|
||||
const orderItems = [];
|
||||
let total = 0;
|
||||
const orderItems = [];
|
||||
|
||||
for (const item of items) {
|
||||
const product = products.find((p) => p._id === item.productId)!;
|
||||
|
||||
if (item.quantity <= 0) {
|
||||
throw {
|
||||
statusCode: 400,
|
||||
message: "Quantity must be greater than zero",
|
||||
};
|
||||
}
|
||||
|
||||
if (product.stock < item.quantity) {
|
||||
throw {
|
||||
statusCode: 409,
|
||||
@@ -41,8 +31,11 @@ export class OrderService {
|
||||
};
|
||||
}
|
||||
|
||||
product.stock -= item.quantity;
|
||||
await product.save({ session });
|
||||
await ProductRepository.decrementStock(
|
||||
product._id,
|
||||
item.quantity,
|
||||
session,
|
||||
);
|
||||
|
||||
orderItems.push({
|
||||
productId: product._id,
|
||||
@@ -53,87 +46,16 @@ export class OrderService {
|
||||
total += product.price * item.quantity;
|
||||
}
|
||||
|
||||
const order = await OrderModel.create(
|
||||
[
|
||||
{
|
||||
userId,
|
||||
items: orderItems,
|
||||
total,
|
||||
status: "created",
|
||||
},
|
||||
],
|
||||
{ session },
|
||||
const order = await OrderRepository.create(
|
||||
{
|
||||
userId,
|
||||
items: orderItems,
|
||||
total,
|
||||
status: "created",
|
||||
},
|
||||
session,
|
||||
);
|
||||
|
||||
await session.commitTransaction();
|
||||
return order[0];
|
||||
} catch (err) {
|
||||
await session.abortTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
session.endSession();
|
||||
}
|
||||
}
|
||||
|
||||
static async listOrders(userId: string, role: "admin" | "customer") {
|
||||
if (role === "admin") {
|
||||
return OrderModel.find().lean();
|
||||
}
|
||||
return OrderModel.find({ userId }).lean();
|
||||
}
|
||||
|
||||
static async payOrder(orderId: string) {
|
||||
const order = await OrderModel.findById(orderId);
|
||||
if (!order) {
|
||||
throw { statusCode: 404, message: "Order not found" };
|
||||
}
|
||||
|
||||
if (order.status === "paid") {
|
||||
return order;
|
||||
}
|
||||
|
||||
if (order.status === "cancelled") {
|
||||
throw { statusCode: 409, message: "Order is cancelled" };
|
||||
}
|
||||
|
||||
order.status = "paid";
|
||||
await order.save();
|
||||
return order;
|
||||
}
|
||||
|
||||
static async cancelOrder(orderId: string) {
|
||||
const session = await mongoose.startSession();
|
||||
session.startTransaction();
|
||||
|
||||
try {
|
||||
const order = await OrderModel.findById(orderId).session(session);
|
||||
if (!order) {
|
||||
throw { statusCode: 404, message: "Order not found" };
|
||||
}
|
||||
|
||||
if (order.status === "cancelled") {
|
||||
await session.commitTransaction();
|
||||
return order;
|
||||
}
|
||||
|
||||
if (order.status === "paid") {
|
||||
throw {
|
||||
statusCode: 409,
|
||||
message: "Paid orders cannot be cancelled",
|
||||
};
|
||||
}
|
||||
|
||||
for (const item of order.items) {
|
||||
await ProductModel.updateOne(
|
||||
{ _id: item.productId },
|
||||
{ $inc: { stock: item.quantity } },
|
||||
{ session },
|
||||
);
|
||||
}
|
||||
|
||||
order.status = "cancelled";
|
||||
await order.save({ session });
|
||||
|
||||
await session.commitTransaction();
|
||||
return order;
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
type LogMeta = Record<string, unknown>;
|
||||
|
||||
function format(level: string, message: string, meta?: LogMeta) {
|
||||
const timestamp = new Date().toISOString();
|
||||
return meta
|
||||
? `[${timestamp}] ${level.toUpperCase()} ${message} ${JSON.stringify(meta)}`
|
||||
: `[${timestamp}] ${level.toUpperCase()} ${message}`;
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
info(message: string, meta?: LogMeta) {
|
||||
console.log(format("info", message, meta));
|
||||
},
|
||||
|
||||
warn(message: string, meta?: LogMeta) {
|
||||
console.warn(format("warn", message, meta));
|
||||
},
|
||||
|
||||
error(message: string, meta?: LogMeta) {
|
||||
console.error(format("error", message, meta));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hash: string,
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user