using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.DependencyInjection;
namespace Baya.WebFramework.ServiceConfiguration;
public static class RateLimitingServiceExtension
{
/// Per-IP baseline applied to every endpoint that doesn't opt into a named policy.
public const string GlobalPolicy = "global";
/// Tighter limit for OTP request/verify endpoints (applied in backend-phase-2).
public const string OtpPolicy = "otp";
/// Limit for login/refresh and other auth endpoints.
public const string AuthPolicy = "auth";
/// Limit for money-sensitive actions (refund/payout) applied in later phases.
public const string SensitivePolicy = "sensitive";
///
/// Registers the built-in rate limiter with a per-IP global limit plus named policies that
/// auth/OTP/sensitive endpoints opt into via [EnableRateLimiting(name)]. Over-limit
/// requests get 429 Too Many Requests. Pair with app.UseRateLimiter() placed
/// before app.UseAuthentication().
///
public static IServiceCollection AddRateLimitingPolicies(this IServiceCollection services)
{
services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.GlobalLimiter = PartitionedRateLimiter.Create(context =>
RateLimitPartition.GetFixedWindowLimiter(
PartitionKey(context),
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0
}));
AddFixedWindowPolicy(options, OtpPolicy, permitLimit: 5, windowSeconds: 60);
AddFixedWindowPolicy(options, AuthPolicy, permitLimit: 10, windowSeconds: 60);
AddFixedWindowPolicy(options, SensitivePolicy, permitLimit: 20, windowSeconds: 60);
// A deliberately tiny policy used by the phase-0 ping endpoint to demonstrate 429s.
AddFixedWindowPolicy(options, GlobalPolicy, permitLimit: 5, windowSeconds: 10);
});
return services;
}
private static void AddFixedWindowPolicy(RateLimiterOptions options, string name, int permitLimit, int windowSeconds)
{
options.AddPolicy(name, context =>
RateLimitPartition.GetFixedWindowLimiter(
PartitionKey(context),
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = permitLimit,
Window = TimeSpan.FromSeconds(windowSeconds),
QueueLimit = 0
}));
}
private static string PartitionKey(HttpContext context) =>
context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}