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 934f92f..9bac61b 100644 --- a/package.json +++ b/package.json @@ -1,91 +1,184 @@ -{ - "name": "stellarAid-api", - "version": "0.0.1", - "description": "", - "author": "", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" - }, - "dependencies": { - "@nestjs/common": "^11.0.1", - "@nestjs/config": "^4.0.4", - "helmet": "^8.0.0", - "@nestjs/core": "^11.0.1", - "@nestjs/jwt": "^11.0.2", - "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^11.0.1", - "@nestjs/swagger": "^11.4.6", - "@prisma/client": "5.17.0", - "@stellar/stellar-sdk": "13.3.0", - "@types/bcrypt": "^6.0.0", - "@types/passport-jwt": "^4.0.1", - "bcrypt": "^6.0.0", - "class-transformer": "^0.5.1", - "class-validator": "^0.15.1", - "ioredis": "^5.11.1", - "passport": "^0.7.0", - "passport-jwt": "^4.0.1", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "swagger-ui-express": "^5.0.1" - }, - "devDependencies": { - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.18.0", - "@nestjs/cli": "^11.0.24", - "@nestjs/schematics": "^11.0.0", - "@nestjs/testing": "^11.0.1", - "@types/express": "^5.0.0", - "@types/jest": "^30.0.0", - "@types/node": "^24.0.0", - "@types/supertest": "^7.0.0", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-prettier": "^5.2.2", - "globals": "^17.0.0", - "jest": "^30.0.0", - "prettier": "^3.4.2", - "prisma": "5.17.0", - "source-map-support": "^0.5.21", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-loader": "^9.5.2", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.7.3", - "typescript-eslint": "^8.20.0" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" - }, - "prisma": { - "seed": "ts-node prisma/seed.ts" - } -} +{ + "name": "stellarAid-api", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "prepare": "husky", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.4.6", + "@prisma/client": "5.17.0", + "@stellar/stellar-sdk": "13.3.0", + "@types/bcrypt": "^6.0.0", + "@types/passport-jwt": "^4.0.1", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "ioredis": "^5.11.1", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@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", + "@types/node": "^24.0.0", + "@types/supertest": "^7.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^17.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "prisma": "5.17.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + }, + "prisma": { + "seed": "ts-node prisma/seed.ts" + } +} +{ + "name": "stellarAid-api", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.4", + "helmet": "^8.0.0", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.4.6", + "@prisma/client": "5.17.0", + "@stellar/stellar-sdk": "13.3.0", + "@types/bcrypt": "^6.0.0", + "@types/passport-jwt": "^4.0.1", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "ioredis": "^5.11.1", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.24", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.0.0", + "@types/supertest": "^7.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^17.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "prisma": "5.17.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + }, + "prisma": { + "seed": "ts-node prisma/seed.ts" + } +} 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,