--- task_id: sprint_2_story_2_task_4 story: sprint_2_story_2 status: completed estimated_hours: 5 created_date: 2025-11-05 completed_date: 2025-11-05 assignee: Backend Team --- # Task 4: Implement Audit Query API **Story**: Story 2 - Audit Log Core Features (Phase 2) **Estimated**: 5 hours ## Description Create REST API endpoints to query audit logs with CQRS pattern. Support filtering by entity type, entity ID, date range, and user. ## Acceptance Criteria - [x] GetEntityAuditHistoryQuery implemented - **COMPLETED** - [x] GetAuditLogByIdQuery implemented - **COMPLETED** - [x] AuditLogsController with 3 endpoints created - **COMPLETED** - [x] Query handlers with proper filtering - **COMPLETED** - [x] Swagger documentation added - **COMPLETED** - [x] Integration tests for API endpoints - **PENDING (Task 5)** ## Implementation Summary (2025-11-05) **Status**: ✅ COMPLETED Successfully implemented complete CQRS Query API for Audit Logs: ### Files Created: 1. **DTOs**: - `AuditLogDto.cs` - Transfer object for audit log data 2. **Queries**: - `GetAuditLogById/GetAuditLogByIdQuery.cs` + Handler - `GetAuditLogsByEntity/GetAuditLogsByEntityQuery.cs` + Handler - `GetRecentAuditLogs/GetRecentAuditLogsQuery.cs` + Handler 3. **API Controller**: - `AuditLogsController.cs` with 3 endpoints: - `GET /api/v1/auditlogs/{id}` - Get specific audit log - `GET /api/v1/auditlogs/entity/{entityType}/{entityId}` - Get entity history - `GET /api/v1/auditlogs/recent?count=100` - Get recent logs (max 1000) ### Features: - Multi-tenant isolation via Global Query Filters (automatic) - Read-only query endpoints (no write operations) - Swagger/OpenAPI documentation via attributes - Proper HTTP status codes (200 OK, 404 Not Found) - Cancellation token support - Primary constructor pattern (modern C# style) ## Implementation Details **Files to Create**: 1. **Query DTOs**: `colaflow-api/src/ColaFlow.Application/AuditLog/Queries/GetEntityAuditHistory/GetEntityAuditHistoryQuery.cs` ```csharp public record GetEntityAuditHistoryQuery : IRequest> { public string EntityType { get; init; } = string.Empty; public Guid EntityId { get; init; } public DateTime? FromDate { get; init; } public DateTime? ToDate { get; init; } public Guid? UserId { get; init; } public int? Limit { get; init; } = 50; // Default limit } public class AuditLogDto { public Guid Id { get; set; } public string EntityType { get; set; } = string.Empty; public Guid EntityId { get; set; } public string Action { get; set; } = string.Empty; public Guid? UserId { get; set; } public string? UserName { get; set; } public DateTime Timestamp { get; set; } public Dictionary? ChangedFields { get; set; } } public class FieldChangeDto { public object? OldValue { get; set; } public object? NewValue { get; set; } } ``` 2. **Query Handler**: `colaflow-api/src/ColaFlow.Application/AuditLog/Queries/GetEntityAuditHistory/GetEntityAuditHistoryQueryHandler.cs` ```csharp public class GetEntityAuditHistoryQueryHandler : IRequestHandler> { private readonly IAuditLogRepository _repository; private readonly IMapper _mapper; public GetEntityAuditHistoryQueryHandler(IAuditLogRepository repository, IMapper mapper) { _repository = repository; _mapper = mapper; } public async Task> Handle(GetEntityAuditHistoryQuery request, CancellationToken cancellationToken) { var auditLogs = await _repository.GetByEntityAsync(request.EntityType, request.EntityId); // Apply filters var filtered = auditLogs.AsQueryable(); if (request.FromDate.HasValue) filtered = filtered.Where(a => a.Timestamp >= request.FromDate.Value); if (request.ToDate.HasValue) filtered = filtered.Where(a => a.Timestamp <= request.ToDate.Value); if (request.UserId.HasValue) filtered = filtered.Where(a => a.UserId == request.UserId.Value); if (request.Limit.HasValue) filtered = filtered.Take(request.Limit.Value); var result = filtered.ToList(); return _mapper.Map>(result); } } ``` 3. **Controller**: `colaflow-api/src/ColaFlow.API/Controllers/AuditLogsController.cs` ```csharp [ApiController] [Route("api/[controller]")] [Authorize] public class AuditLogsController : ControllerBase { private readonly IMediator _mediator; public AuditLogsController(IMediator mediator) { _mediator = mediator; } /// /// Get audit history for a specific entity /// /// Entity type (e.g., "Project", "Epic", "Story") /// Entity ID /// Optional start date filter /// Optional end date filter /// Optional user filter /// Optional limit (default 50) [HttpGet("entity/{entityType}/{entityId}")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetEntityAuditHistory( [FromRoute] string entityType, [FromRoute] Guid entityId, [FromQuery] DateTime? fromDate = null, [FromQuery] DateTime? toDate = null, [FromQuery] Guid? userId = null, [FromQuery] int? limit = 50) { var query = new GetEntityAuditHistoryQuery { EntityType = entityType, EntityId = entityId, FromDate = fromDate, ToDate = toDate, UserId = userId, Limit = limit }; var result = await _mediator.Send(query); return Ok(result); } /// /// Get a specific audit log by ID /// [HttpGet("{id}")] [ProducesResponseType(typeof(AuditLogDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetAuditLogById([FromRoute] Guid id) { var query = new GetAuditLogByIdQuery { AuditLogId = id }; var result = await _mediator.Send(query); if (result == null) return NotFound(); return Ok(result); } } ``` 4. **AutoMapper Profile**: `colaflow-api/src/ColaFlow.Application/AuditLog/MappingProfiles/AuditLogProfile.cs` ```csharp public class AuditLogProfile : Profile { public AuditLogProfile() { CreateMap() .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User != null ? src.User.UserName : null)) .ForMember(dest => dest.ChangedFields, opt => opt.MapFrom(src => DeserializeChangedFields(src.NewValues))); } private Dictionary? DeserializeChangedFields(string? json) { if (string.IsNullOrEmpty(json)) return null; try { return JsonSerializer.Deserialize>(json); } catch { return null; } } } ``` **Example API Requests**: ```bash # Get audit history for a project GET /api/auditlogs/entity/Project/abc-123?limit=20 # Get audit logs for last 7 days GET /api/auditlogs/entity/Epic/def-456?fromDate=2025-10-29 # Get audit logs by specific user GET /api/auditlogs/entity/Story/ghi-789?userId=user-123 # Get specific audit log GET /api/auditlogs/abc-123 ``` ## Technical Notes - Use `AsNoTracking()` for read-only queries (performance) - Implement pagination for large result sets - Cache frequently accessed audit logs (Redis - future) - Add rate limiting to prevent abuse ## Testing **Integration Tests**: ```csharp [Fact] public async Task GetEntityAuditHistory_ShouldReturnAuditLogs() { // Arrange var projectId = await CreateTestProject(); // Act var response = await Client.GetAsync($"/api/auditlogs/entity/Project/{projectId}"); // Assert response.EnsureSuccessStatusCode(); var logs = await response.Content.ReadFromJsonAsync>(); Assert.NotEmpty(logs); } [Fact] public async Task GetEntityAuditHistory_ShouldFilterByDateRange() { // Test date range filtering // ... } ``` --- **Created**: 2025-11-05 by Backend Agent