From 302dc42bb1d52a2659dd1ba14efe3d0f26c59f1d Mon Sep 17 00:00:00 2001 From: nimatstar Date: Mon, 24 Aug 2026 18:03:18 +0100 Subject: [PATCH] chore: add husky hooks, enable strict TS, structured logging, audit action registry - Add husky pre-commit running lint-staged (eslint + prettier) (closes #559) - Enable TypeScript strict mode with typing guidelines doc (closes #557) - Add structured AppLoggerService with levels + correlation ids and docs (closes #556) - Add AuditAction registry for sensitive operations and audit-logging docs (closes #550) --- .husky/pre-commit | 1 + .lintstagedrc.json | 6 +++ docs/audit-logging.md | 32 ++++++++++++ docs/logging-strategy.md | 32 ++++++++++++ docs/typing-guidelines.md | 22 +++++++++ package.json | 3 ++ src/audit/audit-actions.enum.ts | 27 +++++++++++ src/common/logging/app-logger.service.ts | 62 ++++++++++++++++++++++++ tsconfig.json | 2 + 9 files changed, 187 insertions(+) create mode 100644 .husky/pre-commit create mode 100644 .lintstagedrc.json create mode 100644 docs/audit-logging.md create mode 100644 docs/logging-strategy.md create mode 100644 docs/typing-guidelines.md create mode 100644 src/audit/audit-actions.enum.ts create mode 100644 src/common/logging/app-logger.service.ts diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..a33267d --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,6 @@ +{ + "*.ts": [ + "eslint --fix", + "prettier --write" + ] +} diff --git a/docs/audit-logging.md b/docs/audit-logging.md new file mode 100644 index 0000000..e63022b --- /dev/null +++ b/docs/audit-logging.md @@ -0,0 +1,32 @@ +# Audit Logging for Sensitive Actions + +Sensitive operations (payments, wallet and admin actions, auth events) are +recorded to an immutable audit trail via the `audit` module. The set of audited +operations is enumerated in `src/audit/audit-actions.enum.ts` (`AuditAction`). + +## What is recorded + +Every audit entry captures: + +- **who** — the acting user id (and role where relevant) +- **when** — a server timestamp +- **what** — the `AuditAction` and structured `details` (ids, amounts, target) +- **where** — request metadata (IP, correlation id) + +## Immutability + +- Audit records are **append-only**: the service exposes create/read only, never + update or delete. +- In production, the database role used by the app should have `INSERT`/`SELECT` + on the audit table but not `UPDATE`/`DELETE`, so records are tamper-evident. + +## Viewing + +- Admins can browse the trail through the admin endpoints + (`GET /admin/audit-logs`) with filtering by user, action and date range. + +## Adding a new audited action + +1. Add a value to the `AuditAction` enum. +2. Call the audit service from the relevant service method with the action and + its details. diff --git a/docs/logging-strategy.md b/docs/logging-strategy.md new file mode 100644 index 0000000..a3b94bc --- /dev/null +++ b/docs/logging-strategy.md @@ -0,0 +1,32 @@ +# Logging Strategy + +Structured logging is provided by `AppLoggerService` +(`src/common/logging/app-logger.service.ts`). + +## Principles + +- **Structured (JSON) output** so logs are machine-parseable and shippable to a + log aggregator. +- **Log levels** — `debug`, `verbose`, `info`, `warn`, `error`. Use `debug` + liberally in development; keep `info`+ meaningful in production. +- **Per-module context** — call `setContext('PaymentsService')` so each log line + carries the emitting component. +- **Correlation IDs** — set a per-request correlation id + (`setCorrelationId(...)`) so all log lines for one request can be traced. + +## Usage + +```ts +constructor(private readonly logger: AppLoggerService) { + this.logger.setContext(PaymentsService.name); +} + +this.logger.log('Escrow created', /* context */ undefined); +this.logger.error('Payment failed', err.stack); +``` + +## Notes + +- Never log secrets, tokens or full card/wallet credentials. +- The service is `TRANSIENT`-scoped so each injecting class gets its own + context. diff --git a/docs/typing-guidelines.md b/docs/typing-guidelines.md new file mode 100644 index 0000000..1989e07 --- /dev/null +++ b/docs/typing-guidelines.md @@ -0,0 +1,22 @@ +# TypeScript Typing Guidelines + +`strict` mode is enabled in `tsconfig.json`. Follow these guidelines so the +codebase stays type-safe. + +## Rules + +- **No implicit `any`.** Prefer precise types; use `unknown` and narrow when a + value's type is genuinely dynamic. +- **Null-safety.** `strictNullChecks` is on — handle `null`/`undefined` + explicitly (optional chaining, guards, or non-null assertions only when + provably safe). +- **Type external libraries.** Add `@types/*` packages or local `.d.ts` + declarations for untyped dependencies rather than casting to `any`. +- **Avoid `as any`.** If a cast is unavoidable, cast to the narrowest correct + type and add a comment explaining why. + +## Notes + +- `strictPropertyInitialization` is intentionally disabled because NestJS + DTO/entity classes declare properties that are populated by the framework + (validation, ORM) rather than in a constructor. diff --git a/package.json b/package.json index dac2652..4af1559 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "license": "UNLICENSED", "scripts": { "build": "nest build", + "prepare": "husky", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", @@ -46,6 +47,8 @@ "@eslint/js": "^9.18.0", "@nestjs/cli": "^11.0.24", "@nestjs/schematics": "^11.0.0", + "husky": "^9.1.7", + "lint-staged": "^15.2.10", "@nestjs/testing": "^11.0.1", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", diff --git a/src/audit/audit-actions.enum.ts b/src/audit/audit-actions.enum.ts new file mode 100644 index 0000000..a216c35 --- /dev/null +++ b/src/audit/audit-actions.enum.ts @@ -0,0 +1,27 @@ +/** + * Canonical list of sensitive actions that must be written to the audit trail. + * Using an enum keeps action names consistent across the codebase and makes the + * set of audited operations easy to review. See `docs/audit-logging.md`. + */ +export enum AuditAction { + // Auth + USER_LOGIN = 'user.login', + USER_LOGIN_FAILED = 'user.login_failed', + USER_LOGOUT = 'user.logout', + PASSWORD_CHANGED = 'user.password_changed', + + // Wallet + WALLET_LINKED = 'wallet.linked', + WALLET_VERIFIED = 'wallet.verified', + + // Payments + ESCROW_INITIATED = 'payment.escrow_initiated', + PAYMENT_CONFIRMED = 'payment.confirmed', + PAYMENT_RELEASED = 'payment.released', + PAYMENT_REFUNDED = 'payment.refunded', + + // Admin + USER_STATUS_UPDATED = 'admin.user_status_updated', + DISPUTE_RESOLVED = 'admin.dispute_resolved', + ARTIST_VERIFIED = 'admin.artist_verified', +} diff --git a/src/common/logging/app-logger.service.ts b/src/common/logging/app-logger.service.ts new file mode 100644 index 0000000..3010a1c --- /dev/null +++ b/src/common/logging/app-logger.service.ts @@ -0,0 +1,62 @@ +import { Injectable, LoggerService, Scope } from '@nestjs/common'; + +/** + * Thin structured-logging wrapper around NestJS's logger. + * + * Emits JSON log lines with a level, message, optional context and an optional + * correlation id so logs can be traced across a single request. Inject this in + * modules instead of using `console.*`. See `docs/logging-strategy.md`. + */ +@Injectable({ scope: Scope.TRANSIENT }) +export class AppLoggerService implements LoggerService { + private context?: string; + private correlationId?: string; + + setContext(context: string): this { + this.context = context; + return this; + } + + setCorrelationId(correlationId: string): this { + this.correlationId = correlationId; + return this; + } + + log(message: unknown, context?: string): void { + this.write('info', message, context); + } + + error(message: unknown, trace?: string, context?: string): void { + this.write('error', message, context, trace); + } + + warn(message: unknown, context?: string): void { + this.write('warn', message, context); + } + + debug(message: unknown, context?: string): void { + this.write('debug', message, context); + } + + verbose(message: unknown, context?: string): void { + this.write('verbose', message, context); + } + + private write( + level: string, + message: unknown, + context?: string, + trace?: string, + ): void { + const entry = { + level, + time: new Date().toISOString(), + context: context ?? this.context, + correlationId: this.correlationId, + message, + ...(trace ? { trace } : {}), + }; + // eslint-disable-next-line no-console + console.log(JSON.stringify(entry)); + } +} diff --git a/tsconfig.json b/tsconfig.json index aba29b0..2c981dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,9 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, + "strict": true, "strictNullChecks": true, + "strictPropertyInitialization": false, "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false,