Refactoring
This commit is contained in:
@@ -66,11 +66,13 @@ public static class ModuleExtensions
|
|||||||
services.AddScoped<ColaFlow.Modules.ProjectManagement.Application.Services.IProjectPermissionService,
|
services.AddScoped<ColaFlow.Modules.ProjectManagement.Application.Services.IProjectPermissionService,
|
||||||
ColaFlow.Modules.ProjectManagement.Infrastructure.Services.ProjectPermissionService>();
|
ColaFlow.Modules.ProjectManagement.Infrastructure.Services.ProjectPermissionService>();
|
||||||
|
|
||||||
// Register MediatR handlers from Application assembly (v13.x syntax)
|
// Register MediatR handlers from Application assemblies (v13.x syntax)
|
||||||
|
// Consolidate all module handler registrations here to avoid duplicate registrations
|
||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
{
|
{
|
||||||
cfg.LicenseKey = configuration["MediatR:LicenseKey"];
|
cfg.LicenseKey = configuration["MediatR:LicenseKey"];
|
||||||
cfg.RegisterServicesFromAssembly(typeof(CreateProjectCommand).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(CreateProjectCommand).Assembly);
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(ColaFlow.Modules.Mcp.Application.EventHandlers.PendingChangeApprovedEventHandler).Assembly);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register FluentValidation validators
|
// Register FluentValidation validators
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ namespace ColaFlow.Modules.Mcp.Application.Handlers;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for the 'resources/read' MCP method
|
/// Handler for the 'resources/read' MCP method
|
||||||
|
/// Uses scoped IMcpResource instances from DI to avoid DbContext disposal issues
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ResourcesReadMethodHandler(
|
public class ResourcesReadMethodHandler(
|
||||||
ILogger<ResourcesReadMethodHandler> logger,
|
ILogger<ResourcesReadMethodHandler> logger,
|
||||||
IMcpResourceRegistry resourceRegistry)
|
IMcpResourceRegistry resourceRegistry,
|
||||||
|
IEnumerable<IMcpResource> scopedResources)
|
||||||
: IMcpMethodHandler
|
: IMcpMethodHandler
|
||||||
{
|
{
|
||||||
public string MethodName => "resources/read";
|
public string MethodName => "resources/read";
|
||||||
@@ -32,13 +34,20 @@ public class ResourcesReadMethodHandler(
|
|||||||
|
|
||||||
logger.LogInformation("Reading resource: {Uri}", request.Uri);
|
logger.LogInformation("Reading resource: {Uri}", request.Uri);
|
||||||
|
|
||||||
// Find resource by URI
|
// Find resource descriptor from registry (for URI template matching)
|
||||||
var resource = resourceRegistry.GetResourceByUri(request.Uri);
|
var registryResource = resourceRegistry.GetResourceByUri(request.Uri);
|
||||||
if (resource == null)
|
if (registryResource == null)
|
||||||
{
|
{
|
||||||
throw new McpNotFoundException($"Resource not found: {request.Uri}");
|
throw new McpNotFoundException($"Resource not found: {request.Uri}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the scoped resource instance from DI (fresh DbContext)
|
||||||
|
var resource = scopedResources.FirstOrDefault(r => r.Uri == registryResource.Uri);
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
throw new McpNotFoundException($"Resource implementation not found: {registryResource.Uri}");
|
||||||
|
}
|
||||||
|
|
||||||
// Parse URI and extract parameters
|
// Parse URI and extract parameters
|
||||||
var resourceRequest = ParseResourceRequest(request.Uri, resource.Uri);
|
var resourceRequest = ParseResourceRequest(request.Uri, resource.Uri);
|
||||||
|
|
||||||
@@ -114,6 +123,7 @@ public class ResourcesReadMethodHandler(
|
|||||||
|
|
||||||
private class ResourceReadParams
|
private class ResourceReadParams
|
||||||
{
|
{
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("uri")]
|
||||||
public string Uri { get; set; } = string.Empty;
|
public string Uri { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ public static class McpServiceExtensions
|
|||||||
services.AddScoped<IMcpMethodHandler, ToolsListMethodHandler>();
|
services.AddScoped<IMcpMethodHandler, ToolsListMethodHandler>();
|
||||||
services.AddScoped<IMcpMethodHandler, ToolsCallMethodHandler>();
|
services.AddScoped<IMcpMethodHandler, ToolsCallMethodHandler>();
|
||||||
|
|
||||||
|
// Note: MediatR handlers for MCP are registered in ModuleExtensions.cs AddProjectManagementModule()
|
||||||
|
// They are included via RegisterServicesFromAssembly for the MCP Application assembly
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ public class McpApiKeyAuthenticationMiddleware(
|
|||||||
{
|
{
|
||||||
public async Task InvokeAsync(HttpContext context, IMcpApiKeyService apiKeyService)
|
public async Task InvokeAsync(HttpContext context, IMcpApiKeyService apiKeyService)
|
||||||
{
|
{
|
||||||
// Only apply to /mcp endpoints
|
// Only apply to /mcp-sdk endpoint (not /api/mcp/* which uses JWT auth)
|
||||||
if (!context.Request.Path.StartsWithSegments("/mcp"))
|
if (!context.Request.Path.StartsWithSegments("/mcp-sdk"))
|
||||||
{
|
{
|
||||||
await next(context);
|
await next(context);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ public class McpExceptionHandlerMiddleware(
|
|||||||
{
|
{
|
||||||
public async Task InvokeAsync(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
|
// Only handle exceptions for MCP endpoints
|
||||||
|
if (!context.Request.Path.StartsWithSegments("/mcp-sdk"))
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await next(context);
|
await next(context);
|
||||||
|
|||||||
@@ -64,11 +64,12 @@ public class McpLoggingMiddleware(
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if this is an MCP request
|
/// Checks if this is an MCP request
|
||||||
|
/// Only applies to /mcp-sdk endpoint (not /api/mcp/* which uses JWT auth)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool IsMcpRequest(HttpContext context)
|
private static bool IsMcpRequest(HttpContext context)
|
||||||
{
|
{
|
||||||
return context.Request.Method == "POST"
|
return context.Request.Method == "POST"
|
||||||
&& context.Request.Path.StartsWithSegments("/mcp");
|
&& context.Request.Path.StartsWithSegments("/mcp-sdk");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ public class McpMiddleware(RequestDelegate next, ILogger<McpMiddleware> logger)
|
|||||||
{
|
{
|
||||||
public async Task InvokeAsync(HttpContext context, IMcpProtocolHandler protocolHandler)
|
public async Task InvokeAsync(HttpContext context, IMcpProtocolHandler protocolHandler)
|
||||||
{
|
{
|
||||||
// Only handle POST requests to /mcp endpoint
|
// Only handle POST requests to /mcp-sdk endpoint (not /api/mcp/* which uses JWT auth)
|
||||||
if (context.Request.Method != "POST" || !context.Request.Path.StartsWithSegments("/mcp"))
|
if (context.Request.Method != "POST" || !context.Request.Path.StartsWithSegments("/mcp-sdk"))
|
||||||
{
|
{
|
||||||
await next(context);
|
await next(context);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ public interface ITenantContext
|
|||||||
/// <exception cref="UnauthorizedAccessException">Thrown when tenant context is not available</exception>
|
/// <exception cref="UnauthorizedAccessException">Thrown when tenant context is not available</exception>
|
||||||
Guid GetCurrentTenantId();
|
Guid GetCurrentTenantId();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get the current tenant ID without throwing exceptions
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current tenant ID or null if not available</returns>
|
||||||
|
Guid? TryGetCurrentTenantId();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current user ID from claims (optional - may be null for system operations)
|
/// Gets the current user ID from claims (optional - may be null for system operations)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||||
@@ -10,8 +9,9 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Project Management Module DbContext
|
/// Project Management Module DbContext
|
||||||
|
/// Uses ITenantContext for centralized tenant resolution (supports both JWT and MCP API Key authentication)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PMDbContext(DbContextOptions<PMDbContext> options, IHttpContextAccessor httpContextAccessor)
|
public class PMDbContext(DbContextOptions<PMDbContext> options, ITenantContext tenantContext)
|
||||||
: DbContext(options), IApplicationDbContext
|
: DbContext(options), IApplicationDbContext
|
||||||
{
|
{
|
||||||
public DbSet<Project> Projects => Set<Project>();
|
public DbSet<Project> Projects => Set<Project>();
|
||||||
@@ -53,16 +53,12 @@ public class PMDbContext(DbContextOptions<PMDbContext> options, IHttpContextAcce
|
|||||||
|
|
||||||
private TenantId GetCurrentTenantId()
|
private TenantId GetCurrentTenantId()
|
||||||
{
|
{
|
||||||
var tenantIdClaim = httpContextAccessor?.HttpContext?.User
|
// Use centralized tenant resolution from ITenantContext
|
||||||
.FindFirst("tenant_id")?.Value;
|
// TryGetCurrentTenantId handles both JWT claims and MCP API Key authentication
|
||||||
|
var tenantId = tenantContext.TryGetCurrentTenantId();
|
||||||
|
|
||||||
if (Guid.TryParse(tenantIdClaim, out var tenantId) && tenantId != Guid.Empty)
|
// Return dummy value for queries outside tenant context (e.g., migrations)
|
||||||
{
|
|
||||||
return TenantId.From(tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a dummy value for queries outside HTTP context (e.g., migrations)
|
|
||||||
// These will return no results due to the filter
|
// These will return no results due to the filter
|
||||||
return TenantId.From(Guid.Empty);
|
return TenantId.From(tenantId ?? Guid.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
|
|
||||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||||
|
|
||||||
@@ -19,16 +19,18 @@ public class PMDbContextFactory : IDesignTimeDbContextFactory<PMDbContext>
|
|||||||
var optionsBuilder = new DbContextOptionsBuilder<PMDbContext>();
|
var optionsBuilder = new DbContextOptionsBuilder<PMDbContext>();
|
||||||
optionsBuilder.UseNpgsql(connectionString, b => b.MigrationsAssembly("ColaFlow.Modules.ProjectManagement.Infrastructure"));
|
optionsBuilder.UseNpgsql(connectionString, b => b.MigrationsAssembly("ColaFlow.Modules.ProjectManagement.Infrastructure"));
|
||||||
|
|
||||||
// Create DbContext with a mock HttpContextAccessor (for migrations only)
|
// Create DbContext with a mock TenantContext (for migrations only)
|
||||||
return new PMDbContext(optionsBuilder.Options, new MockHttpContextAccessor());
|
return new PMDbContext(optionsBuilder.Options, new MockTenantContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mock HttpContextAccessor for design-time operations
|
/// Mock TenantContext for design-time operations
|
||||||
/// Returns null HttpContext which PMDbContext handles gracefully
|
/// Returns null/empty tenant ID which PMDbContext handles gracefully
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private class MockHttpContextAccessor : IHttpContextAccessor
|
private class MockTenantContext : ITenantContext
|
||||||
{
|
{
|
||||||
public HttpContext? HttpContext { get; set; } = null;
|
public Guid GetCurrentTenantId() => Guid.Empty;
|
||||||
|
public Guid? TryGetCurrentTenantId() => null;
|
||||||
|
public Guid? GetCurrentUserId() => null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,40 @@ using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
|||||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Services;
|
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of ITenantContext that retrieves tenant ID from JWT claims
|
/// Implementation of ITenantContext that retrieves tenant ID from JWT claims or MCP API Key context
|
||||||
|
/// Supports both JWT authentication (claims) and MCP API Key authentication (HttpContext.Items)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class TenantContext(IHttpContextAccessor httpContextAccessor) : ITenantContext
|
public sealed class TenantContext(IHttpContextAccessor httpContextAccessor) : ITenantContext
|
||||||
{
|
{
|
||||||
public Guid GetCurrentTenantId()
|
public Guid GetCurrentTenantId()
|
||||||
|
{
|
||||||
|
var tenantId = TryGetCurrentTenantId();
|
||||||
|
if (tenantId == null)
|
||||||
|
throw new UnauthorizedAccessException("Tenant ID not found in claims");
|
||||||
|
|
||||||
|
return tenantId.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid? TryGetCurrentTenantId()
|
||||||
{
|
{
|
||||||
var httpContext = httpContextAccessor.HttpContext;
|
var httpContext = httpContextAccessor.HttpContext;
|
||||||
if (httpContext == null)
|
if (httpContext == null)
|
||||||
throw new InvalidOperationException("HTTP context is not available");
|
return null;
|
||||||
|
|
||||||
|
// First, try to get from MCP API Key authentication (HttpContext.Items)
|
||||||
|
if (httpContext.Items.TryGetValue("McpTenantId", out var mcpTenantIdObj) && mcpTenantIdObj is Guid mcpTenantId)
|
||||||
|
{
|
||||||
|
return mcpTenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to JWT claims
|
||||||
var user = httpContext.User;
|
var user = httpContext.User;
|
||||||
var tenantClaim = user.FindFirst("tenant_id") ?? user.FindFirst("tenantId");
|
var tenantClaim = user.FindFirst("tenant_id") ?? user.FindFirst("tenantId");
|
||||||
|
|
||||||
if (tenantClaim == null || !Guid.TryParse(tenantClaim.Value, out var tenantId))
|
if (tenantClaim != null && Guid.TryParse(tenantClaim.Value, out var tenantId))
|
||||||
throw new UnauthorizedAccessException("Tenant ID not found in claims");
|
return tenantId;
|
||||||
|
|
||||||
return tenantId;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid? GetCurrentUserId()
|
public Guid? GetCurrentUserId()
|
||||||
@@ -30,6 +47,13 @@ public sealed class TenantContext(IHttpContextAccessor httpContextAccessor) : IT
|
|||||||
if (httpContext == null)
|
if (httpContext == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
// First, try to get from MCP API Key authentication (HttpContext.Items)
|
||||||
|
if (httpContext.Items.TryGetValue("McpUserId", out var mcpUserIdObj) && mcpUserIdObj is Guid mcpUserId)
|
||||||
|
{
|
||||||
|
return mcpUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to JWT claims
|
||||||
var user = httpContext.User;
|
var user = httpContext.User;
|
||||||
var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier)
|
var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier)
|
||||||
?? user.FindFirst("sub")
|
?? user.FindFirst("sub")
|
||||||
|
|||||||
Reference in New Issue
Block a user