Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 34 additions & 27 deletions src/audit-log/audit-log.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between, LessThan, MoreThan, In, Brackets, WhereExpressionBuilder } from 'typeorm';
import { Repository, Between, MoreThan } from 'typeorm';
import { AuditLog } from './audit-log.entity';
import { AuditAction, AuditSeverity, AuditCategory } from './enums/audit-action.enum';
import { ConfigService } from '@nestjs/config';
import { sanitizePii } from '../common/utils/pii-sanitizer.utils';

export interface AuditLogEntry {
userId?: string;
Expand Down Expand Up @@ -93,7 +94,9 @@ export class AuditLogService {

try {
const saved = await this.auditRepo.save(log);
this.logger.debug(`Audit log created: ${entry.action} - ${entry.description || 'no description'}`);
this.logger.debug(
`Audit log created: ${log.action} - ${sanitizePii(log.description || 'no description')}`,
);
return saved;
} catch (error) {
this.logger.error('Failed to create audit log:', error);
Expand Down Expand Up @@ -169,9 +172,12 @@ export class AuditLogService {
userAgent: string,
requestId?: string,
): Promise<AuditLog> {
const severity = statusCode >= 500 ? AuditSeverity.ERROR :
statusCode >= 400 ? AuditSeverity.WARNING :
AuditSeverity.INFO;
const severity =
statusCode >= 500
? AuditSeverity.ERROR
: statusCode >= 400
? AuditSeverity.WARNING
: AuditSeverity.INFO;

return this.log({
userId: userId || undefined,
Expand Down Expand Up @@ -238,11 +244,15 @@ export class AuditLogService {
}

if (filters.categories && filters.categories.length > 0) {
queryBuilder.andWhere('audit.category IN (:...categories)', { categories: filters.categories });
queryBuilder.andWhere('audit.category IN (:...categories)', {
categories: filters.categories,
});
}

if (filters.severities && filters.severities.length > 0) {
queryBuilder.andWhere('audit.severity IN (:...severities)', { severities: filters.severities });
queryBuilder.andWhere('audit.severity IN (:...severities)', {
severities: filters.severities,
});
}

if (filters.entityType) {
Expand Down Expand Up @@ -346,7 +356,11 @@ export class AuditLogService {
/**
* Find logs by entity
*/
async findByEntity(entityType: string, entityId: string, limit: number = 100): Promise<AuditLog[]> {
async findByEntity(
entityType: string,
entityId: string,
limit: number = 100,
): Promise<AuditLog[]> {
return this.auditRepo.find({
where: { entityType, entityId },
order: { timestamp: 'DESC' },
Expand Down Expand Up @@ -567,10 +581,9 @@ export class AuditLogService {
return str;
};

const csvContent = [
headers.join(','),
...rows.map((row) => row.map(escapeCsv).join(',')),
].join('\n');
const csvContent = [headers.join(','), ...rows.map((row) => row.map(escapeCsv).join(','))].join(
'\n',
);

return csvContent;
}
Expand All @@ -591,21 +604,15 @@ export class AuditLogService {
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

const [
totalLogs,
logsToday,
logsThisWeek,
logsThisMonth,
criticalEvents,
errorEvents,
] = await Promise.all([
this.auditRepo.count(),
this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(today) } }),
this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(weekAgo) } }),
this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(monthAgo) } }),
this.auditRepo.count({ where: { severity: AuditSeverity.CRITICAL } }),
this.auditRepo.count({ where: { severity: AuditSeverity.ERROR } }),
]);
const [totalLogs, logsToday, logsThisWeek, logsThisMonth, criticalEvents, errorEvents] =
await Promise.all([
this.auditRepo.count(),
this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(today) } }),
this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(weekAgo) } }),
this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(monthAgo) } }),
this.auditRepo.count({ where: { severity: AuditSeverity.CRITICAL } }),
this.auditRepo.count({ where: { severity: AuditSeverity.ERROR } }),
]);

return {
totalLogs,
Expand Down
95 changes: 95 additions & 0 deletions src/common/utils/pii-sanitizer.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Utility for sanitizing Personally Identifiable Information (PII) from logs.
*/

/**
* Masks an email address to protect PII.
* Example: "john.doe@example.com" -> "j***e@example.com"
*/
export function sanitizeEmail(email: string): string {
if (!email || typeof email !== 'string') return email;
const parts = email.split('@');
if (parts.length !== 2) return email;

const [user, domain] = parts;
if (user.length <= 2) {
return `***@${domain}`;
}

return `${user[0]}***${user[user.length - 1]}@${domain}`;
}

/**
* Masks a name or generic string.
* Example: "John" -> "J***"
*/
export function sanitizeName(name: string): string {
if (!name || typeof name !== 'string') return name;
if (name.length <= 1) return '***';
return `${name[0]}***`;
}

/**
* Recursively sanitizes PII from an object or string.
* Replaces values of sensitive keys with masks.
*/
export function sanitizePii(data: any): any {
if (data === null || data === undefined) {
return data;
}

if (typeof data === 'string') {
// Basic email masking within strings
return data.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, (email) =>
sanitizeEmail(email),
);
}

if (Array.isArray(data)) {
return data.map((item) => sanitizePii(item));
}

if (typeof data === 'object') {
const sanitized: any = {};
const sensitiveKeys = [
'email',
'useremail',
'contactemail',
'owneremail',
'recipientemail',
'firstname',
'lastname',
'fullname',
'phone',
'phonenumber',
'password',
'token',
'secret',
'auth',
'authorization',
'bearer',
'apikey',
'stripekey',
'awskey',
'accesskey',
'privatekey',
];

for (const [key, value] of Object.entries(data)) {
const lowerKey = key.toLowerCase();

if (sensitiveKeys.some((sk) => lowerKey.includes(sk))) {
if (typeof value === 'string' && value.includes('@')) {
sanitized[key] = sanitizeEmail(value);
} else {
sanitized[key] = '***';
}
} else {
sanitized[key] = sanitizePii(value);
}
}
return sanitized;
}

return data;
}
29 changes: 10 additions & 19 deletions src/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { NotificationsGateway } from './notifications.gateway';
import { NotificationTemplatesService } from './notification-templates.service';
import { PreferencesService } from './preferences/preferences.service';
import { EmailService } from './email/email.service';
import { sanitizeEmail } from '../common/utils/pii-sanitizer.utils';

