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 <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-09 09:41:01 +07:00
parent 99fbc1aaca
commit 1e0436e95f
7 changed files with 55 additions and 1 deletions

View File

@@ -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();
});

View File

@@ -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()

View File

@@ -15,6 +15,7 @@ const REQUIRED_IN_PRODUCTION: readonly string[] = [
'DATABASE_URL',
'CORS_ORIGINS',
'REDIS_HOST',
'KYC_ENCRYPTION_KEY',
];
const REQUIRED_WHEN_USED: ReadonlyMap<string, string> = new Map([

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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<void> {

View File

@@ -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