ET-AUDIT-BACKEND: Servicios y API REST
Identificacion
| Campo |
Valor |
| ID |
ET-AUDIT-BACKEND |
| Modulo |
MGN-007 Audit |
| Version |
1.0 |
| Estado |
En Diseno |
| Framework |
NestJS |
| Autor |
Requirements-Analyst |
| Fecha |
2025-12-05 |
Estructura de Archivos
apps/backend/src/modules/audit/
├── audit.module.ts
├── controllers/
│ ├── audit-logs.controller.ts
│ ├── access-logs.controller.ts
│ ├── security-events.controller.ts
│ └── scheduled-reports.controller.ts
├── services/
│ ├── audit-logs.service.ts
│ ├── access-logs.service.ts
│ ├── security-events.service.ts
│ ├── security-alerts.service.ts
│ └── scheduled-reports.service.ts
├── entities/
│ ├── audit-log.entity.ts
│ ├── access-log.entity.ts
│ ├── security-event.entity.ts
│ ├── security-alert.entity.ts
│ ├── scheduled-report.entity.ts
│ └── audit-alert.entity.ts
├── dto/
│ ├── query-audit-logs.dto.ts
│ ├── query-access-logs.dto.ts
│ ├── create-security-event.dto.ts
│ ├── resolve-security-event.dto.ts
│ ├── create-scheduled-report.dto.ts
│ └── export-audit.dto.ts
├── interceptors/
│ ├── audit.interceptor.ts
│ └── access-log.interceptor.ts
├── subscribers/
│ └── entity-audit.subscriber.ts
└── jobs/
├── partition-maintenance.job.ts
└── scheduled-reports.job.ts
Entidades
AuditLog Entity
@Entity('audit_logs', { schema: 'core_audit' })
export class AuditLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string;
@Column({ length: 20 })
action: AuditAction;
@Column({ name: 'entity_type', length: 100 })
entityType: string;
@Column({ name: 'entity_id', type: 'uuid' })
entityId: string;
@Column({ name: 'old_values', type: 'jsonb', nullable: true })
oldValues: Record<string, any>;
@Column({ name: 'new_values', type: 'jsonb', nullable: true })
newValues: Record<string, any>;
@Column({ name: 'changed_fields', type: 'text', array: true, nullable: true })
changedFields: string[];
@Column({ name: 'ip_address', type: 'inet', nullable: true })
ipAddress: string;
@Column({ name: 'user_agent', type: 'text', nullable: true })
userAgent: string;
@Column({ name: 'request_id', type: 'uuid', nullable: true })
requestId: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
}
export type AuditAction = 'create' | 'update' | 'delete' | 'restore';
SecurityEvent Entity
@Entity('security_events', { schema: 'core_audit' })
export class SecurityEvent {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId: string;
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string;
@Column({ name: 'event_type', length: 50 })
eventType: SecurityEventType;
@Column({ length: 20 })
severity: SecuritySeverity;
@Column({ length: 255 })
title: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@Column({ name: 'ip_address', type: 'inet', nullable: true })
ipAddress: string;
@Column({ name: 'country_code', length: 2, nullable: true })
countryCode: string;
@Column({ name: 'is_resolved', default: false })
isResolved: boolean;
@Column({ name: 'resolved_by', type: 'uuid', nullable: true })
resolvedBy: string;
@Column({ name: 'resolved_at', type: 'timestamptz', nullable: true })
resolvedAt: Date;
@Column({ name: 'resolution_notes', type: 'text', nullable: true })
resolutionNotes: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@OneToMany(() => SecurityAlert, a => a.securityEvent)
alerts: SecurityAlert[];
}
export type SecuritySeverity = 'low' | 'medium' | 'high' | 'critical';
export type SecurityEventType =
| 'login_failed' | 'login_blocked' | 'brute_force_detected'
| 'password_changed' | 'password_reset_requested'
| 'session_hijack_attempt' | 'concurrent_session'
| 'session_from_new_location' | 'session_from_new_device'
| 'permission_escalation' | 'role_changed' | 'admin_created'
| 'mass_delete_attempt' | 'data_export' | 'sensitive_data_access'
| 'api_key_created' | 'webhook_modified' | 'settings_changed';
ScheduledReport Entity
@Entity('scheduled_reports', { schema: 'core_audit' })
export class ScheduledReport {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ length: 255 })
name: string;
@Column({ name: 'report_type', length: 50 })
reportType: AuditReportType;
@Column({ type: 'jsonb', default: {} })
filters: ReportFilters;
@Column({ length: 10, default: 'pdf' })
format: ExportFormat;
@Column({ length: 100 })
schedule: string; // cron expression
@Column({ type: 'text', array: true })
recipients: string[];
@Column({ name: 'is_active', default: true })
isActive: boolean;
@Column({ name: 'last_run_at', type: 'timestamptz', nullable: true })
lastRunAt: Date;
@Column({ name: 'next_run_at', type: 'timestamptz', nullable: true })
nextRunAt: Date;
@Column({ name: 'created_by', type: 'uuid' })
createdBy: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
}
export type AuditReportType = 'activity' | 'security' | 'compliance' | 'change' | 'access';
export type ExportFormat = 'csv' | 'xlsx' | 'pdf' | 'json';
Servicios
AuditLogsService
@Injectable()
export class AuditLogsService {
constructor(
@InjectRepository(AuditLog)
private readonly repo: Repository<AuditLog>,
@InjectDataSource()
private readonly dataSource: DataSource,
) {}
async findAll(
tenantId: string,
query: QueryAuditLogsDto
): Promise<PaginatedResult<AuditLog>> {
const qb = this.repo.createQueryBuilder('log')
.where('log.tenant_id = :tenantId', { tenantId });
if (query.entityType) {
qb.andWhere('log.entity_type = :entityType', { entityType: query.entityType });
}
if (query.entityId) {
qb.andWhere('log.entity_id = :entityId', { entityId: query.entityId });
}
if (query.userId) {
qb.andWhere('log.user_id = :userId', { userId: query.userId });
}
if (query.action) {
qb.andWhere('log.action = :action', { action: query.action });
}
if (query.dateFrom) {
qb.andWhere('log.created_at >= :dateFrom', { dateFrom: query.dateFrom });
}
if (query.dateTo) {
qb.andWhere('log.created_at <= :dateTo', { dateTo: query.dateTo });
}
qb.orderBy('log.created_at', 'DESC');
return paginate(qb, query);
}
async getEntityHistory(
tenantId: string,
entityType: string,
entityId: string
): Promise<AuditLog[]> {
return this.repo.find({
where: { tenantId, entityType, entityId },
order: { createdAt: 'DESC' },
});
}
async log(data: CreateAuditLogDto): Promise<AuditLog> {
const log = this.repo.create(data);
return this.repo.save(log);
}
async getStatsByAction(
tenantId: string,
dateFrom: Date,
dateTo: Date
): Promise<ActionStats[]> {
const result = await this.repo
.createQueryBuilder('log')
.select('log.action', 'action')
.addSelect('COUNT(*)', 'count')
.where('log.tenant_id = :tenantId', { tenantId })
.andWhere('log.created_at BETWEEN :dateFrom AND :dateTo', { dateFrom, dateTo })
.groupBy('log.action')
.getRawMany();
return result;
}
async getStatsByEntity(
tenantId: string,
dateFrom: Date,
dateTo: Date
): Promise<EntityStats[]> {
const result = await this.repo
.createQueryBuilder('log')
.select('log.entity_type', 'entityType')
.addSelect('COUNT(*)', 'count')
.where('log.tenant_id = :tenantId', { tenantId })
.andWhere('log.created_at BETWEEN :dateFrom AND :dateTo', { dateFrom, dateTo })
.groupBy('log.entity_type')
.orderBy('COUNT(*)', 'DESC')
.limit(10)
.getRawMany();
return result;
}
}
SecurityEventsService
@Injectable()
export class SecurityEventsService {
constructor(
@InjectRepository(SecurityEvent)
private readonly repo: Repository<SecurityEvent>,
private readonly alertsService: SecurityAlertsService,
private readonly notificationsService: NotificationsService,
) {}
async findAll(
tenantId: string | null,
query: QuerySecurityEventsDto
): Promise<PaginatedResult<SecurityEvent>> {
const qb = this.repo.createQueryBuilder('event');
if (tenantId) {
qb.where('event.tenant_id = :tenantId', { tenantId });
}
if (query.severity) {
qb.andWhere('event.severity = :severity', { severity: query.severity });
}
if (query.eventType) {
qb.andWhere('event.event_type = :eventType', { eventType: query.eventType });
}
if (query.isResolved !== undefined) {
qb.andWhere('event.is_resolved = :isResolved', { isResolved: query.isResolved });
}
if (query.dateFrom) {
qb.andWhere('event.created_at >= :dateFrom', { dateFrom: query.dateFrom });
}
qb.orderBy('event.created_at', 'DESC');
return paginate(qb, query);
}
async create(dto: CreateSecurityEventDto): Promise<SecurityEvent> {
const event = this.repo.create(dto);
const saved = await this.repo.save(event);
// Auto-alert for high/critical severity
if (['high', 'critical'].includes(dto.severity)) {
await this.triggerAlerts(saved);
}
return saved;
}
async resolve(
id: string,
userId: string,
dto: ResolveSecurityEventDto
): Promise<SecurityEvent> {
const event = await this.repo.findOneOrFail({ where: { id } });
event.isResolved = true;
event.resolvedBy = userId;
event.resolvedAt = new Date();
event.resolutionNotes = dto.notes;
return this.repo.save(event);
}
async getUnresolvedCount(tenantId?: string): Promise<SeverityCounts> {
const qb = this.repo.createQueryBuilder('event')
.select('event.severity', 'severity')
.addSelect('COUNT(*)', 'count')
.where('event.is_resolved = false');
if (tenantId) {
qb.andWhere('event.tenant_id = :tenantId', { tenantId });
}
qb.groupBy('event.severity');
const results = await qb.getRawMany();
return {
low: parseInt(results.find(r => r.severity === 'low')?.count || '0'),
medium: parseInt(results.find(r => r.severity === 'medium')?.count || '0'),
high: parseInt(results.find(r => r.severity === 'high')?.count || '0'),
critical: parseInt(results.find(r => r.severity === 'critical')?.count || '0'),
};
}
private async triggerAlerts(event: SecurityEvent): Promise<void> {
// Get admin users to notify
const admins = await this.getSecurityAdmins(event.tenantId);
const alert = await this.alertsService.create({
securityEventId: event.id,
sentTo: admins.map(a => a.id),
channels: ['in_app', 'email'],
});
// Send notifications
for (const admin of admins) {
await this.notificationsService.send({
userId: admin.id,
type: 'security_alert',
title: `Security Alert: ${event.title}`,
body: event.description,
data: { eventId: event.id, severity: event.severity },
channels: ['in_app', 'email'],
});
}
}
}
AccessLogsService
@Injectable()
export class AccessLogsService {
constructor(
@InjectRepository(AccessLog)
private readonly repo: Repository<AccessLog>,
) {}
async findAll(
tenantId: string | null,
query: QueryAccessLogsDto
): Promise<PaginatedResult<AccessLog>> {
const qb = this.repo.createQueryBuilder('log');
if (tenantId) {
qb.where('log.tenant_id = :tenantId', { tenantId });
}
if (query.userId) {
qb.andWhere('log.user_id = :userId', { userId: query.userId });
}
if (query.path) {
qb.andWhere('log.path LIKE :path', { path: `%${query.path}%` });
}
if (query.method) {
qb.andWhere('log.method = :method', { method: query.method });
}
if (query.statusCode) {
qb.andWhere('log.status_code = :statusCode', { statusCode: query.statusCode });
}
if (query.minResponseTime) {
qb.andWhere('log.response_time_ms >= :minResponseTime', {
minResponseTime: query.minResponseTime
});
}
if (query.dateFrom) {
qb.andWhere('log.created_at >= :dateFrom', { dateFrom: query.dateFrom });
}
if (query.dateTo) {
qb.andWhere('log.created_at <= :dateTo', { dateTo: query.dateTo });
}
qb.orderBy('log.created_at', 'DESC');
return paginate(qb, query);
}
async log(data: CreateAccessLogDto): Promise<void> {
// Insert async - no wait for performance
this.repo.insert(data).catch(err => {
console.error('Failed to log access:', err);
});
}
async getAggregatedStats(
tenantId: string,
dateFrom: Date,
dateTo: Date,
groupBy: 'hour' | 'day'
): Promise<AccessStats[]> {
const dateFormat = groupBy === 'hour'
? "to_char(log.created_at, 'YYYY-MM-DD HH24:00')"
: "to_char(log.created_at, 'YYYY-MM-DD')";
const result = await this.repo
.createQueryBuilder('log')
.select(dateFormat, 'period')
.addSelect('COUNT(*)', 'totalRequests')
.addSelect('AVG(log.response_time_ms)', 'avgResponseTime')
.addSelect('COUNT(*) FILTER (WHERE log.status_code >= 400)', 'errorCount')
.where('log.tenant_id = :tenantId', { tenantId })
.andWhere('log.created_at BETWEEN :dateFrom AND :dateTo', { dateFrom, dateTo })
.groupBy('period')
.orderBy('period', 'ASC')
.getRawMany();
return result;
}
async getTopEndpoints(
tenantId: string,
dateFrom: Date,
limit: number = 10
): Promise<EndpointStats[]> {
return this.repo
.createQueryBuilder('log')
.select('log.method', 'method')
.addSelect('log.path', 'path')
.addSelect('COUNT(*)', 'count')
.addSelect('AVG(log.response_time_ms)', 'avgResponseTime')
.where('log.tenant_id = :tenantId', { tenantId })
.andWhere('log.created_at >= :dateFrom', { dateFrom })
.groupBy('log.method, log.path')
.orderBy('COUNT(*)', 'DESC')
.limit(limit)
.getRawMany();
}
}
ScheduledReportsService
@Injectable()
export class ScheduledReportsService {
constructor(
@InjectRepository(ScheduledReport)
private readonly repo: Repository<ScheduledReport>,
private readonly auditLogsService: AuditLogsService,
private readonly accessLogsService: AccessLogsService,
private readonly securityEventsService: SecurityEventsService,
private readonly emailService: EmailService,
private readonly exportService: ExportService,
) {}
async findAll(tenantId: string): Promise<ScheduledReport[]> {
return this.repo.find({
where: { tenantId },
order: { createdAt: 'DESC' },
});
}
async create(
tenantId: string,
userId: string,
dto: CreateScheduledReportDto
): Promise<ScheduledReport> {
const report = this.repo.create({
...dto,
tenantId,
createdBy: userId,
nextRunAt: this.calculateNextRun(dto.schedule),
});
return this.repo.save(report);
}
async update(
id: string,
dto: UpdateScheduledReportDto
): Promise<ScheduledReport> {
const report = await this.repo.findOneOrFail({ where: { id } });
Object.assign(report, dto);
if (dto.schedule) {
report.nextRunAt = this.calculateNextRun(dto.schedule);
}
return this.repo.save(report);
}
async delete(id: string): Promise<void> {
await this.repo.delete(id);
}
async runReport(id: string): Promise<Buffer> {
const report = await this.repo.findOneOrFail({ where: { id } });
const data = await this.fetchReportData(report);
return this.exportService.export(data, report.format, report.reportType);
}
@Cron('0 * * * *') // Every hour
async processScheduledReports(): Promise<void> {
const dueReports = await this.repo.find({
where: {
isActive: true,
nextRunAt: LessThanOrEqual(new Date()),
},
});
for (const report of dueReports) {
try {
const data = await this.fetchReportData(report);
const file = await this.exportService.export(data, report.format, report.reportType);
await this.emailService.sendWithAttachment({
to: report.recipients,
subject: `Audit Report: ${report.name}`,
body: `Attached is your scheduled ${report.reportType} report.`,
attachment: {
filename: `${report.name}_${format(new Date(), 'yyyy-MM-dd')}.${report.format}`,
content: file,
},
});
report.lastRunAt = new Date();
report.nextRunAt = this.calculateNextRun(report.schedule);
await this.repo.save(report);
} catch (error) {
console.error(`Failed to run report ${report.id}:`, error);
}
}
}
private async fetchReportData(report: ScheduledReport): Promise<any[]> {
const { dateFrom, dateTo } = this.getDateRange(report.filters);
switch (report.reportType) {
case 'activity':
case 'change':
return this.auditLogsService.findAll(report.tenantId, {
dateFrom,
dateTo,
...report.filters,
limit: 10000,
}).then(r => r.items);
case 'access':
return this.accessLogsService.findAll(report.tenantId, {
dateFrom,
dateTo,
...report.filters,
limit: 10000,
}).then(r => r.items);
case 'security':
return this.securityEventsService.findAll(report.tenantId, {
dateFrom,
dateTo,
...report.filters,
limit: 10000,
}).then(r => r.items);
default:
return [];
}
}
private calculateNextRun(cronExpression: string): Date {
const interval = parseExpression(cronExpression);
return interval.next().toDate();
}
}
Interceptores
AuditInterceptor
@Injectable()
export class AuditInterceptor implements NestInterceptor {
constructor(
private readonly auditService: AuditLogsService,
private readonly cls: ClsService,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const handler = context.getHandler();
const auditConfig = Reflect.getMetadata('audit', handler);
if (!auditConfig) {
return next.handle();
}
return next.handle().pipe(
tap(async (response) => {
try {
await this.auditService.log({
tenantId: request.tenantId,
userId: request.user?.id,
action: auditConfig.action,
entityType: auditConfig.entityType,
entityId: response?.id || request.params?.id,
oldValues: this.cls.get('oldValues'),
newValues: response,
ipAddress: request.ip,
userAgent: request.headers['user-agent'],
requestId: request.id,
});
} catch (error) {
console.error('Audit log failed:', error);
}
}),
);
}
}
// Decorator
export const Audit = (config: { action: AuditAction; entityType: string }) =>
SetMetadata('audit', config);
// Usage
@Post()
@Audit({ action: 'create', entityType: 'contact' })
async create(@Body() dto: CreateContactDto) { ... }
AccessLogInterceptor
@Injectable()
export class AccessLogInterceptor implements NestInterceptor {
constructor(private readonly accessLogsService: AccessLogsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const startTime = Date.now();
return next.handle().pipe(
finalize(() => {
const response = context.switchToHttp().getResponse();
this.accessLogsService.log({
tenantId: request.tenantId,
userId: request.user?.id,
sessionId: request.sessionId,
requestId: request.id,
method: request.method,
path: request.route?.path || request.path,
queryParams: request.query,
statusCode: response.statusCode,
responseTimeMs: Date.now() - startTime,
ipAddress: request.ip,
userAgent: request.headers['user-agent'],
referer: request.headers['referer'],
});
}),
);
}
}
TypeORM Subscriber
EntityAuditSubscriber
@EventSubscriber()
export class EntityAuditSubscriber implements EntitySubscriberInterface {
private readonly auditableEntities = [
'Contact', 'User', 'Role', 'Tenant', 'Setting',
];
constructor(
dataSource: DataSource,
private readonly auditService: AuditLogsService,
private readonly cls: ClsService,
) {
dataSource.subscribers.push(this);
}
listenTo() {
return Object.class; // Listen to all entities
}
beforeUpdate(event: UpdateEvent<any>): void {
if (this.shouldAudit(event.metadata.name)) {
// Store old values for comparison
this.cls.set('oldValues', event.databaseEntity);
}
}
afterInsert(event: InsertEvent<any>): Promise<void> {
return this.logChange(event, 'create');
}
afterUpdate(event: UpdateEvent<any>): Promise<void> {
return this.logChange(event, 'update');
}
afterRemove(event: RemoveEvent<any>): Promise<void> {
return this.logChange(event, 'delete');
}
afterSoftRemove(event: SoftRemoveEvent<any>): Promise<void> {
return this.logChange(event, 'delete');
}
afterRecover(event: RecoverEvent<any>): Promise<void> {
return this.logChange(event, 'restore');
}
private shouldAudit(entityName: string): boolean {
return this.auditableEntities.includes(entityName);
}
private async logChange(
event: any,
action: AuditAction
): Promise<void> {
if (!this.shouldAudit(event.metadata.name)) {
return;
}
const entity = event.entity || event.databaseEntity;
if (!entity) return;
try {
await this.auditService.log({
tenantId: this.cls.get('tenantId'),
userId: this.cls.get('userId'),
action,
entityType: event.metadata.tableName,
entityId: entity.id,
oldValues: action === 'update' ? this.cls.get('oldValues') : null,
newValues: action === 'delete' ? null : entity,
ipAddress: this.cls.get('ipAddress'),
requestId: this.cls.get('requestId'),
});
} catch (error) {
console.error('Entity audit failed:', error);
}
}
}
Controladores
AuditLogsController
@ApiTags('Audit Logs')
@Controller('audit/logs')
@UseGuards(JwtAuthGuard, RbacGuard)
export class AuditLogsController {
constructor(private readonly service: AuditLogsService) {}
@Get()
@Permissions('audit.logs.read')
async findAll(
@TenantId() tenantId: string,
@Query() query: QueryAuditLogsDto
) {
return this.service.findAll(tenantId, query);
}
@Get('entity/:entityType/:entityId')
@Permissions('audit.logs.read')
async getEntityHistory(
@TenantId() tenantId: string,
@Param('entityType') entityType: string,
@Param('entityId') entityId: string
) {
return this.service.getEntityHistory(tenantId, entityType, entityId);
}
@Get('stats/actions')
@Permissions('audit.logs.read')
async getStatsByAction(
@TenantId() tenantId: string,
@Query('dateFrom') dateFrom: Date,
@Query('dateTo') dateTo: Date
) {
return this.service.getStatsByAction(tenantId, dateFrom, dateTo);
}
@Get('stats/entities')
@Permissions('audit.logs.read')
async getStatsByEntity(
@TenantId() tenantId: string,
@Query('dateFrom') dateFrom: Date,
@Query('dateTo') dateTo: Date
) {
return this.service.getStatsByEntity(tenantId, dateFrom, dateTo);
}
@Post('export')
@Permissions('audit.logs.export')
async export(
@TenantId() tenantId: string,
@Body() dto: ExportAuditDto
) {
const data = await this.service.findAll(tenantId, dto);
return this.service.export(data.items, dto.format);
}
}
SecurityEventsController
@ApiTags('Security Events')
@Controller('audit/security')
@UseGuards(JwtAuthGuard, RbacGuard)
export class SecurityEventsController {
constructor(private readonly service: SecurityEventsService) {}
@Get()
@Permissions('audit.security.read')
async findAll(
@TenantId() tenantId: string,
@Query() query: QuerySecurityEventsDto
) {
return this.service.findAll(tenantId, query);
}
@Get('counts')
@Permissions('audit.security.read')
async getUnresolvedCounts(@TenantId() tenantId: string) {
return this.service.getUnresolvedCount(tenantId);
}
@Patch(':id/resolve')
@Permissions('audit.security.manage')
async resolve(
@Param('id') id: string,
@CurrentUser() user: User,
@Body() dto: ResolveSecurityEventDto
) {
return this.service.resolve(id, user.id, dto);
}
}
// Admin controller for global events
@ApiTags('Security Events Admin')
@Controller('admin/audit/security')
@UseGuards(JwtAuthGuard, RbacGuard)
@Permissions('admin.security.manage')
export class SecurityEventsAdminController {
constructor(private readonly service: SecurityEventsService) {}
@Get()
async findAll(@Query() query: QuerySecurityEventsDto) {
return this.service.findAll(null, query);
}
@Get('counts')
async getGlobalCounts() {
return this.service.getUnresolvedCount();
}
}
ScheduledReportsController
@ApiTags('Audit Reports')
@Controller('audit/reports')
@UseGuards(JwtAuthGuard, RbacGuard)
export class ScheduledReportsController {
constructor(private readonly service: ScheduledReportsService) {}
@Get()
@Permissions('audit.reports.read')
async findAll(@TenantId() tenantId: string) {
return this.service.findAll(tenantId);
}
@Post()
@Permissions('audit.reports.manage')
async create(
@TenantId() tenantId: string,
@CurrentUser() user: User,
@Body() dto: CreateScheduledReportDto
) {
return this.service.create(tenantId, user.id, dto);
}
@Patch(':id')
@Permissions('audit.reports.manage')
async update(
@Param('id') id: string,
@Body() dto: UpdateScheduledReportDto
) {
return this.service.update(id, dto);
}
@Delete(':id')
@Permissions('audit.reports.manage')
async delete(@Param('id') id: string) {
return this.service.delete(id);
}
@Post(':id/run')
@Permissions('audit.reports.manage')
async runNow(@Param('id') id: string, @Res() res: Response) {
const buffer = await this.service.runReport(id);
res.setHeader('Content-Type', 'application/octet-stream');
res.send(buffer);
}
}
DTOs
Query DTOs
export class QueryAuditLogsDto extends PaginationDto {
@IsOptional()
@IsString()
entityType?: string;
@IsOptional()
@IsUUID()
entityId?: string;
@IsOptional()
@IsUUID()
userId?: string;
@IsOptional()
@IsIn(['create', 'update', 'delete', 'restore'])
action?: AuditAction;
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
}
export class QuerySecurityEventsDto extends PaginationDto {
@IsOptional()
@IsIn(['low', 'medium', 'high', 'critical'])
severity?: SecuritySeverity;
@IsOptional()
@IsString()
eventType?: string;
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true')
isResolved?: boolean;
@IsOptional()
@IsDateString()
dateFrom?: string;
}
Create/Update DTOs
export class CreateScheduledReportDto {
@IsString()
@Length(1, 255)
name: string;
@IsIn(['activity', 'security', 'compliance', 'change', 'access'])
reportType: AuditReportType;
@IsObject()
filters: Record<string, any>;
@IsIn(['csv', 'xlsx', 'pdf', 'json'])
@IsOptional()
format?: ExportFormat = 'pdf';
@IsString()
schedule: string; // cron expression
@IsArray()
@IsEmail({}, { each: true })
recipients: string[];
}
export class ResolveSecurityEventDto {
@IsString()
@IsOptional()
notes?: string;
}
API Endpoints Summary
| Method |
Path |
Permission |
Description |
| GET |
/audit/logs |
audit.logs.read |
List audit logs |
| GET |
/audit/logs/entity/:type/:id |
audit.logs.read |
Get entity history |
| GET |
/audit/logs/stats/actions |
audit.logs.read |
Stats by action |
| GET |
/audit/logs/stats/entities |
audit.logs.read |
Stats by entity type |
| POST |
/audit/logs/export |
audit.logs.export |
Export logs |
| GET |
/audit/access |
audit.access.read |
List access logs |
| GET |
/audit/access/stats |
audit.access.read |
Access statistics |
| GET |
/audit/access/endpoints |
audit.access.read |
Top endpoints |
| GET |
/audit/security |
audit.security.read |
List security events |
| GET |
/audit/security/counts |
audit.security.read |
Unresolved counts |
| PATCH |
/audit/security/:id/resolve |
audit.security.manage |
Resolve event |
| GET |
/audit/reports |
audit.reports.read |
List scheduled reports |
| POST |
/audit/reports |
audit.reports.manage |
Create report |
| PATCH |
/audit/reports/:id |
audit.reports.manage |
Update report |
| DELETE |
/audit/reports/:id |
audit.reports.manage |
Delete report |
| POST |
/audit/reports/:id/run |
audit.reports.manage |
Run report now |
Historial
| Version |
Fecha |
Autor |
Cambios |
| 1.0 |
2025-12-05 |
Requirements-Analyst |
Creacion inicial |