using System.Text.Json; using Meezi.API.Services; using Meezi.Core.Interfaces; using Meezi.Shared; namespace Meezi.API.Middleware; public class PlanLimitMiddleware { private static readonly string[] SkipPrefixes = [ "/api/auth", "/api/customers/me", "/api/admin", "/hubs/guest-order", "/api/public", "/api/q/", "/api/webhooks", "/api/billing/verify", "/health", "/swagger", "/hangfire" ]; private readonly RequestDelegate _next; public PlanLimitMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, ITenantContext tenant, IPlanLimitChecker planLimitChecker) { if (ShouldSkip(context.Request.Path)) { await _next(context); return; } if (context.User.Identity?.IsAuthenticated == true) { var (allowed, code, message) = await planLimitChecker.CheckAsync(context, tenant, context.RequestAborted); if (!allowed) { context.Response.StatusCode = StatusCodes.Status403Forbidden; context.Response.ContentType = "application/json"; var payload = new ApiResponse(false, null, new ApiError(code!, message!)); await context.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); return; } } await _next(context); } private static bool ShouldSkip(PathString path) { var value = path.Value ?? string.Empty; return SkipPrefixes.Any(p => value.StartsWith(p, StringComparison.OrdinalIgnoreCase)); } }