using System.Text.Json; namespace WorkClub.Api.Middleware; public class TenantValidationMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public TenantValidationMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { _logger.LogInformation("TenantValidationMiddleware: Processing request for {Path}", context.Request.Path); if (!context.User.Identity?.IsAuthenticated ?? true) { await _next(context); return; } if (!context.Request.Headers.TryGetValue("X-Tenant-Id", out var tenantIdHeader) || string.IsNullOrWhiteSpace(tenantIdHeader)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsJsonAsync(new { error = "X-Tenant-Id header is required" }); return; } var requestedTenantId = tenantIdHeader.ToString(); var clubsClaim = context.User.FindFirst("clubs")?.Value; if (string.IsNullOrEmpty(clubsClaim)) { context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsJsonAsync(new { error = "User does not have clubs claim" }); return; } // Parse comma-separated club UUIDs var clubIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(id => id.Trim()) .ToArray(); if (clubIds.Length == 0 || !clubIds.Contains(requestedTenantId)) { context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsJsonAsync(new { error = $"User is not a member of tenant {requestedTenantId}" }); return; } // Store validated tenant ID in HttpContext.Items for downstream middleware/services context.Items["TenantId"] = requestedTenantId; _logger.LogInformation("TenantValidationMiddleware: Set TenantId={TenantId} in HttpContext.Items", requestedTenantId); await _next(context); } }