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:
@@ -141,6 +141,7 @@ describe('validateEnv', () => {
|
|||||||
process.env['DATABASE_URL'] = 'postgresql://localhost/goodgo';
|
process.env['DATABASE_URL'] = 'postgresql://localhost/goodgo';
|
||||||
process.env['CORS_ORIGINS'] = 'https://goodgo.vn';
|
process.env['CORS_ORIGINS'] = 'https://goodgo.vn';
|
||||||
process.env['REDIS_HOST'] = 'redis.internal';
|
process.env['REDIS_HOST'] = 'redis.internal';
|
||||||
|
process.env['KYC_ENCRYPTION_KEY'] = 'a'.repeat(64); // 32 bytes hex
|
||||||
|
|
||||||
expect(() => validateEnv()).not.toThrow();
|
expect(() => validateEnv()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export enum CachePrefix {
|
|||||||
MARKET_DISTRICT = 'cache:market:district',
|
MARKET_DISTRICT = 'cache:market:district',
|
||||||
USER_PROFILE = 'cache:user:profile',
|
USER_PROFILE = 'cache:user:profile',
|
||||||
USER_QUOTA = 'cache:user:quota',
|
USER_QUOTA = 'cache:user:quota',
|
||||||
|
VALUATION = 'cache:valuation',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const REQUIRED_IN_PRODUCTION: readonly string[] = [
|
|||||||
'DATABASE_URL',
|
'DATABASE_URL',
|
||||||
'CORS_ORIGINS',
|
'CORS_ORIGINS',
|
||||||
'REDIS_HOST',
|
'REDIS_HOST',
|
||||||
|
'KYC_ENCRYPTION_KEY',
|
||||||
];
|
];
|
||||||
|
|
||||||
const REQUIRED_WHEN_USED: ReadonlyMap<string, string> = new Map([
|
const REQUIRED_WHEN_USED: ReadonlyMap<string, string> = new Map([
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ export class LoggerService implements NestLoggerService {
|
|||||||
process.env['NODE_ENV'] !== 'production'
|
process.env['NODE_ENV'] !== 'production'
|
||||||
? { target: 'pino-pretty', options: { colorize: true } }
|
? { target: 'pino-pretty', options: { colorize: true } }
|
||||||
: undefined,
|
: 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: {
|
formatters: {
|
||||||
level(label) {
|
level(label) {
|
||||||
return { level: label };
|
return { level: label };
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
const SENSITIVE_KEYS = new Set([
|
const SENSITIVE_KEYS = new Set([
|
||||||
'password',
|
'password',
|
||||||
|
'passwordHash',
|
||||||
'token',
|
'token',
|
||||||
'accessToken',
|
'accessToken',
|
||||||
'refreshToken',
|
'refreshToken',
|
||||||
@@ -17,6 +18,19 @@ const SENSITIVE_KEYS = new Set([
|
|||||||
'ssn',
|
'ssn',
|
||||||
'cmnd',
|
'cmnd',
|
||||||
'cccd',
|
'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;
|
const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
||||||
|
|||||||
@@ -2,16 +2,40 @@ import { Injectable, type OnModuleInit, type OnModuleDestroy } from '@nestjs/com
|
|||||||
import { PrismaPg } from '@prisma/adapter-pg';
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import pg from 'pg';
|
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()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
private pool: pg.Pool;
|
private pool: pg.Pool;
|
||||||
|
private kycEncryption: FieldEncryptionConfig | null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const pool = new pg.Pool({ connectionString: process.env['DATABASE_URL'] });
|
const pool = new pg.Pool({ connectionString: process.env['DATABASE_URL'] });
|
||||||
const adapter = new PrismaPg(pool);
|
const adapter = new PrismaPg(pool);
|
||||||
super({ adapter });
|
super({ adapter });
|
||||||
this.pool = pool;
|
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> {
|
async onModuleInit(): Promise<void> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { validate } from 'class-validator';
|
import { validate } from 'class-validator';
|
||||||
import { IsVietnamPhone } from '../is-vietnam-phone.validator';
|
|
||||||
import { IsVietnamDistrict } from '../is-vietnam-district.validator';
|
import { IsVietnamDistrict } from '../is-vietnam-district.validator';
|
||||||
|
import { IsVietnamPhone } from '../is-vietnam-phone.validator';
|
||||||
import { IsVND } from '../is-vnd.validator';
|
import { IsVND } from '../is-vnd.validator';
|
||||||
|
|
||||||
// Test DTOs
|
// Test DTOs
|
||||||
|
|||||||
Reference in New Issue
Block a user