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:
162
docs/plans/sprint_2_story_2_task_2.md
Normal file
162
docs/plans/sprint_2_story_2_task_2.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
task_id: sprint_2_story_2_task_2
|
||||
story: sprint_2_story_2
|
||||
status: not_started
|
||||
estimated_hours: 3
|
||||
created_date: 2025-11-05
|
||||
assignee: Backend Team
|
||||
---
|
||||
|
||||
# Task 2: Integrate User Context Tracking
|
||||
|
||||
**Story**: Story 2 - Audit Log Core Features (Phase 2)
|
||||
**Estimated**: 3 hours
|
||||
|
||||
## Description
|
||||
|
||||
Enhance audit logging to automatically capture the current user (UserId) from HTTP context for every operation. This provides accountability and traceability.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] UserId automatically captured from JWT token
|
||||
- [ ] System operations (null user) handled correctly
|
||||
- [ ] User information enriched in audit logs
|
||||
- [ ] Integration tests verify user tracking
|
||||
- [ ] Performance not impacted
|
||||
|
||||
## Implementation Details
|
||||
|
||||
**Already Implemented in Story 1!**
|
||||
|
||||
The `AuditLogInterceptor` already captures UserId from HTTP context:
|
||||
|
||||
```csharp
|
||||
private Guid? GetCurrentUserId()
|
||||
{
|
||||
var userIdClaim = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier);
|
||||
return userIdClaim != null ? Guid.Parse(userIdClaim.Value) : null;
|
||||
}
|
||||
```
|
||||
|
||||
**This Task: Add User Information Enrichment**
|
||||
|
||||
1. **Add User Navigation Property**: `colaflow-api/src/ColaFlow.Domain/Entities/AuditLog.cs`
|
||||
```csharp
|
||||
public class AuditLog
|
||||
{
|
||||
// ... existing properties ...
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
// Navigation property
|
||||
public User? User { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update EF Configuration**: `colaflow-api/src/ColaFlow.Infrastructure/Data/Configurations/AuditLogConfiguration.cs`
|
||||
```csharp
|
||||
public void Configure(EntityTypeBuilder<AuditLog> builder)
|
||||
{
|
||||
// ... existing configuration ...
|
||||
|
||||
// User relationship (optional foreign key)
|
||||
builder.HasOne(a => a.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull); // Don't delete audit logs when user is deleted
|
||||
}
|
||||
```
|
||||
|
||||
3. **Enrich Query Results**: `colaflow-api/src/ColaFlow.Infrastructure/Repositories/AuditLogRepository.cs`
|
||||
```csharp
|
||||
public async Task<List<AuditLog>> GetByEntityAsync(string entityType, Guid entityId)
|
||||
{
|
||||
var tenantId = _tenantContext.TenantId;
|
||||
return await _context.AuditLogs
|
||||
.Include(a => a.User) // Include user info
|
||||
.Where(a => a.TenantId == tenantId && a.EntityType == entityType && a.EntityId == entityId)
|
||||
.OrderByDescending(a => a.Timestamp)
|
||||
.ToListAsync();
|
||||
}
|
||||
```
|
||||
|
||||
4. **Handle System Operations**: Update `AuditLogInterceptor` to handle null users gracefully:
|
||||
```csharp
|
||||
private Guid? GetCurrentUserId()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userIdClaim = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to get current user ID for audit log");
|
||||
}
|
||||
|
||||
return null; // System operation or anonymous
|
||||
}
|
||||
```
|
||||
|
||||
**Example Audit Log with User Info**:
|
||||
```json
|
||||
{
|
||||
"Id": "abc-123",
|
||||
"EntityType": "Project",
|
||||
"Action": "Update",
|
||||
"UserId": "user-456",
|
||||
"User": {
|
||||
"Id": "user-456",
|
||||
"UserName": "john.doe@example.com",
|
||||
"DisplayName": "John Doe"
|
||||
},
|
||||
"Timestamp": "2025-11-05T10:30:00Z",
|
||||
"ChangedFields": {
|
||||
"Title": { "OldValue": "Old", "NewValue": "New" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Use `OnDelete(DeleteBehavior.SetNull)` to preserve audit logs when users are deleted
|
||||
- Handle null users gracefully (system operations, background jobs)
|
||||
- Use `Include(a => a.User)` for enriched query results
|
||||
- Consider caching user info for performance (future optimization)
|
||||
|
||||
## Testing
|
||||
|
||||
**Integration Tests**:
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task CreateProject_ShouldCaptureCurrentUser()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
SetCurrentUser(userId); // Helper to set HTTP context user
|
||||
|
||||
// Act
|
||||
var projectId = await Mediator.Send(new CreateProjectCommand { /* ... */ });
|
||||
|
||||
// Assert
|
||||
var auditLog = await Context.AuditLogs
|
||||
.Include(a => a.User)
|
||||
.FirstAsync(a => a.EntityId == projectId);
|
||||
|
||||
Assert.Equal(userId, auditLog.UserId);
|
||||
Assert.NotNull(auditLog.User);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SystemOperation_ShouldHaveNullUser()
|
||||
{
|
||||
// Test background job or system operation
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Created**: 2025-11-05 by Backend Agent
|
||||
Reference in New Issue
Block a user