From d3c269505a9a10097c1abc3b44bbab6ae3eeb3c1 Mon Sep 17 00:00:00 2001 From: faisolavolut Date: Wed, 10 Dec 2025 14:01:36 +0700 Subject: [PATCH] feat(logging): enhance logging functionality with detailed request and error logging --- prisma/schema.prisma | 32 +++++++++++--- src/controllers/dbController.ts | 75 +++++++++++++++++++++++++++++--- src/controllers/logController.ts | 10 ++--- src/lib/logger-example.ts | 68 +++++++++++++++++++++++++++++ src/lib/logger.ts | 10 ++--- src/middleware/logAuth.ts | 14 +++--- 6 files changed, 179 insertions(+), 30 deletions(-) create mode 100644 src/lib/logger-example.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8791608..1971dc1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -320,11 +320,11 @@ model transactions_lines { line_no Int @default(1) id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid c_payment_id Decimal? @db.Decimal(10, 0) + documentno String? @db.VarChar(255) date DateTime? @db.Timestamptz(6) invoicepartner_id String? @db.Uuid - documentno String? @db.VarChar(255) - invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id], onDelete: NoAction, onUpdate: NoAction) database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction) + invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id], onDelete: NoAction, onUpdate: NoAction) transactions transactions @relation(fields: [transaction_id], references: [id], onDelete: NoAction, onUpdate: NoAction) } @@ -342,19 +342,19 @@ model invoicepartner { id String @id(map: "invoicePartner_pkey") @default(dbgenerated("gen_random_uuid()")) @db.Uuid costumerno String @db.VarChar(50) c_bpartner_id Decimal? @db.Decimal(10, 0) - documentno String? @db.VarChar(30) db_id String? @db.Uuid c_invoice_id Decimal? @db.Decimal(10, 0) is_active Boolean @default(true) created_at DateTime @default(now()) @db.Timestamptz(6) updated_at DateTime @default(now()) @db.Timestamptz(6) is_pay Boolean @default(false) - grandtotal Decimal? @db.Decimal(20, 2) amount Decimal? @db.Decimal(20, 2) rv_openitem_id String? @db.Uuid + documentno String? @db.VarChar(30) + grandtotal Decimal? @db.Decimal(20, 2) + invoiceLines invoice_lines[] rv_openitem rv_openitem? @relation(fields: [rv_openitem_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "invoicePartner_rv_openitem_id_fkey") transactionsLines transactions_lines[] - invoiceLines invoice_lines[] } model invoice { @@ -390,7 +390,6 @@ model invoice_lines { description String? @db.VarChar(255) amount Decimal? @db.Decimal(20, 2) c_invoice_id Decimal? @db.Decimal(10, 0) - invoicepartner_id String? @db.Uuid invoice_id String? @db.Uuid db_id String? @db.Uuid created_at DateTime? @default(now()) @db.Timestamptz(6) @@ -400,9 +399,10 @@ model invoice_lines { line_no Int @default(1) billcode String? @db.VarChar(10) billname String? @db.VarChar(50) - invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id]) + invoicepartner_id String? @db.Uuid database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction) invoice invoice? @relation(fields: [invoice_id], references: [id]) + invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id]) } model sequences { @@ -424,3 +424,21 @@ model whitelistcors { is_active String @default("Y") @db.Char(1) tenants tenants @relation(fields: [tenant_id], references: [tenant_id], onDelete: NoAction, onUpdate: NoAction) } + +model roles { + role_id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String @unique @db.VarChar(100) + description String? @db.VarChar(255) + is_active Boolean @default(true) + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) +} + +model user_roles { + user_role_id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + user_id String @db.Uuid + role_id String @db.Uuid + is_active Boolean @default(true) + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) +} diff --git a/src/controllers/dbController.ts b/src/controllers/dbController.ts index ef9c7da..f66730b 100644 --- a/src/controllers/dbController.ts +++ b/src/controllers/dbController.ts @@ -1,6 +1,13 @@ import { Request, Response } from "express"; import prisma from "../prisma"; import { hash } from "bcryptjs"; +import { createLogger, LogLevel } from "../lib/logger"; + +// Logger untuk database controller +const dbLogger = createLogger({ + logDir: "./logs/log", + minLevel: LogLevel.INFO, +}); type DbRequest = { table: string; @@ -11,12 +18,22 @@ type DbRequest = { export class DbController { async handle(req: Request, res: Response) { const body: DbRequest = req.body; - if (!body || !body.table || !body.action) + if (!body || !body.table || !body.action) { + dbLogger.warning("Invalid DB request - missing table or action", { + body, + // ip: req.ip, + }); return res.status(400).json({ error: "table and action required" }); + } const model = (prisma as any)[body.table]; - if (!model) + if (!model) { + dbLogger.warning("Unknown table access attempt", { + table: body.table, + ip: req.ip, + }); return res.status(400).json({ error: `unknown table ${body.table}` }); + } // Only allow safe actions const allowed = [ @@ -29,8 +46,14 @@ export class DbController { "upsert", "count", ]; - if (!allowed.includes(body.action)) + if (!allowed.includes(body.action)) { + dbLogger.warning("Disallowed action attempt", { + table: body.table, + action: body.action, + ip: req.ip, + }); return res.status(400).json({ error: "action not allowed" }); + } // Tables that require access checks const protectedTables = new Set([ @@ -91,6 +114,11 @@ export class DbController { if (body.table === "users" && body.action === "create") { const payload = body.data?.data || body.data || {}; if (!payload.username || !payload.password) { + dbLogger.warning("User creation missing credentials", { + hasUsername: !!payload.username, + hasPassword: !!payload.password, + ip: req.ip, + }); return res.status(400).json({ error: "username and password are required for users.create", }); @@ -100,8 +128,13 @@ export class DbController { const existing = await prisma.users.findFirst({ where: { username: payload.username }, }); - if (existing) + if (existing) { + dbLogger.warning("Duplicate username creation attempt", { + username: payload.username, + ip: req.ip, + }); return res.status(409).json({ error: "username already exists" }); + } const hashed = await hash(payload.password, 10); const createData = { @@ -112,6 +145,11 @@ export class DbController { // Ensure payload is nested under data if using Prisma create syntax const createArg = { data: createData }; const result = await model.create(createArg); + dbLogger.info("User created successfully", { + username: payload.username, + userId: result.user_id, + ip: req.ip, + }); return res.status(201).json({ result }); } @@ -120,10 +158,16 @@ export class DbController { // Expecting Prisma update signature: { where: {...}, data: {...} } const where = body.data?.where; const data = body.data?.data || body.data; - if (!where || !data) + if (!where || !data) { + dbLogger.warning("User update missing where or data", { + hasWhere: !!where, + hasData: !!data, + ip: req.ip, + }); return res .status(400) .json({ error: "where and data are required for users.update" }); + } // If username is being changed, ensure it's not taken by another user if (data.username) { @@ -136,6 +180,10 @@ export class DbController { existing.user_id !== where.id && existing.user_id !== where.userId ) { + dbLogger.warning("Duplicate username update attempt", { + username: data.username, + ip: req.ip, + }); return res.status(409).json({ error: "username already exists" }); } } @@ -147,13 +195,28 @@ export class DbController { } const result = await model.update({ where, data }); + dbLogger.info("User updated successfully", { + userId: result.user_id, + updatedFields: Object.keys(data), + ip: req.ip, + }); return res.json({ result }); } const result = await model[body.action](body.data || {}); + dbLogger.info("Database operation completed", { + table: body.table, + action: body.action, + ip: req.ip, + }); return res.json({ result }); } catch (err) { - console.error("DB dispatch error", err); + dbLogger.error("Database operation error", { + table: body.table, + action: body.action, + error: err instanceof Error ? err.message : String(err), + ip: req.ip, + }); return res.status(500).json({ error: "internal error", detail: err instanceof Error ? err.message : String(err), diff --git a/src/controllers/logController.ts b/src/controllers/logController.ts index ee912bf..eec36e8 100644 --- a/src/controllers/logController.ts +++ b/src/controllers/logController.ts @@ -184,11 +184,11 @@ export default class LogController { files: allFiles, }; - logAccessLogger.info("Log list accessed", { - path: safePath, - userId: (req as any).user?.user_id, - fileCount: result.files.length, - }); + // logAccessLogger.info("Log list accessed", { + // path: safePath, + // userId: (req as any).user?.user_id, + // fileCount: result.files.length, + // }); return res.json(result); } catch (err) { diff --git a/src/lib/logger-example.ts b/src/lib/logger-example.ts new file mode 100644 index 0000000..9b2f811 --- /dev/null +++ b/src/lib/logger-example.ts @@ -0,0 +1,68 @@ +import { LogLevel, createLogger, getDefaultLogger } from "./logger"; + +// Example 1: Using default logger (logs to ./logs folder) +const defaultLog = getDefaultLogger(); + +console.log("=== Default Logger Example ==="); +defaultLog.debug("a debug message"); +defaultLog.info("a info message"); +defaultLog.notice("a notice message"); +defaultLog.warning("a warning message"); +defaultLog.error("a error message"); +defaultLog.critical("a critical message"); +defaultLog.alert("a alert message"); +defaultLog.emergency("a emergency message"); + +// Example 2: Custom logger with specific folder +const customLog = createLogger({ + logDir: "./custom-logs", + minLevel: LogLevel.WARNING, // Only log WARNING and above + filePrefix: "payment-api", +}); + +console.log("\n=== Custom Logger Example ==="); +customLog.debug("this will not be logged (below min level)"); +customLog.info("this will not be logged (below min level)"); +customLog.warning("a warning message that will be logged"); +customLog.error("a error message that will be logged"); +customLog.critical("a critical message that will be logged"); + +// Example 3: Logger with additional arguments +const apiLog = createLogger({ + logDir: "./api-logs", + filePrefix: "mandiri-api", + maxFileSizeMB: 5, +}); + +console.log("\n=== API Logger Example with Objects ==="); +apiLog.info("Payment received", { + amount: 100000, + virtualAccount: "1234567890", + timestamp: new Date(), +}); + +apiLog.error("Payment failed", { + error: "Insufficient balance", + virtualAccount: "1234567890", + attemptedAmount: 500000, +}); + +// Example 4: Different log levels with context +const bankLog = createLogger({ + logDir: "./bank-integration", + filePrefix: "bank-sync", +}); + +console.log("\n=== Bank Integration Logger Example ==="); +bankLog.debug("Starting database sync"); +bankLog.info("Connected to bank API"); +bankLog.notice("Processing 1000 transactions"); +bankLog.warning("API rate limit approaching"); +bankLog.error("Connection timeout, retrying..."); +bankLog.critical("Multiple failures detected"); +bankLog.alert("System performance degraded"); +bankLog.emergency("All bank connections failed"); + +console.log( + "\nLogger examples completed. Check the log files in respective directories." +); diff --git a/src/lib/logger.ts b/src/lib/logger.ts index fbda5f9..fcf902d 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -29,7 +29,7 @@ export class Logger { logDir: config.logDir, minLevel: config.minLevel ?? LogLevel.DEBUG, maxFileSizeMB: config.maxFileSizeMB ?? 10, - filePrefix: config.filePrefix ?? "app", + filePrefix: config.filePrefix ?? "", }; // Ensure log directory exists @@ -48,10 +48,10 @@ export class Logger { if (this.currentDate !== dateStr) { this.currentDate = dateStr; - this.currentLogFile = path.join( - this.config.logDir, - `${this.config.filePrefix}-${dateStr}.log` - ); + const fileName = this.config.filePrefix + ? `${this.config.filePrefix}-${dateStr}.log` + : `${dateStr}.log`; + this.currentLogFile = path.join(this.config.logDir, fileName); } return this.currentLogFile!; diff --git a/src/middleware/logAuth.ts b/src/middleware/logAuth.ts index 8df8579..af969e4 100644 --- a/src/middleware/logAuth.ts +++ b/src/middleware/logAuth.ts @@ -89,12 +89,12 @@ export async function authenticateLogAccess( session_token: token, }; - authLogger.info("Log access authenticated", { - userId: req.user.user_id, - username: req.user.username, - ip: req.ip, - path: req.path, - }); + // authLogger.info("Log access authenticated", { + // userId: req.user.user_id, + // username: req.user.username, + // ip: req.ip, + // path: req.path, + // }); next(); } catch (err) { @@ -201,7 +201,7 @@ export function auditLogAccess() { }, }); - authLogger.info("Log access audit", auditData); + // authLogger.info("Log access audit", auditData); } catch (err) { authLogger.error("Failed to create audit log", { error: err instanceof Error ? err.message : String(err),