using System.Threading.RateLimiting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace StartupHelpers; public static class RateLimitingExtensions { public static void AddPublicApiRateLimiting(this IServiceCollection services) { services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => { var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; return RateLimitPartition.GetFixedWindowLimiter( partitionKey: ip, factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 120, Window = TimeSpan.FromMinutes(1), QueueLimit = 0, AutoReplenishment = true }); }); options.AddPolicy("contact", httpContext => { var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; return RateLimitPartition.GetFixedWindowLimiter( partitionKey: ip, factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 5, Window = TimeSpan.FromMinutes(1), QueueLimit = 0, AutoReplenishment = true }); }); options.AddPolicy("cv-matcher", httpContext => { var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; return RateLimitPartition.GetFixedWindowLimiter( partitionKey: ip, factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 10, Window = TimeSpan.FromMinutes(10), QueueLimit = 0, AutoReplenishment = true }); }); options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.OnRejected = async (context, ct) => { var logger = context.HttpContext.RequestServices .GetRequiredService() .CreateLogger("RateLimiting"); var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; var endpoint = context.HttpContext.Request.Path; logger.LogWarning("Rate limit exceeded for {Endpoint} from IP {IP}", endpoint, ip); context.HttpContext.Response.ContentType = "application/json"; await context.HttpContext.Response.WriteAsync("""{"error":"Too many requests. Try again later."}""", ct); }; }); } }