feat(backend): Create Sprint 2 backend Stories and Tasks
Created detailed implementation plans for Sprint 2 backend work: Story 1: Audit Log Foundation (Phase 1) - Task 1: Design AuditLog database schema and create migration - Task 2: Create AuditLog entity and Repository - Task 3: Implement EF Core SaveChangesInterceptor - Task 4: Write unit tests for audit logging - Task 5: Integrate with ProjectManagement Module Story 2: Audit Log Core Features (Phase 2) - Task 1: Implement Changed Fields Detection (JSON Diff) - Task 2: Integrate User Context Tracking - Task 3: Add Multi-Tenant Isolation - Task 4: Implement Audit Query API - Task 5: Write Integration Tests Story 3: Sprint Management Module - Task 1: Create Sprint Aggregate Root and Domain Events - Task 2: Implement Sprint Repository and EF Core Configuration - Task 3: Create CQRS Commands and Queries - Task 4: Implement Burndown Chart Calculation - Task 5: Add SignalR Real-Time Notifications - Task 6: Write Integration Tests Total: 3 Stories, 16 Tasks, 24 Story Points (8+8+8) Estimated Duration: 10-12 days All tasks include: - Detailed technical implementation guidance - Code examples and file paths - Testing requirements (>= 90% coverage) - Performance benchmarks (< 5ms audit overhead) - Multi-tenant security validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
246
docs/plans/sprint_2_story_2_task_4.md
Normal file
246
docs/plans/sprint_2_story_2_task_4.md
Normal file
@@ -0,0 +1,246 @@
|
||||
---
|
||||
task_id: sprint_2_story_2_task_4
|
||||
story: sprint_2_story_2
|
||||
status: not_started
|
||||
estimated_hours: 5
|
||||
created_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
|
||||
|
||||
- [ ] GetEntityAuditHistoryQuery implemented
|
||||
- [ ] GetAuditLogByIdQuery implemented
|
||||
- [ ] AuditLogsController with 2 endpoints created
|
||||
- [ ] Query handlers with proper filtering
|
||||
- [ ] Swagger documentation added
|
||||
- [ ] Integration tests for API endpoints
|
||||
|
||||
## Implementation Details
|
||||
|
||||
**Files to Create**:
|
||||
|
||||
1. **Query DTOs**: `colaflow-api/src/ColaFlow.Application/AuditLog/Queries/GetEntityAuditHistory/GetEntityAuditHistoryQuery.cs`
|
||||
```csharp
|
||||
public record GetEntityAuditHistoryQuery : IRequest<List<AuditLogDto>>
|
||||
{
|
||||
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<string, FieldChangeDto>? 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<GetEntityAuditHistoryQuery, List<AuditLogDto>>
|
||||
{
|
||||
private readonly IAuditLogRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public GetEntityAuditHistoryQueryHandler(IAuditLogRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<List<AuditLogDto>> 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<List<AuditLogDto>>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get audit history for a specific entity
|
||||
/// </summary>
|
||||
/// <param name="entityType">Entity type (e.g., "Project", "Epic", "Story")</param>
|
||||
/// <param name="entityId">Entity ID</param>
|
||||
/// <param name="fromDate">Optional start date filter</param>
|
||||
/// <param name="toDate">Optional end date filter</param>
|
||||
/// <param name="userId">Optional user filter</param>
|
||||
/// <param name="limit">Optional limit (default 50)</param>
|
||||
[HttpGet("entity/{entityType}/{entityId}")]
|
||||
[ProducesResponseType(typeof(List<AuditLogDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific audit log by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(AuditLogDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> 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<AuditLog, AuditLogDto>()
|
||||
.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<string, FieldChangeDto>? DeserializeChangedFields(string? json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json)) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<Dictionary<string, FieldChangeDto>>(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<List<AuditLogDto>>();
|
||||
Assert.NotEmpty(logs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEntityAuditHistory_ShouldFilterByDateRange()
|
||||
{
|
||||
// Test date range filtering
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Created**: 2025-11-05 by Backend Agent
|
||||
Reference in New Issue
Block a user