feat: add bcrypt for password hashing and implement logger utility

This commit is contained in:
2026-01-13 00:18:44 +03:00
parent ae6521977d
commit 64e304214b
11 changed files with 109 additions and 104 deletions
+4 -1
View File
@@ -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",
+42
View File
@@ -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]: {}
+2
View File
@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- bcrypt
+1 -1
View File
@@ -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";
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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 -1
View File
@@ -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) => {
+17 -95
View File
@@ -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) {
+22
View File
@@ -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));
},
};
+14
View File
@@ -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);
}