166 lines
3.6 KiB
TypeScript
166 lines
3.6 KiB
TypeScript
/**
|
|
* Audit Service
|
|
*
|
|
* Provides centralized audit logging functionality
|
|
* Tracks all important actions for security and compliance
|
|
*/
|
|
|
|
import { Injectable, Logger, Inject } from '@nestjs/common';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import {
|
|
AuditLog,
|
|
AuditAction,
|
|
AuditStatus,
|
|
} from '../../domain/entities/audit-log.entity';
|
|
import {
|
|
AuditLogRepository,
|
|
AUDIT_LOG_REPOSITORY,
|
|
AuditLogFilters,
|
|
} from '../../domain/ports/out/audit-log.repository';
|
|
|
|
export interface LogAuditInput {
|
|
action: AuditAction;
|
|
status: AuditStatus;
|
|
userId: string;
|
|
userEmail: string;
|
|
organizationId: string;
|
|
resourceType?: string;
|
|
resourceId?: string;
|
|
resourceName?: string;
|
|
metadata?: Record<string, any>;
|
|
ipAddress?: string;
|
|
userAgent?: string;
|
|
errorMessage?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class AuditService {
|
|
private readonly logger = new Logger(AuditService.name);
|
|
|
|
constructor(
|
|
@Inject(AUDIT_LOG_REPOSITORY)
|
|
private readonly auditLogRepository: AuditLogRepository,
|
|
) {}
|
|
|
|
/**
|
|
* Log an audit event
|
|
*/
|
|
async log(input: LogAuditInput): Promise<void> {
|
|
try {
|
|
const auditLog = AuditLog.create({
|
|
id: uuidv4(),
|
|
...input,
|
|
});
|
|
|
|
await this.auditLogRepository.save(auditLog);
|
|
|
|
this.logger.log(
|
|
`Audit log created: ${input.action} by ${input.userEmail} (${input.status})`,
|
|
);
|
|
} catch (error: any) {
|
|
// Never throw on audit logging failure - log the error and continue
|
|
this.logger.error(
|
|
`Failed to create audit log: ${error?.message || 'Unknown error'}`,
|
|
error?.stack,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log successful action
|
|
*/
|
|
async logSuccess(
|
|
action: AuditAction,
|
|
userId: string,
|
|
userEmail: string,
|
|
organizationId: string,
|
|
options?: {
|
|
resourceType?: string;
|
|
resourceId?: string;
|
|
resourceName?: string;
|
|
metadata?: Record<string, any>;
|
|
ipAddress?: string;
|
|
userAgent?: string;
|
|
},
|
|
): Promise<void> {
|
|
await this.log({
|
|
action,
|
|
status: AuditStatus.SUCCESS,
|
|
userId,
|
|
userEmail,
|
|
organizationId,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log failed action
|
|
*/
|
|
async logFailure(
|
|
action: AuditAction,
|
|
userId: string,
|
|
userEmail: string,
|
|
organizationId: string,
|
|
errorMessage: string,
|
|
options?: {
|
|
resourceType?: string;
|
|
resourceId?: string;
|
|
metadata?: Record<string, any>;
|
|
ipAddress?: string;
|
|
userAgent?: string;
|
|
},
|
|
): Promise<void> {
|
|
await this.log({
|
|
action,
|
|
status: AuditStatus.FAILURE,
|
|
userId,
|
|
userEmail,
|
|
organizationId,
|
|
errorMessage,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get audit logs with filters
|
|
*/
|
|
async getAuditLogs(filters: AuditLogFilters): Promise<{
|
|
logs: AuditLog[];
|
|
total: number;
|
|
}> {
|
|
const [logs, total] = await Promise.all([
|
|
this.auditLogRepository.findByFilters(filters),
|
|
this.auditLogRepository.count(filters),
|
|
]);
|
|
|
|
return { logs, total };
|
|
}
|
|
|
|
/**
|
|
* Get audit trail for a specific resource
|
|
*/
|
|
async getResourceAuditTrail(
|
|
resourceType: string,
|
|
resourceId: string,
|
|
): Promise<AuditLog[]> {
|
|
return this.auditLogRepository.findByResource(resourceType, resourceId);
|
|
}
|
|
|
|
/**
|
|
* Get recent activity for an organization
|
|
*/
|
|
async getOrganizationActivity(
|
|
organizationId: string,
|
|
limit: number = 50,
|
|
): Promise<AuditLog[]> {
|
|
return this.auditLogRepository.findRecentByOrganization(organizationId, limit);
|
|
}
|
|
|
|
/**
|
|
* Get user activity history
|
|
*/
|
|
async getUserActivity(userId: string, limit: number = 50): Promise<AuditLog[]> {
|
|
return this.auditLogRepository.findByUser(userId, limit);
|
|
}
|
|
}
|