diff --git a/colaflow-api/src/ColaFlow.API/Controllers/McpApiKeysController.cs b/colaflow-api/src/ColaFlow.API/Controllers/McpApiKeysController.cs index ac94eff..2b0809e 100644 --- a/colaflow-api/src/ColaFlow.API/Controllers/McpApiKeysController.cs +++ b/colaflow-api/src/ColaFlow.API/Controllers/McpApiKeysController.cs @@ -13,18 +13,13 @@ namespace ColaFlow.API.Controllers; [ApiController] [Route("api/mcp/keys")] [Authorize] // Requires JWT authentication -public class McpApiKeysController : ControllerBase +public class McpApiKeysController( + IMcpApiKeyService apiKeyService, + ILogger logger) + : ControllerBase { - private readonly IMcpApiKeyService _apiKeyService; - private readonly ILogger _logger; - - public McpApiKeysController( - IMcpApiKeyService apiKeyService, - ILogger logger) - { - _apiKeyService = apiKeyService ?? throw new ArgumentNullException(nameof(apiKeyService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IMcpApiKeyService _apiKeyService = apiKeyService ?? throw new ArgumentNullException(nameof(apiKeyService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); /// /// Create a new API Key diff --git a/colaflow-api/src/ColaFlow.API/Controllers/McpPendingChangesController.cs b/colaflow-api/src/ColaFlow.API/Controllers/McpPendingChangesController.cs index 7d1e93d..e18ac11 100644 --- a/colaflow-api/src/ColaFlow.API/Controllers/McpPendingChangesController.cs +++ b/colaflow-api/src/ColaFlow.API/Controllers/McpPendingChangesController.cs @@ -14,18 +14,13 @@ namespace ColaFlow.API.Controllers; [ApiController] [Route("api/mcp/pending-changes")] [Authorize] // Requires JWT authentication -public class McpPendingChangesController : ControllerBase +public class McpPendingChangesController( + IPendingChangeService pendingChangeService, + ILogger logger) + : ControllerBase { - private readonly IPendingChangeService _pendingChangeService; - private readonly ILogger _logger; - - public McpPendingChangesController( - IPendingChangeService pendingChangeService, - ILogger logger) - { - _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IPendingChangeService _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); /// /// Get list of pending changes with filtering and pagination diff --git a/colaflow-api/src/ColaFlow.API/Controllers/SignalRTestController.cs b/colaflow-api/src/ColaFlow.API/Controllers/SignalRTestController.cs index 0733cb1..66e1464 100644 --- a/colaflow-api/src/ColaFlow.API/Controllers/SignalRTestController.cs +++ b/colaflow-api/src/ColaFlow.API/Controllers/SignalRTestController.cs @@ -7,15 +7,8 @@ namespace ColaFlow.API.Controllers; [ApiController] [Route("api/[controller]")] [Authorize] -public class SignalRTestController : ControllerBase +public class SignalRTestController(IRealtimeNotificationService notificationService) : ControllerBase { - private readonly IRealtimeNotificationService _notificationService; - - public SignalRTestController(IRealtimeNotificationService notificationService) - { - _notificationService = notificationService; - } - /// /// Test sending notification to current user /// @@ -24,7 +17,7 @@ public class SignalRTestController : ControllerBase { var userId = Guid.Parse(User.FindFirst("sub")!.Value); - await _notificationService.NotifyUser(userId, message, "test"); + await notificationService.NotifyUser(userId, message, "test"); return Ok(new { message = "Notification sent", userId }); } @@ -37,7 +30,7 @@ public class SignalRTestController : ControllerBase { var tenantId = Guid.Parse(User.FindFirst("tenant_id")!.Value); - await _notificationService.NotifyUsersInTenant(tenantId, message, "test"); + await notificationService.NotifyUsersInTenant(tenantId, message, "test"); return Ok(new { message = "Tenant notification sent", tenantId }); } @@ -50,7 +43,7 @@ public class SignalRTestController : ControllerBase { var tenantId = Guid.Parse(User.FindFirst("tenant_id")!.Value); - await _notificationService.NotifyProjectUpdate(tenantId, request.ProjectId, new + await notificationService.NotifyProjectUpdate(tenantId, request.ProjectId, new { Message = request.Message, UpdatedBy = User.FindFirst("sub")!.Value, @@ -68,7 +61,7 @@ public class SignalRTestController : ControllerBase { var tenantId = Guid.Parse(User.FindFirst("tenant_id")!.Value); - await _notificationService.NotifyIssueStatusChanged( + await notificationService.NotifyIssueStatusChanged( tenantId, request.ProjectId, request.IssueId, diff --git a/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs b/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs index 0dd6c8c..0e3d01d 100644 --- a/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs +++ b/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs @@ -22,22 +22,15 @@ namespace ColaFlow.API.Controllers; [ApiController] [Route("api/v1/sprints")] [Authorize] -public class SprintsController : ControllerBase +public class SprintsController(IMediator mediator) : ControllerBase { - private readonly IMediator _mediator; - - public SprintsController(IMediator mediator) - { - _mediator = mediator; - } - /// /// Create a new sprint /// [HttpPost] public async Task> Create([FromBody] CreateSprintCommand command) { - var result = await _mediator.Send(command); + var result = await mediator.Send(command); return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); } @@ -50,7 +43,7 @@ public class SprintsController : ControllerBase if (id != command.SprintId) return BadRequest("Sprint ID mismatch"); - await _mediator.Send(command); + await mediator.Send(command); return NoContent(); } @@ -60,7 +53,7 @@ public class SprintsController : ControllerBase [HttpDelete("{id}")] public async Task Delete(Guid id) { - await _mediator.Send(new DeleteSprintCommand(id)); + await mediator.Send(new DeleteSprintCommand(id)); return NoContent(); } @@ -70,7 +63,7 @@ public class SprintsController : ControllerBase [HttpGet("{id}")] public async Task> GetById(Guid id) { - var result = await _mediator.Send(new GetSprintByIdQuery(id)); + var result = await mediator.Send(new GetSprintByIdQuery(id)); if (result == null) return NotFound(); return Ok(result); @@ -82,7 +75,7 @@ public class SprintsController : ControllerBase [HttpGet] public async Task>> GetByProject([FromQuery] Guid projectId) { - var result = await _mediator.Send(new GetSprintsByProjectIdQuery(projectId)); + var result = await mediator.Send(new GetSprintsByProjectIdQuery(projectId)); return Ok(result); } @@ -92,7 +85,7 @@ public class SprintsController : ControllerBase [HttpGet("active")] public async Task>> GetActive() { - var result = await _mediator.Send(new GetActiveSprintsQuery()); + var result = await mediator.Send(new GetActiveSprintsQuery()); return Ok(result); } @@ -102,7 +95,7 @@ public class SprintsController : ControllerBase [HttpPost("{id}/start")] public async Task Start(Guid id) { - await _mediator.Send(new StartSprintCommand(id)); + await mediator.Send(new StartSprintCommand(id)); return NoContent(); } @@ -112,7 +105,7 @@ public class SprintsController : ControllerBase [HttpPost("{id}/complete")] public async Task Complete(Guid id) { - await _mediator.Send(new CompleteSprintCommand(id)); + await mediator.Send(new CompleteSprintCommand(id)); return NoContent(); } @@ -122,7 +115,7 @@ public class SprintsController : ControllerBase [HttpPost("{id}/tasks/{taskId}")] public async Task AddTask(Guid id, Guid taskId) { - await _mediator.Send(new AddTaskToSprintCommand(id, taskId)); + await mediator.Send(new AddTaskToSprintCommand(id, taskId)); return NoContent(); } @@ -132,7 +125,7 @@ public class SprintsController : ControllerBase [HttpDelete("{id}/tasks/{taskId}")] public async Task RemoveTask(Guid id, Guid taskId) { - await _mediator.Send(new RemoveTaskFromSprintCommand(id, taskId)); + await mediator.Send(new RemoveTaskFromSprintCommand(id, taskId)); return NoContent(); } @@ -142,7 +135,7 @@ public class SprintsController : ControllerBase [HttpGet("{id}/burndown")] public async Task> GetBurndown(Guid id) { - var result = await _mediator.Send(new GetSprintBurndownQuery(id)); + var result = await mediator.Send(new GetSprintBurndownQuery(id)); if (result == null) return NotFound(); return Ok(result); diff --git a/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs b/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs index 5654249..2d8e985 100644 --- a/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs +++ b/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs @@ -8,26 +8,20 @@ namespace ColaFlow.API.EventHandlers; /// /// Handles Sprint domain events and sends SignalR notifications /// -public class SprintEventHandlers : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler +public class SprintEventHandlers( + IRealtimeNotificationService notificationService, + ILogger logger, + IHttpContextAccessor httpContextAccessor) + : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { - private readonly IRealtimeNotificationService _notificationService; - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - - public SprintEventHandlers( - IRealtimeNotificationService notificationService, - ILogger logger, - IHttpContextAccessor httpContextAccessor) - { - _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - } + private readonly IRealtimeNotificationService _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); public async Task Handle(SprintCreatedEvent notification, CancellationToken cancellationToken) { diff --git a/colaflow-api/src/ColaFlow.API/Hubs/ProjectHub.cs b/colaflow-api/src/ColaFlow.API/Hubs/ProjectHub.cs index ff5fa1b..e2ce7b4 100644 --- a/colaflow-api/src/ColaFlow.API/Hubs/ProjectHub.cs +++ b/colaflow-api/src/ColaFlow.API/Hubs/ProjectHub.cs @@ -6,15 +6,8 @@ namespace ColaFlow.API.Hubs; /// /// Project real-time collaboration Hub /// -public class ProjectHub : BaseHub +public class ProjectHub(IProjectPermissionService permissionService) : BaseHub { - private readonly IProjectPermissionService _permissionService; - - public ProjectHub(IProjectPermissionService permissionService) - { - _permissionService = permissionService; - } - /// /// Join project room (to receive project-level updates) /// @@ -24,7 +17,7 @@ public class ProjectHub : BaseHub var userId = GetCurrentUserId(); // Validate user has permission to access this project - var hasPermission = await _permissionService.IsUserProjectMemberAsync( + var hasPermission = await permissionService.IsUserProjectMemberAsync( userId, projectId, Context.ConnectionAborted); if (!hasPermission) @@ -54,7 +47,7 @@ public class ProjectHub : BaseHub var userId = GetCurrentUserId(); // Validate user has permission to access this project (for consistency) - var hasPermission = await _permissionService.IsUserProjectMemberAsync( + var hasPermission = await permissionService.IsUserProjectMemberAsync( userId, projectId, Context.ConnectionAborted); if (!hasPermission) diff --git a/colaflow-api/src/ColaFlow.API/Middleware/PerformanceLoggingMiddleware.cs b/colaflow-api/src/ColaFlow.API/Middleware/PerformanceLoggingMiddleware.cs index 5cf732c..5c19fe9 100644 --- a/colaflow-api/src/ColaFlow.API/Middleware/PerformanceLoggingMiddleware.cs +++ b/colaflow-api/src/ColaFlow.API/Middleware/PerformanceLoggingMiddleware.cs @@ -5,21 +5,12 @@ namespace ColaFlow.API.Middleware; /// /// Middleware to log slow HTTP requests for performance monitoring /// -public class PerformanceLoggingMiddleware +public class PerformanceLoggingMiddleware( + RequestDelegate next, + ILogger logger, + IConfiguration configuration) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - private readonly int _slowRequestThresholdMs; - - public PerformanceLoggingMiddleware( - RequestDelegate next, - ILogger logger, - IConfiguration configuration) - { - _next = next; - _logger = logger; - _slowRequestThresholdMs = configuration.GetValue("Performance:SlowRequestThresholdMs", 1000); - } + private readonly int _slowRequestThresholdMs = configuration.GetValue("Performance:SlowRequestThresholdMs", 1000); public async Task InvokeAsync(HttpContext context) { @@ -29,7 +20,7 @@ public class PerformanceLoggingMiddleware try { - await _next(context); + await next(context); } finally { @@ -39,7 +30,7 @@ public class PerformanceLoggingMiddleware // Log slow requests as warnings if (elapsedMs > _slowRequestThresholdMs) { - _logger.LogWarning( + logger.LogWarning( "Slow request detected: {Method} {Path} took {ElapsedMs}ms (Status: {StatusCode})", requestMethod, requestPath, @@ -49,7 +40,7 @@ public class PerformanceLoggingMiddleware else if (elapsedMs > _slowRequestThresholdMs / 2) { // Log moderately slow requests as information - _logger.LogInformation( + logger.LogInformation( "Request took {ElapsedMs}ms: {Method} {Path} (Status: {StatusCode})", elapsedMs, requestMethod, diff --git a/colaflow-api/src/ColaFlow.API/Services/ProjectNotificationServiceAdapter.cs b/colaflow-api/src/ColaFlow.API/Services/ProjectNotificationServiceAdapter.cs index fa8b92b..6056db7 100644 --- a/colaflow-api/src/ColaFlow.API/Services/ProjectNotificationServiceAdapter.cs +++ b/colaflow-api/src/ColaFlow.API/Services/ProjectNotificationServiceAdapter.cs @@ -6,81 +6,75 @@ namespace ColaFlow.API.Services; /// Adapter that implements IProjectNotificationService by delegating to IRealtimeNotificationService /// This allows the ProjectManagement module to send notifications without depending on the API layer /// -public class ProjectNotificationServiceAdapter : IProjectNotificationService +public class ProjectNotificationServiceAdapter(IRealtimeNotificationService realtimeService) + : IProjectNotificationService { - private readonly IRealtimeNotificationService _realtimeService; - - public ProjectNotificationServiceAdapter(IRealtimeNotificationService realtimeService) - { - _realtimeService = realtimeService; - } - // Project notifications public Task NotifyProjectCreated(Guid tenantId, Guid projectId, object project) { - return _realtimeService.NotifyProjectCreated(tenantId, projectId, project); + return realtimeService.NotifyProjectCreated(tenantId, projectId, project); } public Task NotifyProjectUpdated(Guid tenantId, Guid projectId, object project) { - return _realtimeService.NotifyProjectUpdated(tenantId, projectId, project); + return realtimeService.NotifyProjectUpdated(tenantId, projectId, project); } public Task NotifyProjectArchived(Guid tenantId, Guid projectId) { - return _realtimeService.NotifyProjectArchived(tenantId, projectId); + return realtimeService.NotifyProjectArchived(tenantId, projectId); } // Epic notifications public Task NotifyEpicCreated(Guid tenantId, Guid projectId, Guid epicId, object epic) { - return _realtimeService.NotifyEpicCreated(tenantId, projectId, epicId, epic); + return realtimeService.NotifyEpicCreated(tenantId, projectId, epicId, epic); } public Task NotifyEpicUpdated(Guid tenantId, Guid projectId, Guid epicId, object epic) { - return _realtimeService.NotifyEpicUpdated(tenantId, projectId, epicId, epic); + return realtimeService.NotifyEpicUpdated(tenantId, projectId, epicId, epic); } public Task NotifyEpicDeleted(Guid tenantId, Guid projectId, Guid epicId) { - return _realtimeService.NotifyEpicDeleted(tenantId, projectId, epicId); + return realtimeService.NotifyEpicDeleted(tenantId, projectId, epicId); } // Story notifications public Task NotifyStoryCreated(Guid tenantId, Guid projectId, Guid epicId, Guid storyId, object story) { - return _realtimeService.NotifyStoryCreated(tenantId, projectId, epicId, storyId, story); + return realtimeService.NotifyStoryCreated(tenantId, projectId, epicId, storyId, story); } public Task NotifyStoryUpdated(Guid tenantId, Guid projectId, Guid epicId, Guid storyId, object story) { - return _realtimeService.NotifyStoryUpdated(tenantId, projectId, epicId, storyId, story); + return realtimeService.NotifyStoryUpdated(tenantId, projectId, epicId, storyId, story); } public Task NotifyStoryDeleted(Guid tenantId, Guid projectId, Guid epicId, Guid storyId) { - return _realtimeService.NotifyStoryDeleted(tenantId, projectId, epicId, storyId); + return realtimeService.NotifyStoryDeleted(tenantId, projectId, epicId, storyId); } // Task notifications public Task NotifyTaskCreated(Guid tenantId, Guid projectId, Guid storyId, Guid taskId, object task) { - return _realtimeService.NotifyTaskCreated(tenantId, projectId, storyId, taskId, task); + return realtimeService.NotifyTaskCreated(tenantId, projectId, storyId, taskId, task); } public Task NotifyTaskUpdated(Guid tenantId, Guid projectId, Guid storyId, Guid taskId, object task) { - return _realtimeService.NotifyTaskUpdated(tenantId, projectId, storyId, taskId, task); + return realtimeService.NotifyTaskUpdated(tenantId, projectId, storyId, taskId, task); } public Task NotifyTaskDeleted(Guid tenantId, Guid projectId, Guid storyId, Guid taskId) { - return _realtimeService.NotifyTaskDeleted(tenantId, projectId, storyId, taskId); + return realtimeService.NotifyTaskDeleted(tenantId, projectId, storyId, taskId); } public Task NotifyTaskAssigned(Guid tenantId, Guid projectId, Guid taskId, Guid assigneeId) { - return _realtimeService.NotifyTaskAssigned(tenantId, projectId, taskId, assigneeId); + return realtimeService.NotifyTaskAssigned(tenantId, projectId, taskId, assigneeId); } } diff --git a/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs b/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs index 0b652bf..b411a30 100644 --- a/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs +++ b/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs @@ -3,29 +3,19 @@ using ColaFlow.API.Hubs; namespace ColaFlow.API.Services; -public class RealtimeNotificationService : IRealtimeNotificationService +public class RealtimeNotificationService( + IHubContext projectHubContext, + IHubContext notificationHubContext, + ILogger logger) + : IRealtimeNotificationService { - private readonly IHubContext _projectHubContext; - private readonly IHubContext _notificationHubContext; - private readonly ILogger _logger; - - public RealtimeNotificationService( - IHubContext projectHubContext, - IHubContext notificationHubContext, - ILogger logger) - { - _projectHubContext = projectHubContext; - _notificationHubContext = notificationHubContext; - _logger = logger; - } - public async Task NotifyProjectCreated(Guid tenantId, Guid projectId, object project) { var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying tenant {TenantId} of new project {ProjectId}", tenantId, projectId); + logger.LogInformation("Notifying tenant {TenantId} of new project {ProjectId}", tenantId, projectId); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectCreated", project); + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectCreated", project); } public async Task NotifyProjectUpdated(Guid tenantId, Guid projectId, object project) @@ -33,10 +23,10 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying project {ProjectId} updated", projectId); + logger.LogInformation("Notifying project {ProjectId} updated", projectId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("ProjectUpdated", project); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectUpdated", project); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("ProjectUpdated", project); + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectUpdated", project); } public async Task NotifyProjectArchived(Guid tenantId, Guid projectId) @@ -44,19 +34,19 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying project {ProjectId} archived", projectId); + logger.LogInformation("Notifying project {ProjectId} archived", projectId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("ProjectArchived", new { ProjectId = projectId }); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectArchived", new { ProjectId = projectId }); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("ProjectArchived", new { ProjectId = projectId }); + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectArchived", new { ProjectId = projectId }); } public async Task NotifyProjectUpdate(Guid tenantId, Guid projectId, object data) { var groupName = $"project-{projectId}"; - _logger.LogInformation("Sending project update to group {GroupName}", groupName); + logger.LogInformation("Sending project update to group {GroupName}", groupName); - await _projectHubContext.Clients.Group(groupName).SendAsync("ProjectUpdated", data); + await projectHubContext.Clients.Group(groupName).SendAsync("ProjectUpdated", data); } // Epic notifications @@ -65,28 +55,28 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying epic {EpicId} created in project {ProjectId}", epicId, projectId); + logger.LogInformation("Notifying epic {EpicId} created in project {ProjectId}", epicId, projectId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicCreated", epic); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("EpicCreated", epic); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicCreated", epic); + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("EpicCreated", epic); } public async Task NotifyEpicUpdated(Guid tenantId, Guid projectId, Guid epicId, object epic) { var projectGroupName = $"project-{projectId}"; - _logger.LogInformation("Notifying epic {EpicId} updated", epicId); + logger.LogInformation("Notifying epic {EpicId} updated", epicId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicUpdated", epic); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicUpdated", epic); } public async Task NotifyEpicDeleted(Guid tenantId, Guid projectId, Guid epicId) { var projectGroupName = $"project-{projectId}"; - _logger.LogInformation("Notifying epic {EpicId} deleted", epicId); + logger.LogInformation("Notifying epic {EpicId} deleted", epicId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicDeleted", new { EpicId = epicId }); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicDeleted", new { EpicId = epicId }); } // Story notifications @@ -95,28 +85,28 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying story {StoryId} created in epic {EpicId}", storyId, epicId); + logger.LogInformation("Notifying story {StoryId} created in epic {EpicId}", storyId, epicId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryCreated", story); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("StoryCreated", story); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryCreated", story); + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("StoryCreated", story); } public async Task NotifyStoryUpdated(Guid tenantId, Guid projectId, Guid epicId, Guid storyId, object story) { var projectGroupName = $"project-{projectId}"; - _logger.LogInformation("Notifying story {StoryId} updated", storyId); + logger.LogInformation("Notifying story {StoryId} updated", storyId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryUpdated", story); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryUpdated", story); } public async Task NotifyStoryDeleted(Guid tenantId, Guid projectId, Guid epicId, Guid storyId) { var projectGroupName = $"project-{projectId}"; - _logger.LogInformation("Notifying story {StoryId} deleted", storyId); + logger.LogInformation("Notifying story {StoryId} deleted", storyId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryDeleted", new { StoryId = storyId }); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryDeleted", new { StoryId = storyId }); } // Task notifications @@ -125,37 +115,37 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying task {TaskId} created in story {StoryId}", taskId, storyId); + logger.LogInformation("Notifying task {TaskId} created in story {StoryId}", taskId, storyId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskCreated", task); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("TaskCreated", task); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskCreated", task); + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("TaskCreated", task); } public async Task NotifyTaskUpdated(Guid tenantId, Guid projectId, Guid storyId, Guid taskId, object task) { var projectGroupName = $"project-{projectId}"; - _logger.LogInformation("Notifying task {TaskId} updated", taskId); + logger.LogInformation("Notifying task {TaskId} updated", taskId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskUpdated", task); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskUpdated", task); } public async Task NotifyTaskDeleted(Guid tenantId, Guid projectId, Guid storyId, Guid taskId) { var projectGroupName = $"project-{projectId}"; - _logger.LogInformation("Notifying task {TaskId} deleted", taskId); + logger.LogInformation("Notifying task {TaskId} deleted", taskId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskDeleted", new { TaskId = taskId }); + await projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskDeleted", new { TaskId = taskId }); } public async Task NotifyTaskAssigned(Guid tenantId, Guid projectId, Guid taskId, Guid assigneeId) { var projectGroupName = $"project-{projectId}"; - _logger.LogInformation("Notifying task {TaskId} assigned to {AssigneeId}", taskId, assigneeId); + logger.LogInformation("Notifying task {TaskId} assigned to {AssigneeId}", taskId, assigneeId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskAssigned", new + await projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskAssigned", new { TaskId = taskId, AssigneeId = assigneeId, @@ -167,21 +157,21 @@ public class RealtimeNotificationService : IRealtimeNotificationService { var groupName = $"project-{projectId}"; - await _projectHubContext.Clients.Group(groupName).SendAsync("IssueCreated", issue); + await projectHubContext.Clients.Group(groupName).SendAsync("IssueCreated", issue); } public async Task NotifyIssueUpdated(Guid tenantId, Guid projectId, object issue) { var groupName = $"project-{projectId}"; - await _projectHubContext.Clients.Group(groupName).SendAsync("IssueUpdated", issue); + await projectHubContext.Clients.Group(groupName).SendAsync("IssueUpdated", issue); } public async Task NotifyIssueDeleted(Guid tenantId, Guid projectId, Guid issueId) { var groupName = $"project-{projectId}"; - await _projectHubContext.Clients.Group(groupName).SendAsync("IssueDeleted", new { IssueId = issueId }); + await projectHubContext.Clients.Group(groupName).SendAsync("IssueDeleted", new { IssueId = issueId }); } public async Task NotifyIssueStatusChanged( @@ -193,7 +183,7 @@ public class RealtimeNotificationService : IRealtimeNotificationService { var groupName = $"project-{projectId}"; - await _projectHubContext.Clients.Group(groupName).SendAsync("IssueStatusChanged", new + await projectHubContext.Clients.Group(groupName).SendAsync("IssueStatusChanged", new { IssueId = issueId, OldStatus = oldStatus, @@ -208,9 +198,9 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying sprint {SprintId} created in project {ProjectId}", sprintId, projectId); + logger.LogInformation("Notifying sprint {SprintId} created in project {ProjectId}", sprintId, projectId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCreated", new + await projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCreated", new { SprintId = sprintId, SprintName = sprintName, @@ -218,7 +208,7 @@ public class RealtimeNotificationService : IRealtimeNotificationService Timestamp = DateTime.UtcNow }); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCreated", new + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCreated", new { SprintId = sprintId, SprintName = sprintName, @@ -231,9 +221,9 @@ public class RealtimeNotificationService : IRealtimeNotificationService { var projectGroupName = $"project-{projectId}"; - _logger.LogInformation("Notifying sprint {SprintId} updated", sprintId); + logger.LogInformation("Notifying sprint {SprintId} updated", sprintId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintUpdated", new + await projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintUpdated", new { SprintId = sprintId, SprintName = sprintName, @@ -247,9 +237,9 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying sprint {SprintId} started", sprintId); + logger.LogInformation("Notifying sprint {SprintId} started", sprintId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintStarted", new + await projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintStarted", new { SprintId = sprintId, SprintName = sprintName, @@ -257,7 +247,7 @@ public class RealtimeNotificationService : IRealtimeNotificationService Timestamp = DateTime.UtcNow }); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintStarted", new + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintStarted", new { SprintId = sprintId, SprintName = sprintName, @@ -271,9 +261,9 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying sprint {SprintId} completed", sprintId); + logger.LogInformation("Notifying sprint {SprintId} completed", sprintId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCompleted", new + await projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCompleted", new { SprintId = sprintId, SprintName = sprintName, @@ -281,7 +271,7 @@ public class RealtimeNotificationService : IRealtimeNotificationService Timestamp = DateTime.UtcNow }); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCompleted", new + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCompleted", new { SprintId = sprintId, SprintName = sprintName, @@ -295,9 +285,9 @@ public class RealtimeNotificationService : IRealtimeNotificationService var projectGroupName = $"project-{projectId}"; var tenantGroupName = $"tenant-{tenantId}"; - _logger.LogInformation("Notifying sprint {SprintId} deleted", sprintId); + logger.LogInformation("Notifying sprint {SprintId} deleted", sprintId); - await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintDeleted", new + await projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintDeleted", new { SprintId = sprintId, SprintName = sprintName, @@ -305,7 +295,7 @@ public class RealtimeNotificationService : IRealtimeNotificationService Timestamp = DateTime.UtcNow }); - await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintDeleted", new + await projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintDeleted", new { SprintId = sprintId, SprintName = sprintName, @@ -318,7 +308,7 @@ public class RealtimeNotificationService : IRealtimeNotificationService { var userConnectionId = $"user-{userId}"; - await _notificationHubContext.Clients.User(userId.ToString()).SendAsync("Notification", new + await notificationHubContext.Clients.User(userId.ToString()).SendAsync("Notification", new { Message = message, Type = type, @@ -330,7 +320,7 @@ public class RealtimeNotificationService : IRealtimeNotificationService { var groupName = $"tenant-{tenantId}"; - await _notificationHubContext.Clients.Group(groupName).SendAsync("Notification", new + await notificationHubContext.Clients.Group(groupName).SendAsync("Notification", new { Message = message, Type = type, diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/AcceptInvitation/AcceptInvitationCommandHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/AcceptInvitation/AcceptInvitationCommandHandler.cs index 5ff7de2..c74acb3 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/AcceptInvitation/AcceptInvitationCommandHandler.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/AcceptInvitation/AcceptInvitationCommandHandler.cs @@ -8,38 +8,22 @@ using Microsoft.Extensions.Logging; namespace ColaFlow.Modules.Identity.Application.Commands.AcceptInvitation; -public class AcceptInvitationCommandHandler : IRequestHandler +public class AcceptInvitationCommandHandler( + IInvitationRepository invitationRepository, + IUserRepository userRepository, + IUserTenantRoleRepository userTenantRoleRepository, + ISecurityTokenService tokenService, + IPasswordHasher passwordHasher, + ILogger logger) + : IRequestHandler { - private readonly IInvitationRepository _invitationRepository; - private readonly IUserRepository _userRepository; - private readonly IUserTenantRoleRepository _userTenantRoleRepository; - private readonly ISecurityTokenService _tokenService; - private readonly IPasswordHasher _passwordHasher; - private readonly ILogger _logger; - - public AcceptInvitationCommandHandler( - IInvitationRepository invitationRepository, - IUserRepository userRepository, - IUserTenantRoleRepository userTenantRoleRepository, - ISecurityTokenService tokenService, - IPasswordHasher passwordHasher, - ILogger logger) - { - _invitationRepository = invitationRepository; - _userRepository = userRepository; - _userTenantRoleRepository = userTenantRoleRepository; - _tokenService = tokenService; - _passwordHasher = passwordHasher; - _logger = logger; - } - public async Task Handle(AcceptInvitationCommand request, CancellationToken cancellationToken) { // Hash the token to find the invitation - var tokenHash = _tokenService.HashToken(request.Token); + var tokenHash = tokenService.HashToken(request.Token); // Find invitation by token hash - var invitation = await _invitationRepository.GetByTokenHashAsync(tokenHash, cancellationToken); + var invitation = await invitationRepository.GetByTokenHashAsync(tokenHash, cancellationToken); if (invitation == null) throw new InvalidOperationException("Invalid invitation token"); @@ -50,14 +34,14 @@ public class AcceptInvitationCommandHandler : IRequestHandler +public class CancelInvitationCommandHandler( + IInvitationRepository invitationRepository, + ILogger logger) + : IRequestHandler { - private readonly IInvitationRepository _invitationRepository; - private readonly ILogger _logger; - - public CancelInvitationCommandHandler( - IInvitationRepository invitationRepository, - ILogger logger) - { - _invitationRepository = invitationRepository; - _logger = logger; - } - public async Task Handle(CancelInvitationCommand request, CancellationToken cancellationToken) { var invitationId = InvitationId.Create(request.InvitationId); var tenantId = TenantId.Create(request.TenantId); // Get the invitation - var invitation = await _invitationRepository.GetByIdAsync(invitationId, cancellationToken); + var invitation = await invitationRepository.GetByIdAsync(invitationId, cancellationToken); if (invitation == null) throw new InvalidOperationException($"Invitation {request.InvitationId} not found"); @@ -35,9 +27,9 @@ public class CancelInvitationCommandHandler : IRequestHandler +public class ForgotPasswordCommandHandler( + IUserRepository userRepository, + ITenantRepository tenantRepository, + IPasswordResetTokenRepository tokenRepository, + ISecurityTokenService tokenService, + IEmailService emailService, + IEmailTemplateService emailTemplateService, + IRateLimitService rateLimitService, + ILogger logger) + : IRequestHandler { - private readonly IUserRepository _userRepository; - private readonly ITenantRepository _tenantRepository; - private readonly IPasswordResetTokenRepository _tokenRepository; - private readonly ISecurityTokenService _tokenService; - private readonly IEmailService _emailService; - private readonly IEmailTemplateService _emailTemplateService; - private readonly IRateLimitService _rateLimitService; - private readonly ILogger _logger; - - public ForgotPasswordCommandHandler( - IUserRepository userRepository, - ITenantRepository tenantRepository, - IPasswordResetTokenRepository tokenRepository, - ISecurityTokenService tokenService, - IEmailService emailService, - IEmailTemplateService emailTemplateService, - IRateLimitService rateLimitService, - ILogger logger) - { - _userRepository = userRepository; - _tenantRepository = tenantRepository; - _tokenRepository = tokenRepository; - _tokenService = tokenService; - _emailService = emailService; - _emailTemplateService = emailTemplateService; - _rateLimitService = rateLimitService; - _logger = logger; - } - public async Task Handle(ForgotPasswordCommand request, CancellationToken cancellationToken) { // Rate limiting: 3 requests per hour per email var rateLimitKey = $"forgot-password:{request.Email.ToLowerInvariant()}"; - var isAllowed = await _rateLimitService.IsAllowedAsync( + var isAllowed = await rateLimitService.IsAllowedAsync( rateLimitKey, 3, TimeSpan.FromHours(1), @@ -52,7 +32,7 @@ public class ForgotPasswordCommandHandler : IRequestHandler +public class InviteUserCommandHandler( + IInvitationRepository invitationRepository, + IUserRepository userRepository, + IUserTenantRoleRepository userTenantRoleRepository, + ITenantRepository tenantRepository, + ISecurityTokenService tokenService, + IEmailService emailService, + IEmailTemplateService templateService, + ILogger logger) + : IRequestHandler { - private readonly IInvitationRepository _invitationRepository; - private readonly IUserRepository _userRepository; - private readonly IUserTenantRoleRepository _userTenantRoleRepository; - private readonly ITenantRepository _tenantRepository; - private readonly ISecurityTokenService _tokenService; - private readonly IEmailService _emailService; - private readonly IEmailTemplateService _templateService; - private readonly ILogger _logger; - - public InviteUserCommandHandler( - IInvitationRepository invitationRepository, - IUserRepository userRepository, - IUserTenantRoleRepository userTenantRoleRepository, - ITenantRepository tenantRepository, - ISecurityTokenService tokenService, - IEmailService emailService, - IEmailTemplateService templateService, - ILogger logger) - { - _invitationRepository = invitationRepository; - _userRepository = userRepository; - _userTenantRoleRepository = userTenantRoleRepository; - _tenantRepository = tenantRepository; - _tokenService = tokenService; - _emailService = emailService; - _templateService = templateService; - _logger = logger; - } - public async Task Handle(InviteUserCommand request, CancellationToken cancellationToken) { var tenantId = TenantId.Create(request.TenantId); @@ -50,23 +30,23 @@ public class InviteUserCommandHandler : IRequestHandler throw new ArgumentException($"Invalid role: {request.Role}"); // Check if tenant exists - var tenant = await _tenantRepository.GetByIdAsync(tenantId, cancellationToken); + var tenant = await tenantRepository.GetByIdAsync(tenantId, cancellationToken); if (tenant == null) throw new InvalidOperationException($"Tenant {request.TenantId} not found"); // Check if inviter exists - var inviter = await _userRepository.GetByIdAsync(invitedBy, cancellationToken); + var inviter = await userRepository.GetByIdAsync(invitedBy, cancellationToken); if (inviter == null) throw new InvalidOperationException($"Inviter user {request.InvitedBy} not found"); var email = Email.Create(request.Email); // Check if user already exists in this tenant - var existingUser = await _userRepository.GetByEmailAsync(tenantId, email, cancellationToken); + var existingUser = await userRepository.GetByEmailAsync(tenantId, email, cancellationToken); if (existingUser != null) { // Check if user already has a role in this tenant - var existingRole = await _userTenantRoleRepository.GetByUserAndTenantAsync( + var existingRole = await userTenantRoleRepository.GetByUserAndTenantAsync( UserId.Create(existingUser.Id), tenantId, cancellationToken); @@ -76,7 +56,7 @@ public class InviteUserCommandHandler : IRequestHandler } // Check for existing pending invitation - var existingInvitation = await _invitationRepository.GetPendingByEmailAndTenantAsync( + var existingInvitation = await invitationRepository.GetPendingByEmailAndTenantAsync( request.Email, tenantId, cancellationToken); @@ -85,8 +65,8 @@ public class InviteUserCommandHandler : IRequestHandler throw new InvalidOperationException($"A pending invitation already exists for {request.Email} in this tenant"); // Generate secure token - var token = _tokenService.GenerateToken(); - var tokenHash = _tokenService.HashToken(token); + var token = tokenService.GenerateToken(); + var tokenHash = tokenService.HashToken(token); // Create invitation var invitation = Invitation.Create( @@ -96,11 +76,11 @@ public class InviteUserCommandHandler : IRequestHandler tokenHash, invitedBy); - await _invitationRepository.AddAsync(invitation, cancellationToken); + await invitationRepository.AddAsync(invitation, cancellationToken); // Send invitation email var invitationLink = $"{request.BaseUrl}/accept-invitation?token={token}"; - var htmlBody = _templateService.RenderInvitationEmail( + var htmlBody = templateService.RenderInvitationEmail( recipientName: request.Email.Split('@')[0], // Use email prefix as fallback name tenantName: tenant.Name.Value, inviterName: inviter.FullName.Value, @@ -112,18 +92,18 @@ public class InviteUserCommandHandler : IRequestHandler HtmlBody: htmlBody, PlainTextBody: $"You've been invited to join {tenant.Name.Value}. Click here to accept: {invitationLink}"); - var emailSuccess = await _emailService.SendEmailAsync(emailMessage, cancellationToken); + var emailSuccess = await emailService.SendEmailAsync(emailMessage, cancellationToken); if (!emailSuccess) { - _logger.LogWarning( + logger.LogWarning( "Failed to send invitation email to {Email} for tenant {TenantId}", request.Email, request.TenantId); } else { - _logger.LogInformation( + logger.LogInformation( "Invitation sent to {Email} for tenant {TenantId} with role {Role}", request.Email, request.TenantId, diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/ResendVerificationEmail/ResendVerificationEmailCommandHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/ResendVerificationEmail/ResendVerificationEmailCommandHandler.cs index 3995744..675d53a 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/ResendVerificationEmail/ResendVerificationEmailCommandHandler.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/ResendVerificationEmail/ResendVerificationEmailCommandHandler.cs @@ -16,34 +16,16 @@ namespace ColaFlow.Modules.Identity.Application.Commands.ResendVerificationEmail /// - Rate limiting (1 email per minute) /// - Token rotation (invalidate old token) /// -public class ResendVerificationEmailCommandHandler : IRequestHandler +public class ResendVerificationEmailCommandHandler( + IUserRepository userRepository, + IEmailVerificationTokenRepository tokenRepository, + ISecurityTokenService tokenService, + IEmailService emailService, + IEmailTemplateService templateService, + IRateLimitService rateLimitService, + ILogger logger) + : IRequestHandler { - private readonly IUserRepository _userRepository; - private readonly IEmailVerificationTokenRepository _tokenRepository; - private readonly ISecurityTokenService _tokenService; - private readonly IEmailService _emailService; - private readonly IEmailTemplateService _templateService; - private readonly IRateLimitService _rateLimitService; - private readonly ILogger _logger; - - public ResendVerificationEmailCommandHandler( - IUserRepository userRepository, - IEmailVerificationTokenRepository tokenRepository, - ISecurityTokenService tokenService, - IEmailService emailService, - IEmailTemplateService templateService, - IRateLimitService rateLimitService, - ILogger logger) - { - _userRepository = userRepository; - _tokenRepository = tokenRepository; - _tokenService = tokenService; - _emailService = emailService; - _templateService = templateService; - _rateLimitService = rateLimitService; - _logger = logger; - } - public async Task Handle(ResendVerificationEmailCommand request, CancellationToken cancellationToken) { try @@ -51,25 +33,25 @@ public class ResendVerificationEmailCommandHandler : IRequestHandler +public class ResetPasswordCommandHandler( + IPasswordResetTokenRepository tokenRepository, + IUserRepository userRepository, + IRefreshTokenRepository refreshTokenRepository, + ISecurityTokenService tokenService, + IPasswordHasher passwordHasher, + ILogger logger, + IPublisher publisher) + : IRequestHandler { - private readonly IPasswordResetTokenRepository _tokenRepository; - private readonly IUserRepository _userRepository; - private readonly IRefreshTokenRepository _refreshTokenRepository; - private readonly ISecurityTokenService _tokenService; - private readonly IPasswordHasher _passwordHasher; - private readonly ILogger _logger; - private readonly IPublisher _publisher; - - public ResetPasswordCommandHandler( - IPasswordResetTokenRepository tokenRepository, - IUserRepository userRepository, - IRefreshTokenRepository refreshTokenRepository, - ISecurityTokenService tokenService, - IPasswordHasher passwordHasher, - ILogger logger, - IPublisher publisher) - { - _tokenRepository = tokenRepository; - _userRepository = userRepository; - _refreshTokenRepository = refreshTokenRepository; - _tokenService = tokenService; - _passwordHasher = passwordHasher; - _logger = logger; - _publisher = publisher; - } - public async Task Handle(ResetPasswordCommand request, CancellationToken cancellationToken) { // Validate new password if (string.IsNullOrWhiteSpace(request.NewPassword) || request.NewPassword.Length < 8) { - _logger.LogWarning("Invalid password provided for reset"); + logger.LogWarning("Invalid password provided for reset"); return false; } // Hash the token to look it up - var tokenHash = _tokenService.HashToken(request.Token); - var resetToken = await _tokenRepository.GetByTokenHashAsync(tokenHash, cancellationToken); + var tokenHash = tokenService.HashToken(request.Token); + var resetToken = await tokenRepository.GetByTokenHashAsync(tokenHash, cancellationToken); if (resetToken == null) { - _logger.LogWarning("Password reset token not found"); + logger.LogWarning("Password reset token not found"); return false; } if (!resetToken.IsValid) { - _logger.LogWarning( + logger.LogWarning( "Password reset token is invalid. IsExpired: {IsExpired}, IsUsed: {IsUsed}", resetToken.IsExpired, resetToken.IsUsed); @@ -63,36 +45,36 @@ public class ResetPasswordCommandHandler : IRequestHandler +public class SendVerificationEmailCommandHandler( + IUserRepository userRepository, + IEmailVerificationTokenRepository tokenRepository, + ISecurityTokenService tokenService, + IEmailService emailService, + IEmailTemplateService templateService, + ILogger logger) + : IRequestHandler { - private readonly IUserRepository _userRepository; - private readonly IEmailVerificationTokenRepository _tokenRepository; - private readonly ISecurityTokenService _tokenService; - private readonly IEmailService _emailService; - private readonly IEmailTemplateService _templateService; - private readonly ILogger _logger; - - public SendVerificationEmailCommandHandler( - IUserRepository userRepository, - IEmailVerificationTokenRepository tokenRepository, - ISecurityTokenService tokenService, - IEmailService emailService, - IEmailTemplateService templateService, - ILogger logger) - { - _userRepository = userRepository; - _tokenRepository = tokenRepository; - _tokenService = tokenService; - _emailService = emailService; - _templateService = templateService; - _logger = logger; - } - public async Task Handle(SendVerificationEmailCommand request, CancellationToken cancellationToken) { var userId = UserId.Create(request.UserId); - var user = await _userRepository.GetByIdAsync(userId, cancellationToken); + var user = await userRepository.GetByIdAsync(userId, cancellationToken); if (user == null) { - _logger.LogWarning("User {UserId} not found, cannot send verification email", request.UserId); + logger.LogWarning("User {UserId} not found, cannot send verification email", request.UserId); return Unit.Value; } // If already verified, no need to send email if (user.IsEmailVerified) { - _logger.LogInformation("User {UserId} email already verified, skipping verification email", request.UserId); + logger.LogInformation("User {UserId} email already verified, skipping verification email", request.UserId); return Unit.Value; } // Generate token - var token = _tokenService.GenerateToken(); - var tokenHash = _tokenService.HashToken(token); + var token = tokenService.GenerateToken(); + var tokenHash = tokenService.HashToken(token); // Create verification token entity var verificationToken = EmailVerificationToken.Create( @@ -61,11 +45,11 @@ public class SendVerificationEmailCommandHandler : IRequestHandler +public class VerifyEmailCommandHandler( + IEmailVerificationTokenRepository tokenRepository, + IUserRepository userRepository, + ISecurityTokenService tokenService, + ILogger logger) + : IRequestHandler { - private readonly IEmailVerificationTokenRepository _tokenRepository; - private readonly IUserRepository _userRepository; - private readonly ISecurityTokenService _tokenService; - private readonly ILogger _logger; - - public VerifyEmailCommandHandler( - IEmailVerificationTokenRepository tokenRepository, - IUserRepository userRepository, - ISecurityTokenService tokenService, - ILogger logger) - { - _tokenRepository = tokenRepository; - _userRepository = userRepository; - _tokenService = tokenService; - _logger = logger; - } - public async Task Handle(VerifyEmailCommand request, CancellationToken cancellationToken) { // Hash the token to look it up - var tokenHash = _tokenService.HashToken(request.Token); - var verificationToken = await _tokenRepository.GetByTokenHashAsync(tokenHash, cancellationToken); + var tokenHash = tokenService.HashToken(request.Token); + var verificationToken = await tokenRepository.GetByTokenHashAsync(tokenHash, cancellationToken); if (verificationToken == null) { - _logger.LogWarning("Email verification token not found"); + logger.LogWarning("Email verification token not found"); return false; } if (!verificationToken.IsValid) { - _logger.LogWarning( + logger.LogWarning( "Email verification token is invalid. IsExpired: {IsExpired}, IsVerified: {IsVerified}", verificationToken.IsExpired, verificationToken.IsVerified); @@ -46,22 +34,22 @@ public class VerifyEmailCommandHandler : IRequestHandler /// Event handler for InvitationAcceptedEvent - logs acceptance /// -public class InvitationAcceptedEventHandler : INotificationHandler +public class InvitationAcceptedEventHandler(ILogger logger) + : INotificationHandler { - private readonly ILogger _logger; - - public InvitationAcceptedEventHandler(ILogger logger) - { - _logger = logger; - } - public Task Handle(InvitationAcceptedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation( + logger.LogInformation( "Invitation accepted: Email={Email}, Tenant={TenantId}, Role={Role}", notification.Email, notification.TenantId, diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/InvitationCancelledEventHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/InvitationCancelledEventHandler.cs index 0ff4c97..37d86b7 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/InvitationCancelledEventHandler.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/InvitationCancelledEventHandler.cs @@ -7,18 +7,12 @@ namespace ColaFlow.Modules.Identity.Application.EventHandlers; /// /// Event handler for InvitationCancelledEvent - logs cancellation /// -public class InvitationCancelledEventHandler : INotificationHandler +public class InvitationCancelledEventHandler(ILogger logger) + : INotificationHandler { - private readonly ILogger _logger; - - public InvitationCancelledEventHandler(ILogger logger) - { - _logger = logger; - } - public Task Handle(InvitationCancelledEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation( + logger.LogInformation( "Invitation cancelled: Email={Email}, Tenant={TenantId}", notification.Email, notification.TenantId); diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserInvitedEventHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserInvitedEventHandler.cs index 73e388f..d4ce9a1 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserInvitedEventHandler.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserInvitedEventHandler.cs @@ -7,18 +7,11 @@ namespace ColaFlow.Modules.Identity.Application.EventHandlers; /// /// Event handler for UserInvitedEvent - logs invitation /// -public class UserInvitedEventHandler : INotificationHandler +public class UserInvitedEventHandler(ILogger logger) : INotificationHandler { - private readonly ILogger _logger; - - public UserInvitedEventHandler(ILogger logger) - { - _logger = logger; - } - public Task Handle(UserInvitedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation( + logger.LogInformation( "User invited: Email={Email}, Tenant={TenantId}, Role={Role}, InvitedBy={InvitedBy}", notification.Email, notification.TenantId, diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetPendingInvitations/GetPendingInvitationsQueryHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetPendingInvitations/GetPendingInvitationsQueryHandler.cs index 330e656..ae754df 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetPendingInvitations/GetPendingInvitationsQueryHandler.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetPendingInvitations/GetPendingInvitationsQueryHandler.cs @@ -6,34 +6,24 @@ using Microsoft.Extensions.Logging; namespace ColaFlow.Modules.Identity.Application.Queries.GetPendingInvitations; -public class GetPendingInvitationsQueryHandler : IRequestHandler> +public class GetPendingInvitationsQueryHandler( + IInvitationRepository invitationRepository, + IUserRepository userRepository, + ILogger logger) + : IRequestHandler> { - private readonly IInvitationRepository _invitationRepository; - private readonly IUserRepository _userRepository; - private readonly ILogger _logger; - - public GetPendingInvitationsQueryHandler( - IInvitationRepository invitationRepository, - IUserRepository userRepository, - ILogger logger) - { - _invitationRepository = invitationRepository; - _userRepository = userRepository; - _logger = logger; - } - public async Task> Handle(GetPendingInvitationsQuery request, CancellationToken cancellationToken) { var tenantId = TenantId.Create(request.TenantId); // Get all pending invitations for the tenant - var invitations = await _invitationRepository.GetPendingByTenantAsync(tenantId, cancellationToken); + var invitations = await invitationRepository.GetPendingByTenantAsync(tenantId, cancellationToken); // Get all unique inviter user IDs var inviterIds = invitations.Select(i => (Guid)i.InvitedBy).Distinct().ToList(); // Fetch all inviters in one query - var inviters = await _userRepository.GetByIdsAsync(inviterIds, cancellationToken); + var inviters = await userRepository.GetByIdsAsync(inviterIds, cancellationToken); var inviterDict = inviters.ToDictionary(u => u.Id, u => u.FullName.Value); // Map to DTOs @@ -47,7 +37,7 @@ public class GetPendingInvitationsQueryHandler : IRequestHandler -public class DatabaseEmailRateLimiter : IRateLimitService +public class DatabaseEmailRateLimiter( + IdentityDbContext context, + ILogger logger) + : IRateLimitService { - private readonly IdentityDbContext _context; - private readonly ILogger _logger; - - public DatabaseEmailRateLimiter( - IdentityDbContext context, - ILogger logger) - { - _context = context; - _logger = logger; - } - public async Task IsAllowedAsync( string key, int maxAttempts, @@ -39,7 +31,7 @@ public class DatabaseEmailRateLimiter : IRateLimitService var parts = key.Split(':'); if (parts.Length != 3) { - _logger.LogWarning("Invalid rate limit key format: {Key}. Expected format: 'operation:email:tenantId'", key); + logger.LogWarning("Invalid rate limit key format: {Key}. Expected format: 'operation:email:tenantId'", key); return true; // Fail open (allow request) if key format is invalid } @@ -49,12 +41,12 @@ public class DatabaseEmailRateLimiter : IRateLimitService if (!Guid.TryParse(tenantIdStr, out var tenantId)) { - _logger.LogWarning("Invalid tenant ID in rate limit key: {Key}", key); + logger.LogWarning("Invalid tenant ID in rate limit key: {Key}", key); return true; // Fail open } // Find existing rate limit record - var rateLimit = await _context.EmailRateLimits + var rateLimit = await context.EmailRateLimits .FirstOrDefaultAsync( r => r.Email == email && r.TenantId == tenantId && @@ -65,23 +57,23 @@ public class DatabaseEmailRateLimiter : IRateLimitService if (rateLimit == null) { var newRateLimit = EmailRateLimit.Create(email, tenantId, operationType); - _context.EmailRateLimits.Add(newRateLimit); + context.EmailRateLimits.Add(newRateLimit); try { - await _context.SaveChangesAsync(cancellationToken); - _logger.LogInformation( + await context.SaveChangesAsync(cancellationToken); + logger.LogInformation( "Rate limit record created for {Email} - {Operation} (Attempt 1/{MaxAttempts})", email, operationType, maxAttempts); } catch (DbUpdateException ex) { // Handle race condition: another request created the record simultaneously - _logger.LogWarning(ex, + logger.LogWarning(ex, "Race condition detected while creating rate limit record for {Key}. Retrying...", key); // Re-fetch the record created by the concurrent request - rateLimit = await _context.EmailRateLimits + rateLimit = await context.EmailRateLimits .FirstOrDefaultAsync( r => r.Email == email && r.TenantId == tenantId && @@ -90,7 +82,7 @@ public class DatabaseEmailRateLimiter : IRateLimitService if (rateLimit == null) { - _logger.LogError("Failed to fetch rate limit record after race condition for {Key}", key); + logger.LogError("Failed to fetch rate limit record after race condition for {Key}", key); return true; // Fail open } @@ -106,10 +98,10 @@ public class DatabaseEmailRateLimiter : IRateLimitService { // Window expired - reset counter and allow rateLimit.ResetAttempts(); - _context.EmailRateLimits.Update(rateLimit); - await _context.SaveChangesAsync(cancellationToken); + context.EmailRateLimits.Update(rateLimit); + await context.SaveChangesAsync(cancellationToken); - _logger.LogInformation( + logger.LogInformation( "Rate limit window expired for {Email} - {Operation}. Counter reset (Attempt 1/{MaxAttempts})", email, operationType, maxAttempts); @@ -122,7 +114,7 @@ public class DatabaseEmailRateLimiter : IRateLimitService // Rate limit exceeded var remainingTime = window - (DateTime.UtcNow - rateLimit.LastSentAt); - _logger.LogWarning( + logger.LogWarning( "Rate limit EXCEEDED for {Email} - {Operation}: {Attempts}/{MaxAttempts} attempts. " + "Retry after {RemainingSeconds} seconds", email, operationType, rateLimit.AttemptsCount, maxAttempts, @@ -133,10 +125,10 @@ public class DatabaseEmailRateLimiter : IRateLimitService // Still within limit - increment counter and allow rateLimit.RecordAttempt(); - _context.EmailRateLimits.Update(rateLimit); - await _context.SaveChangesAsync(cancellationToken); + context.EmailRateLimits.Update(rateLimit); + await context.SaveChangesAsync(cancellationToken); - _logger.LogInformation( + logger.LogInformation( "Rate limit check passed for {Email} - {Operation} (Attempt {Attempts}/{MaxAttempts})", email, operationType, rateLimit.AttemptsCount, maxAttempts); @@ -150,16 +142,16 @@ public class DatabaseEmailRateLimiter : IRateLimitService { var cutoffDate = DateTime.UtcNow - retentionPeriod; - var expiredRecords = await _context.EmailRateLimits + var expiredRecords = await context.EmailRateLimits .Where(r => r.LastSentAt < cutoffDate) .ToListAsync(cancellationToken); if (expiredRecords.Any()) { - _context.EmailRateLimits.RemoveRange(expiredRecords); - await _context.SaveChangesAsync(cancellationToken); + context.EmailRateLimits.RemoveRange(expiredRecords); + await context.SaveChangesAsync(cancellationToken); - _logger.LogInformation( + logger.LogInformation( "Cleaned up {Count} expired rate limit records older than {CutoffDate}", expiredRecords.Count, cutoffDate); } diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MemoryRateLimitService.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MemoryRateLimitService.cs index d5a3aa8..d6c77c6 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MemoryRateLimitService.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MemoryRateLimitService.cs @@ -7,15 +7,8 @@ namespace ColaFlow.Modules.Identity.Infrastructure.Services; /// In-memory rate limiting service implementation. /// For production, consider using Redis for distributed rate limiting. /// -public class MemoryRateLimitService : IRateLimitService +public class MemoryRateLimitService(IMemoryCache cache) : IRateLimitService { - private readonly IMemoryCache _cache; - - public MemoryRateLimitService(IMemoryCache cache) - { - _cache = cache; - } - public Task IsAllowedAsync( string key, int maxAttempts, @@ -25,7 +18,7 @@ public class MemoryRateLimitService : IRateLimitService var cacheKey = $"ratelimit:{key}"; // Get current attempt count from cache - var attempts = _cache.GetOrCreate(cacheKey, entry => + var attempts = cache.GetOrCreate(cacheKey, entry => { entry.AbsoluteExpirationRelativeToNow = window; return 0; @@ -38,7 +31,7 @@ public class MemoryRateLimitService : IRateLimitService } // Increment attempt count - _cache.Set(cacheKey, attempts + 1, new MemoryCacheEntryOptions + cache.Set(cacheKey, attempts + 1, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = window }); diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MockEmailService.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MockEmailService.cs index 9c6c4f0..e4f89b1 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MockEmailService.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MockEmailService.cs @@ -8,9 +8,8 @@ namespace ColaFlow.Modules.Identity.Infrastructure.Services; /// Mock email service for development/testing that logs emails instead of sending them /// Captures sent emails for testing purposes /// -public sealed class MockEmailService : IEmailService +public sealed class MockEmailService(ILogger logger) : IEmailService { - private readonly ILogger _logger; private readonly List _sentEmails = new(); /// @@ -18,23 +17,18 @@ public sealed class MockEmailService : IEmailService /// public IReadOnlyList SentEmails => _sentEmails.AsReadOnly(); - public MockEmailService(ILogger logger) - { - _logger = logger; - } - public Task SendEmailAsync(EmailMessage message, CancellationToken cancellationToken = default) { // Capture the email for testing _sentEmails.Add(message); - _logger.LogInformation( + logger.LogInformation( "[MOCK EMAIL] To: {To}, Subject: {Subject}, From: {From}", message.To, message.Subject, message.FromEmail ?? "default"); - _logger.LogDebug( + logger.LogDebug( "[MOCK EMAIL] HTML Body: {HtmlBody}", message.HtmlBody); diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/SmtpEmailService.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/SmtpEmailService.cs index d1ee120..3f6a1ae 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/SmtpEmailService.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/SmtpEmailService.cs @@ -10,31 +10,23 @@ namespace ColaFlow.Modules.Identity.Infrastructure.Services; /// /// SMTP-based email service for production use /// -public sealed class SmtpEmailService : IEmailService +public sealed class SmtpEmailService( + ILogger logger, + IConfiguration configuration) + : IEmailService { - private readonly ILogger _logger; - private readonly IConfiguration _configuration; - - public SmtpEmailService( - ILogger logger, - IConfiguration configuration) - { - _logger = logger; - _configuration = configuration; - } - public async Task SendEmailAsync(EmailMessage message, CancellationToken cancellationToken = default) { try { - var smtpHost = _configuration["Email:Smtp:Host"]; - var smtpPort = int.Parse(_configuration["Email:Smtp:Port"] ?? "587"); - var smtpUsername = _configuration["Email:Smtp:Username"]; - var smtpPassword = _configuration["Email:Smtp:Password"]; - var enableSsl = bool.Parse(_configuration["Email:Smtp:EnableSsl"] ?? "true"); + var smtpHost = configuration["Email:Smtp:Host"]; + var smtpPort = int.Parse(configuration["Email:Smtp:Port"] ?? "587"); + var smtpUsername = configuration["Email:Smtp:Username"]; + var smtpPassword = configuration["Email:Smtp:Password"]; + var enableSsl = bool.Parse(configuration["Email:Smtp:EnableSsl"] ?? "true"); - var defaultFromEmail = _configuration["Email:From"] ?? "noreply@colaflow.local"; - var defaultFromName = _configuration["Email:FromName"] ?? "ColaFlow"; + var defaultFromEmail = configuration["Email:From"] ?? "noreply@colaflow.local"; + var defaultFromName = configuration["Email:FromName"] ?? "ColaFlow"; using var smtpClient = new SmtpClient(smtpHost, smtpPort) { @@ -66,7 +58,7 @@ public sealed class SmtpEmailService : IEmailService await smtpClient.SendMailAsync(mailMessage, cancellationToken); - _logger.LogInformation( + logger.LogInformation( "Email sent successfully to {To} with subject: {Subject}", message.To, message.Subject); @@ -75,7 +67,7 @@ public sealed class SmtpEmailService : IEmailService } catch (Exception ex) { - _logger.LogError( + logger.LogError( ex, "Failed to send email to {To} with subject: {Subject}", message.To, diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeAppliedNotificationHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeAppliedNotificationHandler.cs index 487ba0a..807fe72 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeAppliedNotificationHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeAppliedNotificationHandler.cs @@ -9,18 +9,13 @@ namespace ColaFlow.Modules.Mcp.Application.EventHandlers; /// /// Event handler that sends SignalR notifications when a PendingChange is applied /// -public class PendingChangeAppliedNotificationHandler : INotificationHandler +public class PendingChangeAppliedNotificationHandler( + IMcpNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IMcpNotificationService _notificationService; - private readonly ILogger _logger; - - public PendingChangeAppliedNotificationHandler( - IMcpNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IMcpNotificationService _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task Handle(PendingChangeAppliedEvent notification, CancellationToken cancellationToken) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeApprovedEventHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeApprovedEventHandler.cs index 480584f..64eba3c 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeApprovedEventHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeApprovedEventHandler.cs @@ -19,21 +19,15 @@ namespace ColaFlow.Modules.Mcp.Application.EventHandlers; /// Event handler for PendingChangeApprovedEvent /// Executes the approved change by dispatching appropriate commands /// -public class PendingChangeApprovedEventHandler : INotificationHandler +public class PendingChangeApprovedEventHandler( + IMediator mediator, + IPendingChangeService pendingChangeService, + ILogger logger) + : INotificationHandler { - private readonly IMediator _mediator; - private readonly IPendingChangeService _pendingChangeService; - private readonly ILogger _logger; - - public PendingChangeApprovedEventHandler( - IMediator mediator, - IPendingChangeService pendingChangeService, - ILogger logger) - { - _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); - _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + private readonly IPendingChangeService _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task Handle(PendingChangeApprovedEvent notification, CancellationToken cancellationToken) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeApprovedNotificationHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeApprovedNotificationHandler.cs index 5944d09..3b83a61 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeApprovedNotificationHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeApprovedNotificationHandler.cs @@ -10,18 +10,13 @@ namespace ColaFlow.Modules.Mcp.Application.EventHandlers; /// Event handler that sends SignalR notifications when a PendingChange is approved /// Runs in parallel with PendingChangeApprovedEventHandler (which executes the change) /// -public class PendingChangeApprovedNotificationHandler : INotificationHandler +public class PendingChangeApprovedNotificationHandler( + IMcpNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IMcpNotificationService _notificationService; - private readonly ILogger _logger; - - public PendingChangeApprovedNotificationHandler( - IMcpNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IMcpNotificationService _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task Handle(PendingChangeApprovedEvent notification, CancellationToken cancellationToken) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeCreatedNotificationHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeCreatedNotificationHandler.cs index 26bdc38..e4f91b1 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeCreatedNotificationHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeCreatedNotificationHandler.cs @@ -11,21 +11,15 @@ namespace ColaFlow.Modules.Mcp.Application.EventHandlers; /// /// Event handler that sends SignalR notifications when a PendingChange is created /// -public class PendingChangeCreatedNotificationHandler : INotificationHandler +public class PendingChangeCreatedNotificationHandler( + IMcpNotificationService notificationService, + IPendingChangeRepository repository, + ILogger logger) + : INotificationHandler { - private readonly IMcpNotificationService _notificationService; - private readonly IPendingChangeRepository _repository; - private readonly ILogger _logger; - - public PendingChangeCreatedNotificationHandler( - IMcpNotificationService notificationService, - IPendingChangeRepository repository, - ILogger logger) - { - _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IMcpNotificationService _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + private readonly IPendingChangeRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task Handle(PendingChangeCreatedEvent notification, CancellationToken cancellationToken) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeExpiredNotificationHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeExpiredNotificationHandler.cs index cc14d54..c0f1f60 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeExpiredNotificationHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeExpiredNotificationHandler.cs @@ -9,18 +9,13 @@ namespace ColaFlow.Modules.Mcp.Application.EventHandlers; /// /// Event handler that sends SignalR notifications when a PendingChange expires /// -public class PendingChangeExpiredNotificationHandler : INotificationHandler +public class PendingChangeExpiredNotificationHandler( + IMcpNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IMcpNotificationService _notificationService; - private readonly ILogger _logger; - - public PendingChangeExpiredNotificationHandler( - IMcpNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IMcpNotificationService _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task Handle(PendingChangeExpiredEvent notification, CancellationToken cancellationToken) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeRejectedNotificationHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeRejectedNotificationHandler.cs index edad489..760fee2 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeRejectedNotificationHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/EventHandlers/PendingChangeRejectedNotificationHandler.cs @@ -9,18 +9,13 @@ namespace ColaFlow.Modules.Mcp.Application.EventHandlers; /// /// Event handler that sends SignalR notifications when a PendingChange is rejected /// -public class PendingChangeRejectedNotificationHandler : INotificationHandler +public class PendingChangeRejectedNotificationHandler( + IMcpNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IMcpNotificationService _notificationService; - private readonly ILogger _logger; - - public PendingChangeRejectedNotificationHandler( - IMcpNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IMcpNotificationService _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task Handle(PendingChangeRejectedEvent notification, CancellationToken cancellationToken) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/InitializeMethodHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/InitializeMethodHandler.cs index 6288d57..097a3e2 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/InitializeMethodHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/InitializeMethodHandler.cs @@ -7,17 +7,10 @@ namespace ColaFlow.Modules.Mcp.Application.Handlers; /// /// Handler for the 'initialize' MCP method /// -public class InitializeMethodHandler : IMcpMethodHandler +public class InitializeMethodHandler(ILogger logger) : IMcpMethodHandler { - private readonly ILogger _logger; - public string MethodName => "initialize"; - public InitializeMethodHandler(ILogger logger) - { - _logger = logger; - } - public Task HandleAsync(object? @params, CancellationToken cancellationToken) { try @@ -30,7 +23,7 @@ public class InitializeMethodHandler : IMcpMethodHandler initRequest = JsonSerializer.Deserialize(json); } - _logger.LogInformation( + logger.LogInformation( "MCP Initialize handshake received. Client: {ClientName} {ClientVersion}, Protocol: {ProtocolVersion}", initRequest?.ClientInfo?.Name ?? "Unknown", initRequest?.ClientInfo?.Version ?? "Unknown", @@ -39,7 +32,7 @@ public class InitializeMethodHandler : IMcpMethodHandler // Validate protocol version if (initRequest?.ProtocolVersion != "1.0") { - _logger.LogWarning("Unsupported protocol version: {ProtocolVersion}", initRequest?.ProtocolVersion); + logger.LogWarning("Unsupported protocol version: {ProtocolVersion}", initRequest?.ProtocolVersion); } // Create initialize response @@ -54,13 +47,13 @@ public class InitializeMethodHandler : IMcpMethodHandler Capabilities = McpServerCapabilities.CreateDefault() }; - _logger.LogInformation("MCP Initialize handshake completed successfully"); + logger.LogInformation("MCP Initialize handshake completed successfully"); return Task.FromResult(response); } catch (Exception ex) { - _logger.LogError(ex, "Error handling initialize request"); + logger.LogError(ex, "Error handling initialize request"); throw; } } diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourceHealthCheckHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourceHealthCheckHandler.cs index 20c637e..9b9b60f 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourceHealthCheckHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourceHealthCheckHandler.cs @@ -8,26 +8,18 @@ namespace ColaFlow.Modules.Mcp.Application.Handlers; /// Handler for 'resources/health' method /// Checks availability and health of all registered resources /// -public class ResourceHealthCheckHandler : IMcpMethodHandler +public class ResourceHealthCheckHandler( + ILogger logger, + IMcpResourceRegistry resourceRegistry) + : IMcpMethodHandler { - private readonly ILogger _logger; - private readonly IMcpResourceRegistry _resourceRegistry; - public string MethodName => "resources/health"; - public ResourceHealthCheckHandler( - ILogger logger, - IMcpResourceRegistry resourceRegistry) - { - _logger = logger; - _resourceRegistry = resourceRegistry; - } - public async Task HandleAsync(object? @params, CancellationToken cancellationToken) { - _logger.LogDebug("Handling resources/health request"); + logger.LogDebug("Handling resources/health request"); - var resources = _resourceRegistry.GetAllResources(); + var resources = resourceRegistry.GetAllResources(); var healthResults = new List(); var totalResources = resources.Count; var healthyResources = 0; @@ -67,7 +59,7 @@ public class ResourceHealthCheckHandler : IMcpMethodHandler catch (Exception ex) { unhealthyResources++; - _logger.LogError(ex, "Health check failed for resource {ResourceType}", resource.GetType().Name); + logger.LogError(ex, "Health check failed for resource {ResourceType}", resource.GetType().Name); healthResults.Add(new { @@ -82,7 +74,7 @@ public class ResourceHealthCheckHandler : IMcpMethodHandler var overallStatus = unhealthyResources == 0 ? "healthy" : "degraded"; - _logger.LogInformation("Resource health check completed: {Healthy}/{Total} healthy", + logger.LogInformation("Resource health check completed: {Healthy}/{Total} healthy", healthyResources, totalResources); var response = new diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourcesListMethodHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourcesListMethodHandler.cs index 12ce5e2..ea95ee4 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourcesListMethodHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourcesListMethodHandler.cs @@ -7,30 +7,22 @@ namespace ColaFlow.Modules.Mcp.Application.Handlers; /// Handler for the 'resources/list' MCP method /// Returns categorized list of all available resources with full metadata /// -public class ResourcesListMethodHandler : IMcpMethodHandler +public class ResourcesListMethodHandler( + ILogger logger, + IMcpResourceRegistry resourceRegistry) + : IMcpMethodHandler { - private readonly ILogger _logger; - private readonly IMcpResourceRegistry _resourceRegistry; - public string MethodName => "resources/list"; - public ResourcesListMethodHandler( - ILogger logger, - IMcpResourceRegistry resourceRegistry) - { - _logger = logger; - _resourceRegistry = resourceRegistry; - } - public Task HandleAsync(object? @params, CancellationToken cancellationToken) { - _logger.LogDebug("Handling resources/list request"); + logger.LogDebug("Handling resources/list request"); // Get all registered resource descriptors with full metadata - var descriptors = _resourceRegistry.GetResourceDescriptors(); - var categories = _resourceRegistry.GetCategories(); + var descriptors = resourceRegistry.GetResourceDescriptors(); + var categories = resourceRegistry.GetCategories(); - _logger.LogInformation("Returning {Count} MCP resources in {CategoryCount} categories", + logger.LogInformation("Returning {Count} MCP resources in {CategoryCount} categories", descriptors.Count, categories.Count); // Group by category for better organization diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourcesReadMethodHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourcesReadMethodHandler.cs index 301f320..0405e74 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourcesReadMethodHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ResourcesReadMethodHandler.cs @@ -10,24 +10,16 @@ namespace ColaFlow.Modules.Mcp.Application.Handlers; /// /// Handler for the 'resources/read' MCP method /// -public class ResourcesReadMethodHandler : IMcpMethodHandler +public class ResourcesReadMethodHandler( + ILogger logger, + IMcpResourceRegistry resourceRegistry) + : IMcpMethodHandler { - private readonly ILogger _logger; - private readonly IMcpResourceRegistry _resourceRegistry; - public string MethodName => "resources/read"; - public ResourcesReadMethodHandler( - ILogger logger, - IMcpResourceRegistry resourceRegistry) - { - _logger = logger; - _resourceRegistry = resourceRegistry; - } - public async Task HandleAsync(object? @params, CancellationToken cancellationToken) { - _logger.LogDebug("Handling resources/read request"); + logger.LogDebug("Handling resources/read request"); // Parse parameters var paramsJson = JsonSerializer.Serialize(@params); @@ -38,10 +30,10 @@ public class ResourcesReadMethodHandler : IMcpMethodHandler throw new McpInvalidParamsException("Missing required parameter: uri"); } - _logger.LogInformation("Reading resource: {Uri}", request.Uri); + logger.LogInformation("Reading resource: {Uri}", request.Uri); // Find resource by URI - var resource = _resourceRegistry.GetResourceByUri(request.Uri); + var resource = resourceRegistry.GetResourceByUri(request.Uri); if (resource == null) { throw new McpNotFoundException($"Resource not found: {request.Uri}"); diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ToolsCallMethodHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ToolsCallMethodHandler.cs index 631a503..ad8d707 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ToolsCallMethodHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ToolsCallMethodHandler.cs @@ -5,20 +5,13 @@ namespace ColaFlow.Modules.Mcp.Application.Handlers; /// /// Handler for the 'tools/call' MCP method /// -public class ToolsCallMethodHandler : IMcpMethodHandler +public class ToolsCallMethodHandler(ILogger logger) : IMcpMethodHandler { - private readonly ILogger _logger; - public string MethodName => "tools/call"; - public ToolsCallMethodHandler(ILogger logger) - { - _logger = logger; - } - public Task HandleAsync(object? @params, CancellationToken cancellationToken) { - _logger.LogDebug("Handling tools/call request"); + logger.LogDebug("Handling tools/call request"); // TODO: Implement in Story 5.11 (Core MCP Tools) // For now, return error diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ToolsListMethodHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ToolsListMethodHandler.cs index 89fc3bc..2f3bb4f 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ToolsListMethodHandler.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Handlers/ToolsListMethodHandler.cs @@ -5,20 +5,13 @@ namespace ColaFlow.Modules.Mcp.Application.Handlers; /// /// Handler for the 'tools/list' MCP method /// -public class ToolsListMethodHandler : IMcpMethodHandler +public class ToolsListMethodHandler(ILogger logger) : IMcpMethodHandler { - private readonly ILogger _logger; - public string MethodName => "tools/list"; - public ToolsListMethodHandler(ILogger logger) - { - _logger = logger; - } - public Task HandleAsync(object? @params, CancellationToken cancellationToken) { - _logger.LogDebug("Handling tools/list request"); + logger.LogDebug("Handling tools/list request"); // TODO: Implement in Story 5.11 (Core MCP Tools) // For now, return empty list diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/IssuesGetResource.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/IssuesGetResource.cs index ebada61..2fae313 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/IssuesGetResource.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/IssuesGetResource.cs @@ -12,7 +12,11 @@ namespace ColaFlow.Modules.Mcp.Application.Resources; /// Resource: colaflow://issues.get/{id} /// Gets detailed information about a specific issue (Epic, Story, or Task) /// -public class IssuesGetResource : IMcpResource +public class IssuesGetResource( + IProjectRepository projectRepository, + ITenantContext tenantContext, + ILogger logger) + : IMcpResource { public string Uri => "colaflow://issues.get/{id}"; public string Name => "Issue Details"; @@ -21,25 +25,11 @@ public class IssuesGetResource : IMcpResource public string Category => "Issues"; public string Version => "1.0"; - private readonly IProjectRepository _projectRepository; - private readonly ITenantContext _tenantContext; - private readonly ILogger _logger; - - public IssuesGetResource( - IProjectRepository projectRepository, - ITenantContext tenantContext, - ILogger logger) - { - _projectRepository = projectRepository; - _tenantContext = tenantContext; - _logger = logger; - } - public async Task GetContentAsync( McpResourceRequest request, CancellationToken cancellationToken) { - var tenantId = _tenantContext.GetCurrentTenantId(); + var tenantId = tenantContext.GetCurrentTenantId(); // Extract {id} from URI parameters if (!request.UriParams.TryGetValue("id", out var idString)) @@ -52,10 +42,10 @@ public class IssuesGetResource : IMcpResource throw new McpInvalidParamsException($"Invalid issue ID format: {idString}"); } - _logger.LogDebug("Fetching issue {IssueId} for tenant {TenantId}", issueIdGuid, tenantId); + logger.LogDebug("Fetching issue {IssueId} for tenant {TenantId}", issueIdGuid, tenantId); // Try to find as Epic - var epic = await _projectRepository.GetEpicByIdReadOnlyAsync(EpicId.From(issueIdGuid), cancellationToken); + var epic = await projectRepository.GetEpicByIdReadOnlyAsync(EpicId.From(issueIdGuid), cancellationToken); if (epic != null) { var epicDto = new @@ -89,7 +79,7 @@ public class IssuesGetResource : IMcpResource } // Try to find as Story - var story = await _projectRepository.GetStoryByIdReadOnlyAsync(StoryId.From(issueIdGuid), cancellationToken); + var story = await projectRepository.GetStoryByIdReadOnlyAsync(StoryId.From(issueIdGuid), cancellationToken); if (story != null) { var storyDto = new @@ -124,7 +114,7 @@ public class IssuesGetResource : IMcpResource } // Try to find as Task - var task = await _projectRepository.GetTaskByIdReadOnlyAsync(TaskId.From(issueIdGuid), cancellationToken); + var task = await projectRepository.GetTaskByIdReadOnlyAsync(TaskId.From(issueIdGuid), cancellationToken); if (task != null) { var taskDto = new diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/IssuesSearchResource.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/IssuesSearchResource.cs index 42cdaa8..8b06b32 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/IssuesSearchResource.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/IssuesSearchResource.cs @@ -12,7 +12,11 @@ namespace ColaFlow.Modules.Mcp.Application.Resources; /// Searches issues with filters (Epics, Stories, Tasks) /// Query params: status, priority, assignee, type, project, limit, offset /// -public class IssuesSearchResource : IMcpResource +public class IssuesSearchResource( + IProjectRepository projectRepository, + ITenantContext tenantContext, + ILogger logger) + : IMcpResource { public string Uri => "colaflow://issues.search"; public string Name => "Issues Search"; @@ -21,27 +25,13 @@ public class IssuesSearchResource : IMcpResource public string Category => "Issues"; public string Version => "1.0"; - private readonly IProjectRepository _projectRepository; - private readonly ITenantContext _tenantContext; - private readonly ILogger _logger; - - public IssuesSearchResource( - IProjectRepository projectRepository, - ITenantContext tenantContext, - ILogger logger) - { - _projectRepository = projectRepository; - _tenantContext = tenantContext; - _logger = logger; - } - public async Task GetContentAsync( McpResourceRequest request, CancellationToken cancellationToken) { - var tenantId = _tenantContext.GetCurrentTenantId(); + var tenantId = tenantContext.GetCurrentTenantId(); - _logger.LogDebug("Searching issues for tenant {TenantId} with filters: {@Filters}", + logger.LogDebug("Searching issues for tenant {TenantId} with filters: {@Filters}", tenantId, request.QueryParams); // Parse query parameters @@ -57,13 +47,13 @@ public class IssuesSearchResource : IMcpResource limit = Math.Min(limit, 100); // Get all projects - var projects = await _projectRepository.GetAllProjectsReadOnlyAsync(cancellationToken); + var projects = await projectRepository.GetAllProjectsReadOnlyAsync(cancellationToken); // Filter by project if specified if (!string.IsNullOrEmpty(projectFilter) && Guid.TryParse(projectFilter, out var projectIdGuid)) { var projectId = ProjectId.From(projectIdGuid); - var project = await _projectRepository.GetProjectWithFullHierarchyReadOnlyAsync(projectId, cancellationToken); + var project = await projectRepository.GetProjectWithFullHierarchyReadOnlyAsync(projectId, cancellationToken); projects = project != null ? new List { project } : new(); } else @@ -72,7 +62,7 @@ public class IssuesSearchResource : IMcpResource var projectsWithHierarchy = new List(); foreach (var p in projects) { - var fullProject = await _projectRepository.GetProjectWithFullHierarchyReadOnlyAsync(p.Id, cancellationToken); + var fullProject = await projectRepository.GetProjectWithFullHierarchyReadOnlyAsync(p.Id, cancellationToken); if (fullProject != null) { projectsWithHierarchy.Add(fullProject); @@ -180,7 +170,7 @@ public class IssuesSearchResource : IMcpResource offset = offset }, new JsonSerializerOptions { WriteIndented = true }); - _logger.LogInformation("Found {Count} issues for tenant {TenantId} (total: {Total})", + logger.LogInformation("Found {Count} issues for tenant {TenantId} (total: {Total})", paginatedIssues.Count, tenantId, total); return new McpResourceContent diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/ProjectsGetResource.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/ProjectsGetResource.cs index 10399be..80c2f43 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/ProjectsGetResource.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/ProjectsGetResource.cs @@ -12,7 +12,11 @@ namespace ColaFlow.Modules.Mcp.Application.Resources; /// Resource: colaflow://projects.get/{id} /// Gets detailed information about a specific project /// -public class ProjectsGetResource : IMcpResource +public class ProjectsGetResource( + IProjectRepository projectRepository, + ITenantContext tenantContext, + ILogger logger) + : IMcpResource { public string Uri => "colaflow://projects.get/{id}"; public string Name => "Project Details"; @@ -21,20 +25,6 @@ public class ProjectsGetResource : IMcpResource public string Category => "Projects"; public string Version => "1.0"; - private readonly IProjectRepository _projectRepository; - private readonly ITenantContext _tenantContext; - private readonly ILogger _logger; - - public ProjectsGetResource( - IProjectRepository projectRepository, - ITenantContext tenantContext, - ILogger logger) - { - _projectRepository = projectRepository; - _tenantContext = tenantContext; - _logger = logger; - } - public McpResourceDescriptor GetDescriptor() { return new McpResourceDescriptor @@ -63,7 +53,7 @@ public class ProjectsGetResource : IMcpResource McpResourceRequest request, CancellationToken cancellationToken) { - var tenantId = _tenantContext.GetCurrentTenantId(); + var tenantId = tenantContext.GetCurrentTenantId(); // Extract {id} from URI parameters if (!request.UriParams.TryGetValue("id", out var idString)) @@ -78,10 +68,10 @@ public class ProjectsGetResource : IMcpResource var projectId = ProjectId.From(projectIdGuid); - _logger.LogDebug("Fetching project {ProjectId} for tenant {TenantId}", projectId, tenantId); + logger.LogDebug("Fetching project {ProjectId} for tenant {TenantId}", projectId, tenantId); // Get project with full hierarchy (read-only) - var project = await _projectRepository.GetProjectWithFullHierarchyReadOnlyAsync(projectId, cancellationToken); + var project = await projectRepository.GetProjectWithFullHierarchyReadOnlyAsync(projectId, cancellationToken); if (project == null) { @@ -113,7 +103,7 @@ public class ProjectsGetResource : IMcpResource var json = JsonSerializer.Serialize(projectDto, new JsonSerializerOptions { WriteIndented = true }); - _logger.LogInformation("Retrieved project {ProjectId} for tenant {TenantId}", projectId, tenantId); + logger.LogInformation("Retrieved project {ProjectId} for tenant {TenantId}", projectId, tenantId); return new McpResourceContent { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/ProjectsListResource.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/ProjectsListResource.cs index bfca876..acdd377 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/ProjectsListResource.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/ProjectsListResource.cs @@ -10,7 +10,11 @@ namespace ColaFlow.Modules.Mcp.Application.Resources; /// Resource: colaflow://projects.list /// Lists all projects in the current tenant /// -public class ProjectsListResource : IMcpResource +public class ProjectsListResource( + IProjectRepository projectRepository, + ITenantContext tenantContext, + ILogger logger) + : IMcpResource { public string Uri => "colaflow://projects.list"; public string Name => "Projects List"; @@ -19,20 +23,6 @@ public class ProjectsListResource : IMcpResource public string Category => "Projects"; public string Version => "1.0"; - private readonly IProjectRepository _projectRepository; - private readonly ITenantContext _tenantContext; - private readonly ILogger _logger; - - public ProjectsListResource( - IProjectRepository projectRepository, - ITenantContext tenantContext, - ILogger logger) - { - _projectRepository = projectRepository; - _tenantContext = tenantContext; - _logger = logger; - } - public McpResourceDescriptor GetDescriptor() { return new McpResourceDescriptor @@ -58,12 +48,12 @@ public class ProjectsListResource : IMcpResource McpResourceRequest request, CancellationToken cancellationToken) { - var tenantId = _tenantContext.GetCurrentTenantId(); + var tenantId = tenantContext.GetCurrentTenantId(); - _logger.LogDebug("Fetching projects list for tenant {TenantId}", tenantId); + logger.LogDebug("Fetching projects list for tenant {TenantId}", tenantId); // Get all projects (read-only) - var projects = await _projectRepository.GetAllProjectsReadOnlyAsync(cancellationToken); + var projects = await projectRepository.GetAllProjectsReadOnlyAsync(cancellationToken); // Map to DTOs var projectDtos = projects.Select(p => new @@ -84,7 +74,7 @@ public class ProjectsListResource : IMcpResource total = projectDtos.Count }, new JsonSerializerOptions { WriteIndented = true }); - _logger.LogInformation("Retrieved {Count} projects for tenant {TenantId}", projectDtos.Count, tenantId); + logger.LogInformation("Retrieved {Count} projects for tenant {TenantId}", projectDtos.Count, tenantId); return new McpResourceContent { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/SprintsCurrentResource.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/SprintsCurrentResource.cs index 6daf198..576fa32 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/SprintsCurrentResource.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/SprintsCurrentResource.cs @@ -11,7 +11,11 @@ namespace ColaFlow.Modules.Mcp.Application.Resources; /// Resource: colaflow://sprints.current /// Gets the currently active Sprint(s) /// -public class SprintsCurrentResource : IMcpResource +public class SprintsCurrentResource( + IProjectRepository projectRepository, + ITenantContext tenantContext, + ILogger logger) + : IMcpResource { public string Uri => "colaflow://sprints.current"; public string Name => "Current Sprint"; @@ -20,34 +24,20 @@ public class SprintsCurrentResource : IMcpResource public string Category => "Sprints"; public string Version => "1.0"; - private readonly IProjectRepository _projectRepository; - private readonly ITenantContext _tenantContext; - private readonly ILogger _logger; - - public SprintsCurrentResource( - IProjectRepository projectRepository, - ITenantContext tenantContext, - ILogger logger) - { - _projectRepository = projectRepository; - _tenantContext = tenantContext; - _logger = logger; - } - public async Task GetContentAsync( McpResourceRequest request, CancellationToken cancellationToken) { - var tenantId = _tenantContext.GetCurrentTenantId(); + var tenantId = tenantContext.GetCurrentTenantId(); - _logger.LogDebug("Fetching active sprints for tenant {TenantId}", tenantId); + logger.LogDebug("Fetching active sprints for tenant {TenantId}", tenantId); // Get active sprints - var activeSprints = await _projectRepository.GetActiveSprintsAsync(cancellationToken); + var activeSprints = await projectRepository.GetActiveSprintsAsync(cancellationToken); if (activeSprints.Count == 0) { - _logger.LogWarning("No active sprints found for tenant {TenantId}", tenantId); + logger.LogWarning("No active sprints found for tenant {TenantId}", tenantId); throw new McpNotFoundException("No active sprints found"); } @@ -75,7 +65,7 @@ public class SprintsCurrentResource : IMcpResource total = sprintDtos.Count }, new JsonSerializerOptions { WriteIndented = true }); - _logger.LogInformation("Retrieved {Count} active sprints for tenant {TenantId}", + logger.LogInformation("Retrieved {Count} active sprints for tenant {TenantId}", sprintDtos.Count, tenantId); return new McpResourceContent diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/UsersListResource.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/UsersListResource.cs index a5c0a45..44ab431 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/UsersListResource.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Resources/UsersListResource.cs @@ -12,7 +12,11 @@ namespace ColaFlow.Modules.Mcp.Application.Resources; /// Lists all team members in the current tenant /// Query params: project (optional filter by project) /// -public class UsersListResource : IMcpResource +public class UsersListResource( + IUserRepository userRepository, + ITenantContext tenantContext, + ILogger logger) + : IMcpResource { public string Uri => "colaflow://users.list"; public string Name => "Team Members"; @@ -21,30 +25,16 @@ public class UsersListResource : IMcpResource public string Category => "Users"; public string Version => "1.0"; - private readonly IUserRepository _userRepository; - private readonly ITenantContext _tenantContext; - private readonly ILogger _logger; - - public UsersListResource( - IUserRepository userRepository, - ITenantContext tenantContext, - ILogger logger) - { - _userRepository = userRepository; - _tenantContext = tenantContext; - _logger = logger; - } - public async Task GetContentAsync( McpResourceRequest request, CancellationToken cancellationToken) { - var tenantId = _tenantContext.GetCurrentTenantId(); + var tenantId = tenantContext.GetCurrentTenantId(); - _logger.LogDebug("Fetching users list for tenant {TenantId}", tenantId); + logger.LogDebug("Fetching users list for tenant {TenantId}", tenantId); // Get all users for tenant - var users = await _userRepository.GetAllByTenantAsync(TenantId.Create(tenantId), cancellationToken); + var users = await userRepository.GetAllByTenantAsync(TenantId.Create(tenantId), cancellationToken); // Map to DTOs var userDtos = users.Select(u => new @@ -64,7 +54,7 @@ public class UsersListResource : IMcpResource total = userDtos.Count }, new JsonSerializerOptions { WriteIndented = true }); - _logger.LogInformation("Retrieved {Count} users for tenant {TenantId}", userDtos.Count, tenantId); + logger.LogInformation("Retrieved {Count} users for tenant {TenantId}", userDtos.Count, tenantId); return new McpResourceContent { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpApiKeyService.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpApiKeyService.cs index f4c5f6e..241f20d 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpApiKeyService.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpApiKeyService.cs @@ -9,18 +9,13 @@ namespace ColaFlow.Modules.Mcp.Application.Services; /// /// Service implementation for MCP API Key management /// -public class McpApiKeyService : IMcpApiKeyService +public class McpApiKeyService( + IMcpApiKeyRepository repository, + ILogger logger) + : IMcpApiKeyService { - private readonly IMcpApiKeyRepository _repository; - private readonly ILogger _logger; - - public McpApiKeyService( - IMcpApiKeyRepository repository, - ILogger logger) - { - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IMcpApiKeyRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task CreateAsync(CreateApiKeyRequest request, CancellationToken cancellationToken = default) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpResourceRegistry.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpResourceRegistry.cs index cdfef18..7881d50 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpResourceRegistry.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpResourceRegistry.cs @@ -8,32 +8,26 @@ namespace ColaFlow.Modules.Mcp.Application.Services; /// Implementation of MCP Resource Registry /// Enhanced with category support and dynamic registration /// -public class McpResourceRegistry : IMcpResourceRegistry +public class McpResourceRegistry(ILogger logger) : IMcpResourceRegistry { - private readonly ILogger _logger; private readonly Dictionary _resources = new(); private readonly List _resourceList = new(); private readonly object _lock = new(); - public McpResourceRegistry(ILogger logger) - { - _logger = logger; - } - public void RegisterResource(IMcpResource resource) { lock (_lock) { if (_resources.ContainsKey(resource.Uri)) { - _logger.LogWarning("Resource already registered: {Uri}. Overwriting.", resource.Uri); + logger.LogWarning("Resource already registered: {Uri}. Overwriting.", resource.Uri); _resourceList.Remove(_resources[resource.Uri]); } _resources[resource.Uri] = resource; _resourceList.Add(resource); - _logger.LogInformation("Registered MCP Resource: {Uri} - {Name} [{Category}]", + logger.LogInformation("Registered MCP Resource: {Uri} - {Name} [{Category}]", resource.Uri, resource.Name, resource.Category); } } diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/PendingChangeService.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/PendingChangeService.cs index 831ddd0..cb7da5f 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/PendingChangeService.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/PendingChangeService.cs @@ -13,27 +13,19 @@ namespace ColaFlow.Modules.Mcp.Application.Services; /// /// Service implementation for PendingChange management /// -public class PendingChangeService : IPendingChangeService +public class PendingChangeService( + IPendingChangeRepository repository, + ITenantContext tenantContext, + IHttpContextAccessor httpContextAccessor, + IPublisher publisher, + ILogger logger) + : IPendingChangeService { - private readonly IPendingChangeRepository _repository; - private readonly ITenantContext _tenantContext; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IPublisher _publisher; - private readonly ILogger _logger; - - public PendingChangeService( - IPendingChangeRepository repository, - ITenantContext tenantContext, - IHttpContextAccessor httpContextAccessor, - IPublisher publisher, - ILogger logger) - { - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext)); - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - _publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IPendingChangeRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext)); + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + private readonly IPublisher _publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task CreateAsync( CreatePendingChangeRequest request, diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/ResourceDiscoveryService.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/ResourceDiscoveryService.cs index 8fb5a1e..e0c7e9a 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/ResourceDiscoveryService.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/ResourceDiscoveryService.cs @@ -9,18 +9,11 @@ namespace ColaFlow.Modules.Mcp.Application.Services; /// Implementation of Resource Discovery Service /// Scans assemblies to find all IMcpResource implementations /// -public class ResourceDiscoveryService : IResourceDiscoveryService +public class ResourceDiscoveryService(ILogger logger) : IResourceDiscoveryService { - private readonly ILogger _logger; - - public ResourceDiscoveryService(ILogger logger) - { - _logger = logger; - } - public IReadOnlyList DiscoverResourceTypes() { - _logger.LogInformation("Starting MCP Resource discovery via Assembly scanning..."); + logger.LogInformation("Starting MCP Resource discovery via Assembly scanning..."); var resourceTypes = new List(); @@ -29,7 +22,7 @@ public class ResourceDiscoveryService : IResourceDiscoveryService .Where(a => !a.IsDynamic && a.FullName != null && a.FullName.StartsWith("ColaFlow")) .ToList(); - _logger.LogDebug("Scanning {Count} assemblies for IMcpResource implementations", assemblies.Count); + logger.LogDebug("Scanning {Count} assemblies for IMcpResource implementations", assemblies.Count); foreach (var assembly in assemblies) { @@ -44,18 +37,18 @@ public class ResourceDiscoveryService : IResourceDiscoveryService if (types.Any()) { - _logger.LogDebug("Found {Count} resources in assembly {Assembly}", + logger.LogDebug("Found {Count} resources in assembly {Assembly}", types.Count, assembly.GetName().Name); resourceTypes.AddRange(types); } } catch (ReflectionTypeLoadException ex) { - _logger.LogWarning(ex, "Failed to load types from assembly {Assembly}", assembly.FullName); + logger.LogWarning(ex, "Failed to load types from assembly {Assembly}", assembly.FullName); } } - _logger.LogInformation("Discovered {Count} MCP Resource types", resourceTypes.Count); + logger.LogInformation("Discovered {Count} MCP Resource types", resourceTypes.Count); return resourceTypes.AsReadOnly(); } @@ -75,17 +68,17 @@ public class ResourceDiscoveryService : IResourceDiscoveryService if (resource != null) { resources.Add(resource); - _logger.LogDebug("Instantiated resource: {ResourceType} -> {Uri}", + logger.LogDebug("Instantiated resource: {ResourceType} -> {Uri}", resourceType.Name, resource.Uri); } } catch (Exception ex) { - _logger.LogError(ex, "Failed to instantiate resource type {ResourceType}", resourceType.FullName); + logger.LogError(ex, "Failed to instantiate resource type {ResourceType}", resourceType.FullName); } } - _logger.LogInformation("Instantiated {Count} MCP Resources", resources.Count); + logger.LogInformation("Instantiated {Count} MCP Resources", resources.Count); return resources.AsReadOnly(); } diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/AddCommentTool.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/AddCommentTool.cs index 0e68e96..95db0a7 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/AddCommentTool.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/AddCommentTool.cs @@ -16,13 +16,19 @@ namespace ColaFlow.Modules.Mcp.Application.Tools; /// Adds a comment to an existing Issue /// Generates a Diff Preview and creates a PendingChange for approval /// -public class AddCommentTool : IMcpTool +public class AddCommentTool( + IPendingChangeService pendingChangeService, + IIssueRepository issueRepository, + IHttpContextAccessor httpContextAccessor, + DiffPreviewService diffPreviewService, + ILogger logger) + : IMcpTool { - private readonly IPendingChangeService _pendingChangeService; - private readonly IIssueRepository _issueRepository; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly DiffPreviewService _diffPreviewService; - private readonly ILogger _logger; + private readonly IPendingChangeService _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); + private readonly IIssueRepository _issueRepository = issueRepository ?? throw new ArgumentNullException(nameof(issueRepository)); + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + private readonly DiffPreviewService _diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public string Name => "add_comment"; @@ -52,20 +58,6 @@ public class AddCommentTool : IMcpTool Required = new List { "issueId", "content" } }; - public AddCommentTool( - IPendingChangeService pendingChangeService, - IIssueRepository issueRepository, - IHttpContextAccessor httpContextAccessor, - DiffPreviewService diffPreviewService, - ILogger logger) - { - _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); - _issueRepository = issueRepository ?? throw new ArgumentNullException(nameof(issueRepository)); - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - _diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - public async Task ExecuteAsync( McpToolCall toolCall, CancellationToken cancellationToken) diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/CreateIssueTool.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/CreateIssueTool.cs index 3309b4e..81ede7e 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/CreateIssueTool.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/CreateIssueTool.cs @@ -18,13 +18,19 @@ namespace ColaFlow.Modules.Mcp.Application.Tools; /// Creates a new Issue (Epic, Story, Task, or Bug) /// Generates a Diff Preview and creates a PendingChange for approval /// -public class CreateIssueTool : IMcpTool +public class CreateIssueTool( + IPendingChangeService pendingChangeService, + IProjectRepository projectRepository, + ITenantContext tenantContext, + DiffPreviewService diffPreviewService, + ILogger logger) + : IMcpTool { - private readonly IPendingChangeService _pendingChangeService; - private readonly IProjectRepository _projectRepository; - private readonly ITenantContext _tenantContext; - private readonly DiffPreviewService _diffPreviewService; - private readonly ILogger _logger; + private readonly IPendingChangeService _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); + private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext)); + private readonly DiffPreviewService _diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public string Name => "create_issue"; @@ -77,20 +83,6 @@ public class CreateIssueTool : IMcpTool Required = new List { "projectId", "title", "type" } }; - public CreateIssueTool( - IPendingChangeService pendingChangeService, - IProjectRepository projectRepository, - ITenantContext tenantContext, - DiffPreviewService diffPreviewService, - ILogger logger) - { - _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); - _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); - _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext)); - _diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - public async Task ExecuteAsync( McpToolCall toolCall, CancellationToken cancellationToken) diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/UpdateStatusTool.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/UpdateStatusTool.cs index 337db71..bba6164 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/UpdateStatusTool.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/UpdateStatusTool.cs @@ -16,12 +16,17 @@ namespace ColaFlow.Modules.Mcp.Application.Tools; /// Updates the status of an existing Issue /// Generates a Diff Preview and creates a PendingChange for approval /// -public class UpdateStatusTool : IMcpTool +public class UpdateStatusTool( + IPendingChangeService pendingChangeService, + IIssueRepository issueRepository, + DiffPreviewService diffPreviewService, + ILogger logger) + : IMcpTool { - private readonly IPendingChangeService _pendingChangeService; - private readonly IIssueRepository _issueRepository; - private readonly DiffPreviewService _diffPreviewService; - private readonly ILogger _logger; + private readonly IPendingChangeService _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); + private readonly IIssueRepository _issueRepository = issueRepository ?? throw new ArgumentNullException(nameof(issueRepository)); + private readonly DiffPreviewService _diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public string Name => "update_status"; @@ -50,18 +55,6 @@ public class UpdateStatusTool : IMcpTool Required = new List { "issueId", "newStatus" } }; - public UpdateStatusTool( - IPendingChangeService pendingChangeService, - IIssueRepository issueRepository, - DiffPreviewService diffPreviewService, - ILogger logger) - { - _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); - _issueRepository = issueRepository ?? throw new ArgumentNullException(nameof(issueRepository)); - _diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - public async Task ExecuteAsync( McpToolCall toolCall, CancellationToken cancellationToken) diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Domain/Services/TaskLockService.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Domain/Services/TaskLockService.cs index f9195d4..6c0be5f 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Domain/Services/TaskLockService.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Domain/Services/TaskLockService.cs @@ -6,14 +6,9 @@ namespace ColaFlow.Modules.Mcp.Domain.Services; /// /// Domain service for managing task locks and concurrency control /// -public sealed class TaskLockService +public sealed class TaskLockService(ITaskLockRepository taskLockRepository) { - private readonly ITaskLockRepository _taskLockRepository; - - public TaskLockService(ITaskLockRepository taskLockRepository) - { - _taskLockRepository = taskLockRepository ?? throw new ArgumentNullException(nameof(taskLockRepository)); - } + private readonly ITaskLockRepository _taskLockRepository = taskLockRepository ?? throw new ArgumentNullException(nameof(taskLockRepository)); /// /// Try to acquire a lock for a resource diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Auditing/McpSecurityAuditLogger.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Auditing/McpSecurityAuditLogger.cs index 2f20f83..5ec06d0 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Auditing/McpSecurityAuditLogger.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Auditing/McpSecurityAuditLogger.cs @@ -71,18 +71,11 @@ public class McpAuditStatistics /// /// Implementation of MCP security audit logger /// -public class McpSecurityAuditLogger : IMcpSecurityAuditLogger +public class McpSecurityAuditLogger(ILogger logger) : IMcpSecurityAuditLogger { - private readonly ILogger _logger; - private readonly McpAuditStatistics _statistics; + private readonly McpAuditStatistics _statistics = new(); private readonly object _statsLock = new(); - public McpSecurityAuditLogger(ILogger logger) - { - _logger = logger; - _statistics = new McpAuditStatistics(); - } - /// /// Log successful MCP operation /// @@ -94,7 +87,7 @@ public class McpSecurityAuditLogger : IMcpSecurityAuditLogger _statistics.SuccessfulOperations++; } - _logger.LogInformation( + logger.LogInformation( "MCP Operation SUCCESS | Tenant: {TenantId} | User: {UserId} | Operation: {Operation} | Resource: {ResourceType}/{ResourceId}", auditEvent.TenantId, auditEvent.UserId, @@ -115,7 +108,7 @@ public class McpSecurityAuditLogger : IMcpSecurityAuditLogger _statistics.AuthenticationFailures++; } - _logger.LogWarning( + logger.LogWarning( "MCP Authentication FAILURE | IP: {IpAddress} | Reason: {ErrorMessage}", auditEvent.IpAddress, auditEvent.ErrorMessage); @@ -134,7 +127,7 @@ public class McpSecurityAuditLogger : IMcpSecurityAuditLogger _statistics.LastCrossTenantAttempt = DateTime.UtcNow; } - _logger.LogCritical( + logger.LogCritical( "SECURITY ALERT: Cross-Tenant Access Attempt! | Attacker Tenant: {TenantId} | Target Tenant: {TargetTenantId} | " + "User: {UserId} | Resource: {ResourceType}/{ResourceId} | IP: {IpAddress}", auditEvent.TenantId, @@ -160,7 +153,7 @@ public class McpSecurityAuditLogger : IMcpSecurityAuditLogger _statistics.AuthorizationFailures++; } - _logger.LogWarning( + logger.LogWarning( "MCP Authorization FAILURE | Tenant: {TenantId} | User: {UserId} | Operation: {Operation} | " + "Resource: {ResourceType}/{ResourceId} | Reason: {ErrorMessage}", auditEvent.TenantId, diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/BackgroundServices/PendingChangeExpirationBackgroundService.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/BackgroundServices/PendingChangeExpirationBackgroundService.cs index 55f8432..4c77c37 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/BackgroundServices/PendingChangeExpirationBackgroundService.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/BackgroundServices/PendingChangeExpirationBackgroundService.cs @@ -9,20 +9,15 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.BackgroundServices; /// Background service to periodically expire old PendingChanges /// Runs every 5 minutes and marks expired changes /// -public class PendingChangeExpirationBackgroundService : BackgroundService +public class PendingChangeExpirationBackgroundService( + IServiceProvider serviceProvider, + ILogger logger) + : BackgroundService { - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly TimeSpan _interval = TimeSpan.FromMinutes(5); - public PendingChangeExpirationBackgroundService( - IServiceProvider serviceProvider, - ILogger logger) - { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("PendingChange Expiration Background Service started"); diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Hubs/McpNotificationHub.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Hubs/McpNotificationHub.cs index 3901163..65c2f9d 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Hubs/McpNotificationHub.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Hubs/McpNotificationHub.cs @@ -9,14 +9,9 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Hubs; /// Supports notifying AI agents and users about PendingChange status updates /// [Authorize] -public class McpNotificationHub : Hub +public class McpNotificationHub(ILogger logger) : Hub { - private readonly ILogger _logger; - - public McpNotificationHub(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public override async Task OnConnectedAsync() { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpApiKeyAuthenticationMiddleware.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpApiKeyAuthenticationMiddleware.cs index 92d6eef..9284a7b 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpApiKeyAuthenticationMiddleware.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpApiKeyAuthenticationMiddleware.cs @@ -9,25 +9,16 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Middleware; /// Middleware for authenticating MCP requests using API Keys /// Only applies to /mcp endpoints /// -public class McpApiKeyAuthenticationMiddleware +public class McpApiKeyAuthenticationMiddleware( + RequestDelegate next, + ILogger logger) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public McpApiKeyAuthenticationMiddleware( - RequestDelegate next, - ILogger logger) - { - _next = next; - _logger = logger; - } - public async Task InvokeAsync(HttpContext context, IMcpApiKeyService apiKeyService) { // Only apply to /mcp endpoints if (!context.Request.Path.StartsWithSegments("/mcp")) { - await _next(context); + await next(context); return; } @@ -35,7 +26,7 @@ public class McpApiKeyAuthenticationMiddleware var apiKey = ExtractApiKey(context.Request.Headers); if (string.IsNullOrEmpty(apiKey)) { - _logger.LogWarning("MCP request rejected - Missing API Key"); + logger.LogWarning("MCP request rejected - Missing API Key"); await WriteUnauthorizedResponse(context, "Missing API Key. Please provide Authorization: Bearer header."); return; } @@ -47,7 +38,7 @@ public class McpApiKeyAuthenticationMiddleware var validationResult = await apiKeyService.ValidateAsync(apiKey, ipAddress, context.RequestAborted); if (!validationResult.IsValid) { - _logger.LogWarning("MCP request rejected - Invalid API Key: {ErrorMessage}", validationResult.ErrorMessage); + logger.LogWarning("MCP request rejected - Invalid API Key: {ErrorMessage}", validationResult.ErrorMessage); await WriteUnauthorizedResponse(context, validationResult.ErrorMessage ?? "Invalid API Key"); return; } @@ -59,10 +50,10 @@ public class McpApiKeyAuthenticationMiddleware context.Items["McpUserId"] = validationResult.UserId; context.Items["McpPermissions"] = validationResult.Permissions; - _logger.LogDebug("MCP request authenticated - ApiKey: {ApiKeyId}, Tenant: {TenantId}, User: {UserId}", + logger.LogDebug("MCP request authenticated - ApiKey: {ApiKeyId}, Tenant: {TenantId}, User: {UserId}", validationResult.ApiKeyId, validationResult.TenantId, validationResult.UserId); - await _next(context); + await next(context); } /// diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpCorrelationIdMiddleware.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpCorrelationIdMiddleware.cs index 32cb4a5..638a24e 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpCorrelationIdMiddleware.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpCorrelationIdMiddleware.cs @@ -10,16 +10,10 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Middleware; /// - Response headers (for client-side tracking) /// - Serilog LogContext (for structured logging) /// -public class McpCorrelationIdMiddleware +public class McpCorrelationIdMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; private const string CorrelationIdHeaderName = "X-Correlation-Id"; - public McpCorrelationIdMiddleware(RequestDelegate next) - { - _next = next; - } - public async Task InvokeAsync(HttpContext context) { // Try to get correlation ID from request header, otherwise generate new one @@ -41,7 +35,7 @@ public class McpCorrelationIdMiddleware // Add to Serilog LogContext so it appears in all log entries for this request using (LogContext.PushProperty("CorrelationId", correlationId)) { - await _next(context); + await next(context); } } diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpExceptionHandlerMiddleware.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpExceptionHandlerMiddleware.cs index 6e09fc5..3f2b333 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpExceptionHandlerMiddleware.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpExceptionHandlerMiddleware.cs @@ -10,24 +10,15 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Middleware; /// Global exception handler middleware for MCP requests /// Catches all unhandled exceptions and converts them to JSON-RPC error responses /// -public class McpExceptionHandlerMiddleware +public class McpExceptionHandlerMiddleware( + RequestDelegate next, + ILogger logger) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public McpExceptionHandlerMiddleware( - RequestDelegate next, - ILogger logger) - { - _next = next; - _logger = logger; - } - public async Task InvokeAsync(HttpContext context) { try { - await _next(context); + await next(context); } catch (McpException mcpEx) { @@ -49,7 +40,7 @@ public class McpExceptionHandlerMiddleware var apiKeyId = context.Items["ApiKeyId"]?.ToString(); // Log the error with structured data - _logger.LogError(mcpEx, + logger.LogError(mcpEx, "MCP Error: {ErrorCode} - {Message} | CorrelationId: {CorrelationId} | TenantId: {TenantId} | ApiKeyId: {ApiKeyId}", mcpEx.ErrorCode, mcpEx.Message, correlationId, tenantId, apiKeyId); @@ -84,7 +75,7 @@ public class McpExceptionHandlerMiddleware var apiKeyId = context.Items["ApiKeyId"]?.ToString(); // Log the full exception with stack trace - _logger.LogError(ex, + logger.LogError(ex, "Unexpected error in MCP Server | CorrelationId: {CorrelationId} | TenantId: {TenantId} | ApiKeyId: {ApiKeyId}", correlationId, tenantId, apiKeyId); diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpLoggingMiddleware.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpLoggingMiddleware.cs index a5ea8bf..ec403cf 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpLoggingMiddleware.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpLoggingMiddleware.cs @@ -10,30 +10,21 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Middleware; /// Middleware that logs all MCP requests and responses /// Includes performance timing and sensitive data sanitization /// -public class McpLoggingMiddleware +public class McpLoggingMiddleware( + RequestDelegate next, + ILogger logger) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - // Patterns for sanitizing sensitive data private static readonly Regex ApiKeyHashPattern = new(@"""keyHash"":\s*""[^""]+""", RegexOptions.Compiled); private static readonly Regex ApiKeyPattern = new(@"""apiKey"":\s*""[^""]+""", RegexOptions.Compiled); private static readonly Regex PasswordPattern = new(@"""password"":\s*""[^""]+""", RegexOptions.Compiled); - public McpLoggingMiddleware( - RequestDelegate next, - ILogger logger) - { - _next = next; - _logger = logger; - } - public async Task InvokeAsync(HttpContext context) { // Only log MCP requests (POST to /mcp endpoint) if (!IsMcpRequest(context)) { - await _next(context); + await next(context); return; } @@ -54,7 +45,7 @@ public class McpLoggingMiddleware try { // Execute the rest of the pipeline - await _next(context); + await next(context); stopwatch.Stop(); @@ -96,7 +87,7 @@ public class McpLoggingMiddleware // Sanitize sensitive data before logging var sanitizedBody = SanitizeSensitiveData(bodyText); - _logger.LogDebug( + logger.LogDebug( "MCP Request | Method: {Method} | Path: {Path} | CorrelationId: {CorrelationId} | " + "TenantId: {TenantId} | ApiKeyId: {ApiKeyId} | UserId: {UserId}\nBody: {Body}", context.Request.Method, @@ -123,7 +114,7 @@ public class McpLoggingMiddleware var statusCode = context.Response.StatusCode; var logLevel = statusCode >= 400 ? LogLevel.Error : LogLevel.Debug; - _logger.Log(logLevel, + logger.Log(logLevel, "MCP Response | StatusCode: {StatusCode} | CorrelationId: {CorrelationId} | " + "Duration: {Duration}ms\nBody: {Body}", statusCode, @@ -134,7 +125,7 @@ public class McpLoggingMiddleware // Also log performance metrics if (elapsedMs > 1000) // Log slow requests (> 1 second) { - _logger.LogWarning( + logger.LogWarning( "Slow MCP Request | CorrelationId: {CorrelationId} | Duration: {Duration}ms", correlationId, elapsedMs); diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpMiddleware.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpMiddleware.cs index eea1428..b4d912b 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpMiddleware.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Middleware/McpMiddleware.cs @@ -9,27 +9,18 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Middleware; /// /// Middleware for handling MCP JSON-RPC 2.0 requests /// -public class McpMiddleware +public class McpMiddleware(RequestDelegate next, ILogger logger) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public McpMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } - public async Task InvokeAsync(HttpContext context, IMcpProtocolHandler protocolHandler) { // Only handle POST requests to /mcp endpoint if (context.Request.Method != "POST" || !context.Request.Path.StartsWithSegments("/mcp")) { - await _next(context); + await next(context); return; } - _logger.LogDebug("MCP request received from {RemoteIp}", context.Connection.RemoteIpAddress); + logger.LogDebug("MCP request received from {RemoteIp}", context.Connection.RemoteIpAddress); JsonRpcResponse? response = null; JsonRpcRequest? request = null; @@ -40,7 +31,7 @@ public class McpMiddleware using var reader = new StreamReader(context.Request.Body); var requestBody = await reader.ReadToEndAsync(); - _logger.LogTrace("MCP request body: {RequestBody}", requestBody); + logger.LogTrace("MCP request body: {RequestBody}", requestBody); // Parse JSON-RPC request try @@ -53,7 +44,7 @@ public class McpMiddleware } catch (JsonException ex) { - _logger.LogWarning(ex, "Failed to parse JSON-RPC request"); + logger.LogWarning(ex, "Failed to parse JSON-RPC request"); response = JsonRpcResponse.ParseError(ex.Message); } @@ -75,7 +66,7 @@ public class McpMiddleware DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); - _logger.LogTrace("MCP response: {ResponseJson}", responseJson); + logger.LogTrace("MCP response: {ResponseJson}", responseJson); await context.Response.WriteAsync(responseJson); } @@ -83,12 +74,12 @@ public class McpMiddleware { // For notifications, return 204 No Content context.Response.StatusCode = 204; - _logger.LogDebug("Notification processed, no response sent"); + logger.LogDebug("Notification processed, no response sent"); } } catch (Exception ex) { - _logger.LogError(ex, "Unhandled exception in MCP middleware"); + logger.LogError(ex, "Unhandled exception in MCP middleware"); // Send internal error response (id is null because we don't know the request id) response = JsonRpcResponse.InternalError("Unhandled server error", null); diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/McpDbContext.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/McpDbContext.cs index 441374a..804ee13 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/McpDbContext.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/McpDbContext.cs @@ -7,12 +7,8 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Persistence; /// /// DbContext for MCP module /// -public class McpDbContext : DbContext +public class McpDbContext(DbContextOptions options) : DbContext(options) { - public McpDbContext(DbContextOptions options) : base(options) - { - } - public DbSet ApiKeys => Set(); public DbSet PendingChanges => Set(); public DbSet TaskLocks => Set(); diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/McpApiKeyRepository.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/McpApiKeyRepository.cs index 2ff4168..6b5d054 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/McpApiKeyRepository.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/McpApiKeyRepository.cs @@ -7,14 +7,9 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Persistence.Repositories; /// /// Repository implementation for MCP API Keys /// -public class McpApiKeyRepository : IMcpApiKeyRepository +public class McpApiKeyRepository(McpDbContext context) : IMcpApiKeyRepository { - private readonly McpDbContext _context; - - public McpApiKeyRepository(McpDbContext context) - { - _context = context ?? throw new ArgumentNullException(nameof(context)); - } + private readonly McpDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/PendingChangeRepository.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/PendingChangeRepository.cs index ca07205..edc2b1a 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/PendingChangeRepository.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/PendingChangeRepository.cs @@ -8,14 +8,9 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Persistence.Repositories; /// /// Repository implementation for PendingChange aggregate /// -public sealed class PendingChangeRepository : IPendingChangeRepository +public sealed class PendingChangeRepository(McpDbContext context) : IPendingChangeRepository { - private readonly McpDbContext _context; - - public PendingChangeRepository(McpDbContext context) - { - _context = context ?? throw new ArgumentNullException(nameof(context)); - } + private readonly McpDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/TaskLockRepository.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/TaskLockRepository.cs index ebed495..99ebe18 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/TaskLockRepository.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Persistence/Repositories/TaskLockRepository.cs @@ -8,14 +8,9 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Persistence.Repositories; /// /// Repository implementation for TaskLock aggregate /// -public sealed class TaskLockRepository : ITaskLockRepository +public sealed class TaskLockRepository(McpDbContext context) : ITaskLockRepository { - private readonly McpDbContext _context; - - public TaskLockRepository(McpDbContext context) - { - _context = context ?? throw new ArgumentNullException(nameof(context)); - } + private readonly McpDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Reporting/MultiTenantSecurityReport.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Reporting/MultiTenantSecurityReport.cs index c174ec5..96b574e 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Reporting/MultiTenantSecurityReport.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Reporting/MultiTenantSecurityReport.cs @@ -79,19 +79,11 @@ public class SecurityScore /// /// Implementation of multi-tenant security report generator /// -public class MultiTenantSecurityReportGenerator : IMultiTenantSecurityReportGenerator +public class MultiTenantSecurityReportGenerator( + IMcpSecurityAuditLogger? auditLogger = null, + ITenantContextValidator? tenantValidator = null) + : IMultiTenantSecurityReportGenerator { - private readonly IMcpSecurityAuditLogger? _auditLogger; - private readonly ITenantContextValidator? _tenantValidator; - - public MultiTenantSecurityReportGenerator( - IMcpSecurityAuditLogger? auditLogger = null, - ITenantContextValidator? tenantValidator = null) - { - _auditLogger = auditLogger; - _tenantValidator = tenantValidator; - } - /// /// Generate comprehensive security report /// @@ -100,15 +92,15 @@ public class MultiTenantSecurityReportGenerator : IMultiTenantSecurityReportGene var report = new MultiTenantSecurityReport(); // Gather audit statistics - if (_auditLogger != null) + if (auditLogger != null) { - report.AuditStatistics = _auditLogger.GetAuditStatistics(); + report.AuditStatistics = auditLogger.GetAuditStatistics(); } // Gather validation statistics - if (_tenantValidator != null) + if (tenantValidator != null) { - report.ValidationStatistics = _tenantValidator.GetValidationStats(); + report.ValidationStatistics = tenantValidator.GetValidationStats(); } // Perform security checks @@ -276,7 +268,7 @@ public class MultiTenantSecurityReportGenerator : IMultiTenantSecurityReportGene GlobalQueryFiltersEnabled = true, // Assumed (would need EF Core inspection) ApiKeyTenantBindingEnabled = true, // Verified by API Key entity CrossTenantAccessBlocked = true, // Verified by tests - AuditLoggingEnabled = _auditLogger != null + AuditLoggingEnabled = auditLogger != null }; results.TotalChecks = 5; diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Services/McpNotificationService.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Services/McpNotificationService.cs index 0b7a71f..1f9f6d5 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Services/McpNotificationService.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Services/McpNotificationService.cs @@ -9,18 +9,13 @@ namespace ColaFlow.Modules.Mcp.Infrastructure.Services; /// /// Implementation of IMcpNotificationService using SignalR /// -public class McpNotificationService : IMcpNotificationService +public class McpNotificationService( + IHubContext hubContext, + ILogger logger) + : IMcpNotificationService { - private readonly IHubContext _hubContext; - private readonly ILogger _logger; - - public McpNotificationService( - IHubContext hubContext, - ILogger logger) - { - _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IHubContext _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task NotifyPendingChangeCreatedAsync( PendingChangeCreatedNotification notification, diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Validation/TenantContextValidator.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Validation/TenantContextValidator.cs index 6ac3081..d711408 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Validation/TenantContextValidator.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Validation/TenantContextValidator.cs @@ -42,16 +42,9 @@ public class TenantValidationStats /// Implementation of tenant context validator /// Uses EF Core Query Tags and SQL inspection to verify tenant filtering /// -public class TenantContextValidator : ITenantContextValidator +public class TenantContextValidator(ILogger logger) : ITenantContextValidator { - private readonly ILogger _logger; - private readonly TenantValidationStats _stats; - - public TenantContextValidator(ILogger logger) - { - _logger = logger; - _stats = new TenantValidationStats(); - } + private readonly TenantValidationStats _stats = new(); /// /// Validate that a query includes TenantId filter @@ -61,7 +54,7 @@ public class TenantContextValidator : ITenantContextValidator { if (string.IsNullOrWhiteSpace(queryString)) { - _logger.LogWarning("Empty query string provided for validation"); + logger.LogWarning("Empty query string provided for validation"); return false; } @@ -75,14 +68,14 @@ public class TenantContextValidator : ITenantContextValidator if (hasTenantFilter) { _stats.QueriesWithTenantFilter++; - _logger.LogDebug("Query validation PASSED - TenantId filter present"); + logger.LogDebug("Query validation PASSED - TenantId filter present"); return true; } else { _stats.QueriesWithoutTenantFilter++; _stats.ViolatingQueries.Add(queryString); - _logger.LogWarning("SECURITY WARNING: Query validation FAILED - No TenantId filter detected: {Query}", + logger.LogWarning("SECURITY WARNING: Query validation FAILED - No TenantId filter detected: {Query}", TruncateQuery(queryString)); return false; } @@ -96,7 +89,7 @@ public class TenantContextValidator : ITenantContextValidator { // Note: This would typically check HttpContext.Items["McpTenantId"] // For now, we'll log a placeholder - _logger.LogDebug("Tenant context validation requested"); + logger.LogDebug("Tenant context validation requested"); return true; // Placeholder - implement with actual context check } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicCreatedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicCreatedEventHandler.cs index d75041a..967f796 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicCreatedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicCreatedEventHandler.cs @@ -8,22 +8,14 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for EpicCreatedEvent - sends SignalR notification /// -public class EpicCreatedEventHandler : INotificationHandler +public class EpicCreatedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public EpicCreatedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(EpicCreatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling EpicCreatedEvent for epic {EpicId}", notification.EpicId); + logger.LogInformation("Handling EpicCreatedEvent for epic {EpicId}", notification.EpicId); var epicData = new { @@ -33,12 +25,12 @@ public class EpicCreatedEventHandler : INotificationHandler CreatedAt = DateTime.UtcNow }; - await _notificationService.NotifyEpicCreated( + await notificationService.NotifyEpicCreated( notification.TenantId.Value, notification.ProjectId.Value, notification.EpicId.Value, epicData); - _logger.LogInformation("SignalR notification sent for epic {EpicId}", notification.EpicId); + logger.LogInformation("SignalR notification sent for epic {EpicId}", notification.EpicId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicDeletedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicDeletedEventHandler.cs index 9dfb0ff..41d3a74 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicDeletedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicDeletedEventHandler.cs @@ -8,28 +8,20 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for EpicDeletedEvent - sends SignalR notification /// -public class EpicDeletedEventHandler : INotificationHandler +public class EpicDeletedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public EpicDeletedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(EpicDeletedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling EpicDeletedEvent for epic {EpicId}", notification.EpicId); + logger.LogInformation("Handling EpicDeletedEvent for epic {EpicId}", notification.EpicId); - await _notificationService.NotifyEpicDeleted( + await notificationService.NotifyEpicDeleted( notification.TenantId.Value, notification.ProjectId.Value, notification.EpicId.Value); - _logger.LogInformation("SignalR notification sent for epic {EpicId}", notification.EpicId); + logger.LogInformation("SignalR notification sent for epic {EpicId}", notification.EpicId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicUpdatedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicUpdatedEventHandler.cs index 6ea94d4..f2f729e 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicUpdatedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/EpicUpdatedEventHandler.cs @@ -8,22 +8,14 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for EpicUpdatedEvent - sends SignalR notification /// -public class EpicUpdatedEventHandler : INotificationHandler +public class EpicUpdatedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public EpicUpdatedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(EpicUpdatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling EpicUpdatedEvent for epic {EpicId}", notification.EpicId); + logger.LogInformation("Handling EpicUpdatedEvent for epic {EpicId}", notification.EpicId); var epicData = new { @@ -33,12 +25,12 @@ public class EpicUpdatedEventHandler : INotificationHandler UpdatedAt = DateTime.UtcNow }; - await _notificationService.NotifyEpicUpdated( + await notificationService.NotifyEpicUpdated( notification.TenantId.Value, notification.ProjectId.Value, notification.EpicId.Value, epicData); - _logger.LogInformation("SignalR notification sent for epic {EpicId}", notification.EpicId); + logger.LogInformation("SignalR notification sent for epic {EpicId}", notification.EpicId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/ProjectArchivedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/ProjectArchivedEventHandler.cs index 7e94a0d..915e8d7 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/ProjectArchivedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/ProjectArchivedEventHandler.cs @@ -9,38 +9,28 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for ProjectArchivedEvent - sends SignalR notification /// -public class ProjectArchivedEventHandler : INotificationHandler +public class ProjectArchivedEventHandler( + IProjectNotificationService notificationService, + IProjectRepository projectRepository, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly IProjectRepository _projectRepository; - private readonly ILogger _logger; - - public ProjectArchivedEventHandler( - IProjectNotificationService notificationService, - IProjectRepository projectRepository, - ILogger logger) - { - _notificationService = notificationService; - _projectRepository = projectRepository; - _logger = logger; - } - public async Task Handle(ProjectArchivedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling ProjectArchivedEvent for project {ProjectId}", notification.ProjectId); + logger.LogInformation("Handling ProjectArchivedEvent for project {ProjectId}", notification.ProjectId); // Get full project to obtain TenantId - var project = await _projectRepository.GetByIdAsync(notification.ProjectId, cancellationToken); + var project = await projectRepository.GetByIdAsync(notification.ProjectId, cancellationToken); if (project == null) { - _logger.LogWarning("Project {ProjectId} not found for archive notification", notification.ProjectId); + logger.LogWarning("Project {ProjectId} not found for archive notification", notification.ProjectId); return; } - await _notificationService.NotifyProjectArchived( + await notificationService.NotifyProjectArchived( project.TenantId.Value, notification.ProjectId.Value); - _logger.LogInformation("SignalR notification sent for archived project {ProjectId}", notification.ProjectId); + logger.LogInformation("SignalR notification sent for archived project {ProjectId}", notification.ProjectId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/ProjectCreatedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/ProjectCreatedEventHandler.cs index b8fe147..186485c 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/ProjectCreatedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/ProjectCreatedEventHandler.cs @@ -8,22 +8,14 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for ProjectCreatedEvent - sends SignalR notification /// -public class ProjectCreatedEventHandler : INotificationHandler +public class ProjectCreatedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public ProjectCreatedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(ProjectCreatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling ProjectCreatedEvent for project {ProjectId}", notification.ProjectId); + logger.LogInformation("Handling ProjectCreatedEvent for project {ProjectId}", notification.ProjectId); var projectData = new { @@ -33,11 +25,11 @@ public class ProjectCreatedEventHandler : INotificationHandler /// Handler for ProjectUpdatedEvent - sends SignalR notification /// -public class ProjectUpdatedEventHandler : INotificationHandler +public class ProjectUpdatedEventHandler( + IProjectNotificationService notificationService, + IProjectRepository projectRepository, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly IProjectRepository _projectRepository; - private readonly ILogger _logger; - - public ProjectUpdatedEventHandler( - IProjectNotificationService notificationService, - IProjectRepository projectRepository, - ILogger logger) - { - _notificationService = notificationService; - _projectRepository = projectRepository; - _logger = logger; - } - public async Task Handle(ProjectUpdatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling ProjectUpdatedEvent for project {ProjectId}", notification.ProjectId); + logger.LogInformation("Handling ProjectUpdatedEvent for project {ProjectId}", notification.ProjectId); // Get full project to obtain TenantId - var project = await _projectRepository.GetByIdAsync(notification.ProjectId, cancellationToken); + var project = await projectRepository.GetByIdAsync(notification.ProjectId, cancellationToken); if (project == null) { - _logger.LogWarning("Project {ProjectId} not found for update notification", notification.ProjectId); + logger.LogWarning("Project {ProjectId} not found for update notification", notification.ProjectId); return; } @@ -45,11 +35,11 @@ public class ProjectUpdatedEventHandler : INotificationHandler /// Handler for StoryCreatedEvent - sends SignalR notification /// -public class StoryCreatedEventHandler : INotificationHandler +public class StoryCreatedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public StoryCreatedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(StoryCreatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling StoryCreatedEvent for story {StoryId}", notification.StoryId); + logger.LogInformation("Handling StoryCreatedEvent for story {StoryId}", notification.StoryId); var storyData = new { @@ -34,13 +26,13 @@ public class StoryCreatedEventHandler : INotificationHandler CreatedAt = DateTime.UtcNow }; - await _notificationService.NotifyStoryCreated( + await notificationService.NotifyStoryCreated( notification.TenantId.Value, notification.ProjectId.Value, notification.EpicId.Value, notification.StoryId.Value, storyData); - _logger.LogInformation("SignalR notification sent for story {StoryId}", notification.StoryId); + logger.LogInformation("SignalR notification sent for story {StoryId}", notification.StoryId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/StoryDeletedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/StoryDeletedEventHandler.cs index 8130b3d..d9cb4d8 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/StoryDeletedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/StoryDeletedEventHandler.cs @@ -8,29 +8,21 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for StoryDeletedEvent - sends SignalR notification /// -public class StoryDeletedEventHandler : INotificationHandler +public class StoryDeletedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public StoryDeletedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(StoryDeletedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling StoryDeletedEvent for story {StoryId}", notification.StoryId); + logger.LogInformation("Handling StoryDeletedEvent for story {StoryId}", notification.StoryId); - await _notificationService.NotifyStoryDeleted( + await notificationService.NotifyStoryDeleted( notification.TenantId.Value, notification.ProjectId.Value, notification.EpicId.Value, notification.StoryId.Value); - _logger.LogInformation("SignalR notification sent for story {StoryId}", notification.StoryId); + logger.LogInformation("SignalR notification sent for story {StoryId}", notification.StoryId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/StoryUpdatedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/StoryUpdatedEventHandler.cs index f2317c6..7e5f626 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/StoryUpdatedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/StoryUpdatedEventHandler.cs @@ -8,22 +8,14 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for StoryUpdatedEvent - sends SignalR notification /// -public class StoryUpdatedEventHandler : INotificationHandler +public class StoryUpdatedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public StoryUpdatedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(StoryUpdatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling StoryUpdatedEvent for story {StoryId}", notification.StoryId); + logger.LogInformation("Handling StoryUpdatedEvent for story {StoryId}", notification.StoryId); var storyData = new { @@ -34,13 +26,13 @@ public class StoryUpdatedEventHandler : INotificationHandler UpdatedAt = DateTime.UtcNow }; - await _notificationService.NotifyStoryUpdated( + await notificationService.NotifyStoryUpdated( notification.TenantId.Value, notification.ProjectId.Value, notification.EpicId.Value, notification.StoryId.Value, storyData); - _logger.LogInformation("SignalR notification sent for story {StoryId}", notification.StoryId); + logger.LogInformation("SignalR notification sent for story {StoryId}", notification.StoryId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskAssignedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskAssignedEventHandler.cs index f670175..926369f 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskAssignedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskAssignedEventHandler.cs @@ -8,30 +8,22 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for TaskAssignedEvent - sends SignalR notification /// -public class TaskAssignedEventHandler : INotificationHandler +public class TaskAssignedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public TaskAssignedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(TaskAssignedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling TaskAssignedEvent for task {TaskId}", notification.TaskId); + logger.LogInformation("Handling TaskAssignedEvent for task {TaskId}", notification.TaskId); - await _notificationService.NotifyTaskAssigned( + await notificationService.NotifyTaskAssigned( notification.TenantId.Value, notification.ProjectId.Value, notification.TaskId.Value, notification.AssigneeId.Value); - _logger.LogInformation("SignalR notification sent for task {TaskId} assigned to {AssigneeId}", + logger.LogInformation("SignalR notification sent for task {TaskId} assigned to {AssigneeId}", notification.TaskId, notification.AssigneeId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskCreatedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskCreatedEventHandler.cs index 4694bee..e290549 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskCreatedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskCreatedEventHandler.cs @@ -8,22 +8,14 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for TaskCreatedEvent - sends SignalR notification /// -public class TaskCreatedEventHandler : INotificationHandler +public class TaskCreatedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public TaskCreatedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(TaskCreatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling TaskCreatedEvent for task {TaskId}", notification.TaskId); + logger.LogInformation("Handling TaskCreatedEvent for task {TaskId}", notification.TaskId); var taskData = new { @@ -34,13 +26,13 @@ public class TaskCreatedEventHandler : INotificationHandler CreatedAt = DateTime.UtcNow }; - await _notificationService.NotifyTaskCreated( + await notificationService.NotifyTaskCreated( notification.TenantId.Value, notification.ProjectId.Value, notification.StoryId.Value, notification.TaskId.Value, taskData); - _logger.LogInformation("SignalR notification sent for task {TaskId}", notification.TaskId); + logger.LogInformation("SignalR notification sent for task {TaskId}", notification.TaskId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskDeletedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskDeletedEventHandler.cs index 0a4be8d..1348a1b 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskDeletedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskDeletedEventHandler.cs @@ -8,29 +8,21 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for TaskDeletedEvent - sends SignalR notification /// -public class TaskDeletedEventHandler : INotificationHandler +public class TaskDeletedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public TaskDeletedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(TaskDeletedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling TaskDeletedEvent for task {TaskId}", notification.TaskId); + logger.LogInformation("Handling TaskDeletedEvent for task {TaskId}", notification.TaskId); - await _notificationService.NotifyTaskDeleted( + await notificationService.NotifyTaskDeleted( notification.TenantId.Value, notification.ProjectId.Value, notification.StoryId.Value, notification.TaskId.Value); - _logger.LogInformation("SignalR notification sent for task {TaskId}", notification.TaskId); + logger.LogInformation("SignalR notification sent for task {TaskId}", notification.TaskId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskUpdatedEventHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskUpdatedEventHandler.cs index 4b05cec..caa9b7d 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskUpdatedEventHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/EventHandlers/TaskUpdatedEventHandler.cs @@ -8,22 +8,14 @@ namespace ColaFlow.Modules.ProjectManagement.Application.EventHandlers; /// /// Handler for TaskUpdatedEvent - sends SignalR notification /// -public class TaskUpdatedEventHandler : INotificationHandler +public class TaskUpdatedEventHandler( + IProjectNotificationService notificationService, + ILogger logger) + : INotificationHandler { - private readonly IProjectNotificationService _notificationService; - private readonly ILogger _logger; - - public TaskUpdatedEventHandler( - IProjectNotificationService notificationService, - ILogger logger) - { - _notificationService = notificationService; - _logger = logger; - } - public async Task Handle(TaskUpdatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Handling TaskUpdatedEvent for task {TaskId}", notification.TaskId); + logger.LogInformation("Handling TaskUpdatedEvent for task {TaskId}", notification.TaskId); var taskData = new { @@ -34,13 +26,13 @@ public class TaskUpdatedEventHandler : INotificationHandler UpdatedAt = DateTime.UtcNow }; - await _notificationService.NotifyTaskUpdated( + await notificationService.NotifyTaskUpdated( notification.TenantId.Value, notification.ProjectId.Value, notification.StoryId.Value, notification.TaskId.Value, taskData); - _logger.LogInformation("SignalR notification sent for task {TaskId}", notification.TaskId); + logger.LogInformation("SignalR notification sent for task {TaskId}", notification.TaskId); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetAuditLogById/GetAuditLogByIdQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetAuditLogById/GetAuditLogByIdQueryHandler.cs index 0bc2130..0f3e9c8 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetAuditLogById/GetAuditLogByIdQueryHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetAuditLogById/GetAuditLogByIdQueryHandler.cs @@ -7,18 +7,12 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetAu /// Handler for GetAuditLogByIdQuery /// Retrieves a single audit log entry by its unique identifier /// -public class GetAuditLogByIdQueryHandler : IRequestHandler +public class GetAuditLogByIdQueryHandler(IAuditLogRepository auditLogRepository) + : IRequestHandler { - private readonly IAuditLogRepository _auditLogRepository; - - public GetAuditLogByIdQueryHandler(IAuditLogRepository auditLogRepository) - { - _auditLogRepository = auditLogRepository; - } - public async Task Handle(GetAuditLogByIdQuery request, CancellationToken cancellationToken) { - var auditLog = await _auditLogRepository.GetByIdAsync(request.AuditLogId, cancellationToken); + var auditLog = await auditLogRepository.GetByIdAsync(request.AuditLogId, cancellationToken); if (auditLog == null) return null; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetAuditLogsByEntity/GetAuditLogsByEntityQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetAuditLogsByEntity/GetAuditLogsByEntityQueryHandler.cs index c7e6994..4f58190 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetAuditLogsByEntity/GetAuditLogsByEntityQueryHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetAuditLogsByEntity/GetAuditLogsByEntityQueryHandler.cs @@ -8,18 +8,12 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetAu /// Retrieves all audit log entries for a specific entity (e.g., all changes to a Project) /// Results are automatically filtered by tenant via global query filter /// -public class GetAuditLogsByEntityQueryHandler : IRequestHandler> +public class GetAuditLogsByEntityQueryHandler(IAuditLogRepository auditLogRepository) + : IRequestHandler> { - private readonly IAuditLogRepository _auditLogRepository; - - public GetAuditLogsByEntityQueryHandler(IAuditLogRepository auditLogRepository) - { - _auditLogRepository = auditLogRepository; - } - public async Task> Handle(GetAuditLogsByEntityQuery request, CancellationToken cancellationToken) { - var auditLogs = await _auditLogRepository.GetByEntityAsync( + var auditLogs = await auditLogRepository.GetByEntityAsync( request.EntityType, request.EntityId, cancellationToken); diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetRecentAuditLogs/GetRecentAuditLogsQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetRecentAuditLogs/GetRecentAuditLogsQueryHandler.cs index fd08fea..6ec5c1e 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetRecentAuditLogs/GetRecentAuditLogsQueryHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/AuditLogs/GetRecentAuditLogs/GetRecentAuditLogsQueryHandler.cs @@ -8,18 +8,12 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetRe /// Retrieves the most recent audit log entries across all entities /// Results are automatically filtered by tenant via global query filter /// -public class GetRecentAuditLogsQueryHandler : IRequestHandler> +public class GetRecentAuditLogsQueryHandler(IAuditLogRepository auditLogRepository) + : IRequestHandler> { - private readonly IAuditLogRepository _auditLogRepository; - - public GetRecentAuditLogsQueryHandler(IAuditLogRepository auditLogRepository) - { - _auditLogRepository = auditLogRepository; - } - public async Task> Handle(GetRecentAuditLogsQuery request, CancellationToken cancellationToken) { - var auditLogs = await _auditLogRepository.GetRecentAsync(request.Count, cancellationToken); + var auditLogs = await auditLogRepository.GetRecentAsync(request.Count, cancellationToken); return auditLogs .Select(a => new AuditLogDto( diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs index a073fb4..3515b69 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs @@ -11,21 +11,15 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintBurndo /// Handler for GetSprintBurndownQuery /// Calculates ideal and actual burndown data for sprint progress visualization /// -public sealed class GetSprintBurndownQueryHandler : IRequestHandler +public sealed class GetSprintBurndownQueryHandler( + IProjectRepository projectRepository, + IApplicationDbContext context, + ILogger logger) + : IRequestHandler { - private readonly IProjectRepository _projectRepository; - private readonly IApplicationDbContext _context; - private readonly ILogger _logger; - - public GetSprintBurndownQueryHandler( - IProjectRepository projectRepository, - IApplicationDbContext context, - ILogger logger) - { - _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); - _context = context ?? throw new ArgumentNullException(nameof(context)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task Handle(GetSprintBurndownQuery request, CancellationToken cancellationToken) { diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs index 26e8a20..8119883 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs @@ -14,15 +14,8 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Intercep /// Tracks Create/Update/Delete operations with tenant and user context /// Phase 2: Field-level change detection with JSON diff /// -public class AuditInterceptor : SaveChangesInterceptor +public class AuditInterceptor(ITenantContext tenantContext) : SaveChangesInterceptor { - private readonly ITenantContext _tenantContext; - - public AuditInterceptor(ITenantContext tenantContext) - { - _tenantContext = tenantContext; - } - public override InterceptionResult SavingChanges( DbContextEventData eventData, InterceptionResult result) @@ -52,8 +45,8 @@ public class AuditInterceptor : SaveChangesInterceptor private void AuditChanges(DbContext context) { // Remove try-catch temporarily to see actual errors - var tenantId = TenantId.From(_tenantContext.GetCurrentTenantId()); - var userId = _tenantContext.GetCurrentUserId(); + var tenantId = TenantId.From(tenantContext.GetCurrentTenantId()); + var userId = tenantContext.GetCurrentUserId(); UserId? userIdVO = userId.HasValue ? UserId.From(userId.Value) : null; var entries = context.ChangeTracker.Entries() diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs index c80c172..08a5930 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs @@ -11,16 +11,9 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence; /// /// Project Management Module DbContext /// -public class PMDbContext : DbContext, IApplicationDbContext +public class PMDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) + : DbContext(options), IApplicationDbContext { - private readonly IHttpContextAccessor _httpContextAccessor; - - public PMDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) - : base(options) - { - _httpContextAccessor = httpContextAccessor; - } - public DbSet Projects => Set(); public DbSet Epics => Set(); public DbSet Stories => Set(); @@ -60,7 +53,7 @@ public class PMDbContext : DbContext, IApplicationDbContext private TenantId GetCurrentTenantId() { - var tenantIdClaim = _httpContextAccessor?.HttpContext?.User + var tenantIdClaim = httpContextAccessor?.HttpContext?.User .FindFirst("tenant_id")?.Value; if (Guid.TryParse(tenantIdClaim, out var tenantId) && tenantId != Guid.Empty) diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/AuditLogRepository.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/AuditLogRepository.cs index d42f468..1ae0b98 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/AuditLogRepository.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/AuditLogRepository.cs @@ -6,18 +6,11 @@ using Microsoft.EntityFrameworkCore; namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories; -public class AuditLogRepository : IAuditLogRepository +public class AuditLogRepository(PMDbContext context) : IAuditLogRepository { - private readonly PMDbContext _context; - - public AuditLogRepository(PMDbContext context) - { - _context = context; - } - public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { - return await _context.AuditLogs + return await context.AuditLogs .AsNoTracking() .FirstOrDefaultAsync(a => a.Id == id, cancellationToken); } @@ -27,7 +20,7 @@ public class AuditLogRepository : IAuditLogRepository Guid entityId, CancellationToken cancellationToken = default) { - return await _context.AuditLogs + return await context.AuditLogs .AsNoTracking() .Where(a => a.EntityType == entityType && a.EntityId == entityId) .OrderByDescending(a => a.Timestamp) @@ -41,7 +34,7 @@ public class AuditLogRepository : IAuditLogRepository CancellationToken cancellationToken = default) { var userIdVO = UserId.From(userId); - return await _context.AuditLogs + return await context.AuditLogs .AsNoTracking() .Where(a => a.UserId == userIdVO) .OrderByDescending(a => a.Timestamp) @@ -54,7 +47,7 @@ public class AuditLogRepository : IAuditLogRepository int count = 100, CancellationToken cancellationToken = default) { - return await _context.AuditLogs + return await context.AuditLogs .AsNoTracking() .OrderByDescending(a => a.Timestamp) .Take(count) @@ -63,12 +56,12 @@ public class AuditLogRepository : IAuditLogRepository public async Task AddAsync(AuditLog auditLog, CancellationToken cancellationToken = default) { - await _context.AuditLogs.AddAsync(auditLog, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); + await context.AuditLogs.AddAsync(auditLog, cancellationToken); + await context.SaveChangesAsync(cancellationToken); } public async Task GetCountAsync(CancellationToken cancellationToken = default) { - return await _context.AuditLogs.CountAsync(cancellationToken); + return await context.AuditLogs.CountAsync(cancellationToken); } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/ProjectPermissionService.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/ProjectPermissionService.cs index 8be51c6..7e32576 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/ProjectPermissionService.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/ProjectPermissionService.cs @@ -7,15 +7,8 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Services; /// /// Implementation of project permission checking service /// -public sealed class ProjectPermissionService : IProjectPermissionService +public sealed class ProjectPermissionService(PMDbContext dbContext) : IProjectPermissionService { - private readonly PMDbContext _dbContext; - - public ProjectPermissionService(PMDbContext dbContext) - { - _dbContext = dbContext; - } - /// /// Checks if a user has permission to access a project /// Currently checks if user is the project owner @@ -25,7 +18,7 @@ public sealed class ProjectPermissionService : IProjectPermissionService public async Task IsUserProjectMemberAsync(Guid userId, Guid projectId, CancellationToken cancellationToken = default) { // Query will automatically apply tenant filter from PMDbContext - var project = await _dbContext.Projects + var project = await dbContext.Projects .AsNoTracking() .FirstOrDefaultAsync(p => p.Id.Value == projectId, cancellationToken); diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/TenantContext.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/TenantContext.cs index 283d8ca..5958433 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/TenantContext.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/TenantContext.cs @@ -7,18 +7,11 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Services; /// /// Implementation of ITenantContext that retrieves tenant ID from JWT claims /// -public sealed class TenantContext : ITenantContext +public sealed class TenantContext(IHttpContextAccessor httpContextAccessor) : ITenantContext { - private readonly IHttpContextAccessor _httpContextAccessor; - - public TenantContext(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - public Guid GetCurrentTenantId() { - var httpContext = _httpContextAccessor.HttpContext; + var httpContext = httpContextAccessor.HttpContext; if (httpContext == null) throw new InvalidOperationException("HTTP context is not available"); @@ -33,7 +26,7 @@ public sealed class TenantContext : ITenantContext public Guid? GetCurrentUserId() { - var httpContext = _httpContextAccessor.HttpContext; + var httpContext = httpContextAccessor.HttpContext; if (httpContext == null) return null; diff --git a/colaflow-api/tests/ColaFlow.IntegrationTests/Mcp/McpMultiTenantIsolationTests.cs b/colaflow-api/tests/ColaFlow.IntegrationTests/Mcp/McpMultiTenantIsolationTests.cs index 37527c0..fece916 100644 --- a/colaflow-api/tests/ColaFlow.IntegrationTests/Mcp/McpMultiTenantIsolationTests.cs +++ b/colaflow-api/tests/ColaFlow.IntegrationTests/Mcp/McpMultiTenantIsolationTests.cs @@ -17,22 +17,16 @@ namespace ColaFlow.IntegrationTests.Mcp; /// 4. Verify ALL cross-tenant access returns 404 (NOT 403 - avoid info leakage) /// 5. Verify search queries NEVER return cross-tenant results /// -public class McpMultiTenantIsolationTests : IClassFixture +public class McpMultiTenantIsolationTests(MultiTenantTestFixture fixture) : IClassFixture { - private readonly MultiTenantTestFixture _fixture; - private readonly HttpClient _client; + private readonly MultiTenantTestFixture _fixture = fixture; + private readonly HttpClient _client = fixture.CreateClient(); // Test tenants private TenantTestData _tenantA = null!; private TenantTestData _tenantB = null!; private TenantTestData _tenantC = null!; - public McpMultiTenantIsolationTests(MultiTenantTestFixture fixture) - { - _fixture = fixture; - _client = fixture.CreateClient(); - } - #region Setup /// diff --git a/colaflow-api/tests/ColaFlow.IntegrationTests/Mcp/McpProtocolIntegrationTests.cs b/colaflow-api/tests/ColaFlow.IntegrationTests/Mcp/McpProtocolIntegrationTests.cs index 69a175a..25e7be1 100644 --- a/colaflow-api/tests/ColaFlow.IntegrationTests/Mcp/McpProtocolIntegrationTests.cs +++ b/colaflow-api/tests/ColaFlow.IntegrationTests/Mcp/McpProtocolIntegrationTests.cs @@ -12,14 +12,10 @@ namespace ColaFlow.IntegrationTests.Mcp; /// /// Integration tests for MCP Protocol endpoint /// -public class McpProtocolIntegrationTests : IClassFixture> +public class McpProtocolIntegrationTests(WebApplicationFactory factory) + : IClassFixture> { - private readonly HttpClient _client; - - public McpProtocolIntegrationTests(WebApplicationFactory factory) - { - _client = factory.CreateClient(); - } + private readonly HttpClient _client = factory.CreateClient(); [Fact] public async Task McpEndpoint_WithInitializeRequest_ReturnsSuccess()