@Injectable()
export class NotificationsService {
Expand All @@ -30,10 +31,10 @@ export class NotificationsService {
async sendVerificationEmail(email: string, token: string): Promise<void> {
try {
await this.emailService.sendVerificationEmail(email, token);
this.logger.log(`Verification email sent to ${email}`);
this.logger.log(`Verification email sent to ${sanitizeEmail(email)}`);
} catch (error) {
this.logger.error(
`Failed to send verification email to ${email}`,
`Failed to send verification email to ${sanitizeEmail(email)}`,
error instanceof Error ? error.stack : String(error),
);
throw error;
Expand All @@ -43,10 +44,10 @@ export class NotificationsService {
async sendPasswordResetEmail(email: string, token: string): Promise<void> {
try {
await this.emailService.sendPasswordResetEmail(email, token);
this.logger.log(`Password reset email sent to ${email}`);
this.logger.log(`Password reset email sent to ${sanitizeEmail(email)}`);
} catch (error) {
this.logger.error(
`Failed to send password reset email to ${email}`,
`Failed to send password reset email to ${sanitizeEmail(email)}`,
error instanceof Error ? error.stack : String(error),
);
throw error;
Expand All @@ -60,15 +61,10 @@ export class NotificationsService {
const { userId, title, content, type, priority, metadata } = createNotificationDto;

const preferences = await this.preferencesService.getPreferences(userId);
const shouldSend = this.shouldSendNotification(
type || NotificationType.IN_APP,
preferences,
);
const shouldSend = this.shouldSendNotification(type || NotificationType.IN_APP, preferences);

if (!shouldSend) {
this.logger.debug(
`Notification skipped for user ${userId} based on preferences`,
);
this.logger.debug(`Notification skipped for user ${userId} based on preferences`);
}

const notification = this.notificationRepository.create({
Expand Down Expand Up @@ -123,9 +119,7 @@ export class NotificationsService {
// Integrate with EmailService or MailerService here if notification email delivery is required
}

private async sendExternalPushNotification(
notification: Notification,
): Promise<void> {
private async sendExternalPushNotification(notification: Notification): Promise<void> {
this.logger.log(
`Sending external push notification to user ${notification.userId}: ${notification.title}`,
);
Expand Down Expand Up @@ -248,10 +242,7 @@ export class NotificationsService {
data: any;
type?: NotificationType;
}): Promise<void> {
const template = this.templatesService.renderTemplate(
payload.templateType,
payload.data,
);
const template = this.templatesService.renderTemplate(payload.templateType, payload.data);

await this.create({
userId: payload.userId,
Expand All @@ -261,4 +252,4 @@ export class NotificationsService {
priority: NotificationPriority.MEDIUM,
});
}
}
}
7 changes: 5 additions & 2 deletions src/queues/processors/default-queue.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Processor, Process, OnQueueActive, OnQueueCompleted, OnQueueFailed } fr
import { Logger } from '@nestjs/common';
import { Job } from 'bull';
import { RetryLogicService } from '../retry/retry-logic.service';
import { sanitizeEmail, sanitizePii } from '../../common/utils/pii-sanitizer.utils';

/**
* Default Queue Processor
Expand Down Expand Up @@ -54,7 +55,7 @@ export class DefaultQueueProcessor {
private async processSendEmail(job: Job): Promise<any> {
await job.progress(30);
// Email sending logic here
this.logger.log(`Sending email to ${job.data.to}`);
this.logger.log(`Sending email to ${sanitizeEmail(job.data.to)}`);
await this.simulateWork(2000);
await job.progress(80);
return { status: 'sent', recipient: job.data.to };
Expand Down Expand Up @@ -99,7 +100,9 @@ export class DefaultQueueProcessor {
onCompleted(job: Job, result: any) {
const processingTime = (job.finishedOn ?? Date.now()) - (job.processedOn ?? Date.now());
this.logger.log(
`Job ${job.name} (${job.id}) completed in ${processingTime}ms - Result: ${JSON.stringify(result)}`,
`Job ${job.name} (${job.id}) completed in ${processingTime}ms - Result: ${JSON.stringify(
sanitizePii(result),
)}`,
);
}

Expand Down
Loading