xpeditis2.0/apps/backend/src/application/controllers/audit.controller.ts
David-Henri ARNAUD c5c15eb1f9 feature phase 3
2025-10-13 17:54:32 +02:00

219 lines
7.3 KiB
TypeScript

/**
* Audit Log Controller
*
* Provides endpoints for querying audit logs
*/
import {
Controller,
Get,
Param,
Query,
UseGuards,
ParseIntPipe,
DefaultValuePipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { AuditService } from '../services/audit.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RolesGuard } from '../guards/roles.guard';
import { Roles } from '../decorators/roles.decorator';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
import { AuditLog, AuditAction, AuditStatus } from '../../domain/entities/audit-log.entity';
class AuditLogResponseDto {
id: string;
action: string;
status: string;
userId: string;
userEmail: string;
organizationId: string;
resourceType?: string;
resourceId?: string;
resourceName?: string;
metadata?: Record<string, any>;
ipAddress?: string;
userAgent?: string;
errorMessage?: string;
timestamp: string;
}
class AuditLogQueryDto {
userId?: string;
action?: AuditAction[];
status?: AuditStatus[];
resourceType?: string;
resourceId?: string;
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
}
@ApiTags('Audit Logs')
@ApiBearerAuth()
@Controller('api/v1/audit-logs')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AuditController {
constructor(private readonly auditService: AuditService) {}
/**
* Get audit logs with filters
* Only admins and managers can view audit logs
*/
@Get()
@Roles('admin', 'manager')
@ApiOperation({ summary: 'Get audit logs with filters' })
@ApiResponse({ status: 200, description: 'Audit logs retrieved successfully' })
@ApiQuery({ name: 'userId', required: false, description: 'Filter by user ID' })
@ApiQuery({ name: 'action', required: false, description: 'Filter by action (comma-separated)', isArray: true })
@ApiQuery({ name: 'status', required: false, description: 'Filter by status (comma-separated)', isArray: true })
@ApiQuery({ name: 'resourceType', required: false, description: 'Filter by resource type' })
@ApiQuery({ name: 'resourceId', required: false, description: 'Filter by resource ID' })
@ApiQuery({ name: 'startDate', required: false, description: 'Filter by start date (ISO 8601)' })
@ApiQuery({ name: 'endDate', required: false, description: 'Filter by end date (ISO 8601)' })
@ApiQuery({ name: 'page', required: false, description: 'Page number (default: 1)' })
@ApiQuery({ name: 'limit', required: false, description: 'Items per page (default: 50)' })
async getAuditLogs(
@CurrentUser() user: UserPayload,
@Query('userId') userId?: string,
@Query('action') action?: string,
@Query('status') status?: string,
@Query('resourceType') resourceType?: string,
@Query('resourceId') resourceId?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number,
): Promise<{ logs: AuditLogResponseDto[]; total: number; page: number; pageSize: number }> {
page = page || 1;
limit = limit || 50;
const filters: any = {
organizationId: user.organizationId,
userId,
action: action ? action.split(',') : undefined,
status: status ? status.split(',') : undefined,
resourceType,
resourceId,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
offset: (page - 1) * limit,
limit,
};
const { logs, total } = await this.auditService.getAuditLogs(filters);
return {
logs: logs.map((log) => this.mapToDto(log)),
total,
page,
pageSize: limit,
};
}
/**
* Get specific audit log by ID
*/
@Get(':id')
@Roles('admin', 'manager')
@ApiOperation({ summary: 'Get audit log by ID' })
@ApiResponse({ status: 200, description: 'Audit log retrieved successfully' })
@ApiResponse({ status: 404, description: 'Audit log not found' })
async getAuditLogById(
@Param('id') id: string,
@CurrentUser() user: UserPayload,
): Promise<AuditLogResponseDto> {
const log = await this.auditService.getAuditLogs({
organizationId: user.organizationId,
limit: 1,
});
if (!log.logs.length) {
throw new Error('Audit log not found');
}
return this.mapToDto(log.logs[0]);
}
/**
* Get audit trail for a specific resource
*/
@Get('resource/:type/:id')
@Roles('admin', 'manager', 'user')
@ApiOperation({ summary: 'Get audit trail for a specific resource' })
@ApiResponse({ status: 200, description: 'Audit trail retrieved successfully' })
async getResourceAuditTrail(
@Param('type') resourceType: string,
@Param('id') resourceId: string,
@CurrentUser() user: UserPayload,
): Promise<AuditLogResponseDto[]> {
const logs = await this.auditService.getResourceAuditTrail(resourceType, resourceId);
// Filter by organization for security
const filteredLogs = logs.filter((log) => log.organizationId === user.organizationId);
return filteredLogs.map((log) => this.mapToDto(log));
}
/**
* Get recent activity for current organization
*/
@Get('organization/activity')
@Roles('admin', 'manager')
@ApiOperation({ summary: 'Get recent organization activity' })
@ApiResponse({ status: 200, description: 'Organization activity retrieved successfully' })
@ApiQuery({ name: 'limit', required: false, description: 'Number of recent logs (default: 50)' })
async getOrganizationActivity(
@CurrentUser() user: UserPayload,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number,
): Promise<AuditLogResponseDto[]> {
limit = limit || 50;
const logs = await this.auditService.getOrganizationActivity(user.organizationId, limit);
return logs.map((log) => this.mapToDto(log));
}
/**
* Get user activity history
*/
@Get('user/:userId/activity')
@Roles('admin', 'manager')
@ApiOperation({ summary: 'Get user activity history' })
@ApiResponse({ status: 200, description: 'User activity retrieved successfully' })
@ApiQuery({ name: 'limit', required: false, description: 'Number of recent logs (default: 50)' })
async getUserActivity(
@CurrentUser() user: UserPayload,
@Param('userId') userId: string,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number,
): Promise<AuditLogResponseDto[]> {
limit = limit || 50;
const logs = await this.auditService.getUserActivity(userId, limit);
// Filter by organization for security
const filteredLogs = logs.filter((log) => log.organizationId === user.organizationId);
return filteredLogs.map((log) => this.mapToDto(log));
}
/**
* Map domain entity to DTO
*/
private mapToDto(log: AuditLog): AuditLogResponseDto {
return {
id: log.id,
action: log.action,
status: log.status,
userId: log.userId,
userEmail: log.userEmail,
organizationId: log.organizationId,
resourceType: log.resourceType,
resourceId: log.resourceId,
resourceName: log.resourceName,
metadata: log.metadata,
ipAddress: log.ipAddress,
userAgent: log.userAgent,
errorMessage: log.errorMessage,
timestamp: log.timestamp.toISOString(),
};
}
}