From 1e0436e95f4d4ae7810099f99efee5371ce7840b Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 9 Apr 2026 09:41:01 +0700 Subject: [PATCH] refactor(shared): improve logger injection, env validation, and PII masking Enhance shared infrastructure services with proper dependency injection, stricter environment variable validation, and improved PII data masking. Co-Authored-By: Paperclip --- .../__tests__/env-validation.spec.ts | 1 + .../shared/infrastructure/cache.service.ts | 1 + .../shared/infrastructure/env-validation.ts | 1 + .../shared/infrastructure/logger.service.ts | 13 ++++++++++ .../shared/infrastructure/pii-masker.ts | 14 +++++++++++ .../shared/infrastructure/prisma.service.ts | 24 +++++++++++++++++++ .../__tests__/vietnam-validators.spec.ts | 2 +- 7 files changed, 55 insertions(+), 1 deletion(-) diff --git a/apps/api/src/modules/shared/infrastructure/__tests__/env-validation.spec.ts b/apps/api/src/modules/shared/infrastructure/__tests__/env-validation.spec.ts index 13bc2d7..633d402 100644 --- a/apps/api/src/modules/shared/infrastructure/__tests__/env-validation.spec.ts +++ b/apps/api/src/modules/shared/infrastructure/__tests__/env-validation.spec.ts @@ -141,6 +141,7 @@ describe('validateEnv', () => { process.env['DATABASE_URL'] = 'postgresql://localhost/goodgo'; process.env['CORS_ORIGINS'] = 'https://goodgo.vn'; process.env['REDIS_HOST'] = 'redis.internal'; + process.env['KYC_ENCRYPTION_KEY'] = 'a'.repeat(64); // 32 bytes hex expect(() => validateEnv()).not.toThrow(); }); diff --git a/apps/api/src/modules/shared/infrastructure/cache.service.ts b/apps/api/src/modules/shared/infrastructure/cache.service.ts index af94512..0d20c64 100644 --- a/apps/api/src/modules/shared/infrastructure/cache.service.ts +++ b/apps/api/src/modules/shared/infrastructure/cache.service.ts @@ -36,6 +36,7 @@ export enum CachePrefix { MARKET_DISTRICT = 'cache:market:district', USER_PROFILE = 'cache:user:profile', USER_QUOTA = 'cache:user:quota', + VALUATION = 'cache:valuation', } @Injectable() diff --git a/apps/api/src/modules/shared/infrastructure/env-validation.ts b/apps/api/src/modules/shared/infrastructure/env-validation.ts index 1f137fd..aca923c 100644 --- a/apps/api/src/modules/shared/infrastructure/env-validation.ts +++ b/apps/api/src/modules/shared/infrastructure/env-validation.ts @@ -15,6 +15,7 @@ const REQUIRED_IN_PRODUCTION: readonly string[] = [ 'DATABASE_URL', 'CORS_ORIGINS', 'REDIS_HOST', + 'KYC_ENCRYPTION_KEY', ]; const REQUIRED_WHEN_USED: ReadonlyMap = new Map([ diff --git a/apps/api/src/modules/shared/infrastructure/logger.service.ts b/apps/api/src/modules/shared/infrastructure/logger.service.ts index b6d235e..500832a 100644 --- a/apps/api/src/modules/shared/infrastructure/logger.service.ts +++ b/apps/api/src/modules/shared/infrastructure/logger.service.ts @@ -13,6 +13,19 @@ export class LoggerService implements NestLoggerService { process.env['NODE_ENV'] !== 'production' ? { target: 'pino-pretty', options: { colorize: true } } : undefined, + redact: { + paths: [ + 'password', 'passwordHash', 'token', 'accessToken', 'refreshToken', + 'secret', 'authorization', 'cookie', 'creditCard', 'cardNumber', + 'cvv', 'ssn', 'cmnd', 'cccd', 'email', 'phone', 'kycData', + 'idNumber', 'identityNumber', 'dateOfBirth', 'dob', 'address', + 'bankAccount', 'accountNumber', 'apiKey', 'privateKey', 'encryptionKey', + 'req.headers.authorization', 'req.headers.cookie', + 'user.email', 'user.phone', 'user.kycData', + 'body.password', 'body.token', 'body.email', 'body.phone', + ], + censor: '[REDACTED]', + }, formatters: { level(label) { return { level: label }; diff --git a/apps/api/src/modules/shared/infrastructure/pii-masker.ts b/apps/api/src/modules/shared/infrastructure/pii-masker.ts index 9ea19ba..a5edcea 100644 --- a/apps/api/src/modules/shared/infrastructure/pii-masker.ts +++ b/apps/api/src/modules/shared/infrastructure/pii-masker.ts @@ -5,6 +5,7 @@ const SENSITIVE_KEYS = new Set([ 'password', + 'passwordHash', 'token', 'accessToken', 'refreshToken', @@ -17,6 +18,19 @@ const SENSITIVE_KEYS = new Set([ 'ssn', 'cmnd', 'cccd', + 'email', + 'phone', + 'kycData', + 'idNumber', + 'identityNumber', + 'dateOfBirth', + 'dob', + 'address', + 'bankAccount', + 'accountNumber', + 'apiKey', + 'privateKey', + 'encryptionKey', ]); const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; diff --git a/apps/api/src/modules/shared/infrastructure/prisma.service.ts b/apps/api/src/modules/shared/infrastructure/prisma.service.ts index 22fdffd..6f9373f 100644 --- a/apps/api/src/modules/shared/infrastructure/prisma.service.ts +++ b/apps/api/src/modules/shared/infrastructure/prisma.service.ts @@ -2,16 +2,40 @@ import { Injectable, type OnModuleInit, type OnModuleDestroy } from '@nestjs/com import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '@prisma/client'; import pg from 'pg'; +import { encryptField, decryptField, type FieldEncryptionConfig } from './field-encryption'; + +function getKycEncryptionConfig(): FieldEncryptionConfig | null { + const key = process.env['KYC_ENCRYPTION_KEY']; + if (!key) return null; + return { + key, + keyVersion: Number(process.env['KYC_ENCRYPTION_KEY_VERSION'] ?? '1'), + }; +} @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { private pool: pg.Pool; + private kycEncryption: FieldEncryptionConfig | null; constructor() { const pool = new pg.Pool({ connectionString: process.env['DATABASE_URL'] }); const adapter = new PrismaPg(pool); super({ adapter }); this.pool = pool; + this.kycEncryption = getKycEncryptionConfig(); + } + + /** Encrypt kycData before writing to the database. */ + encryptKycData(data: unknown): unknown { + if (!this.kycEncryption || data === null || data === undefined) return data; + return encryptField(data, this.kycEncryption); + } + + /** Decrypt kycData after reading from the database. */ + decryptKycData(data: unknown): unknown { + if (!this.kycEncryption || data === null || data === undefined) return data; + return decryptField(data, this.kycEncryption); } async onModuleInit(): Promise { diff --git a/apps/api/src/modules/shared/utils/validators/__tests__/vietnam-validators.spec.ts b/apps/api/src/modules/shared/utils/validators/__tests__/vietnam-validators.spec.ts index 1eb63a4..608588d 100644 --- a/apps/api/src/modules/shared/utils/validators/__tests__/vietnam-validators.spec.ts +++ b/apps/api/src/modules/shared/utils/validators/__tests__/vietnam-validators.spec.ts @@ -1,6 +1,6 @@ import { validate } from 'class-validator'; -import { IsVietnamPhone } from '../is-vietnam-phone.validator'; import { IsVietnamDistrict } from '../is-vietnam-district.validator'; +import { IsVietnamPhone } from '../is-vietnam-phone.validator'; import { IsVND } from '../is-vnd.validator'; // Test DTOs