765cc632d5
Remove the Order demo (entity/feature/repo/config/gRPC/proto) and the three pre-marketplace migrations; regenerate a fresh InitialBaseline migration. Stand up the REST surface (PingController + System/Ping CQRS) proving the Mediator -> behaviors -> OperationResult -> ApiResult envelope end to end. Close wiring gaps: register LoggingBehavior (outermost) and add the built-in rate limiter (per-IP global + otp/auth/sensitive policies), placed before authentication. Add current-user + audit plumbing: ICurrentUser (HttpContext + null impls), rename BaseEntity audit fields to CreatedAt/ModifiedAt (DateTimeOffset) + CreatedById/ModifiedById, stamped by a new AuditFieldInterceptor. Introduce five cross-cutting seams (IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher) with in-memory/local mocks registered via AddCrossCuttingSeams. Add Baya.Test.Foundation (encryptor, audit interceptor, ping handler) and update docs, contracts (swagger.v1.json), handoff, report, and mocks registry.
72 lines
3.0 KiB
C#
72 lines
3.0 KiB
C#
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
|
|
{
|
|
/// <summary>Per-IP baseline applied to every endpoint that doesn't opt into a named policy.</summary>
|
|
public const string GlobalPolicy = "global";
|
|
|
|
/// <summary>Tighter limit for OTP request/verify endpoints (applied in backend-phase-2).</summary>
|
|
public const string OtpPolicy = "otp";
|
|
|
|
/// <summary>Limit for login/refresh and other auth endpoints.</summary>
|
|
public const string AuthPolicy = "auth";
|
|
|
|
/// <summary>Limit for money-sensitive actions (refund/payout) applied in later phases.</summary>
|
|
public const string SensitivePolicy = "sensitive";
|
|
|
|
/// <summary>
|
|
/// Registers the built-in rate limiter with a per-IP global limit plus named policies that
|
|
/// auth/OTP/sensitive endpoints opt into via <c>[EnableRateLimiting(name)]</c>. Over-limit
|
|
/// requests get <c>429 Too Many Requests</c>. Pair with <c>app.UseRateLimiter()</c> placed
|
|
/// before <c>app.UseAuthentication()</c>.
|
|
/// </summary>
|
|
public static IServiceCollection AddRateLimitingPolicies(this IServiceCollection services)
|
|
{
|
|
services.AddRateLimiter(options =>
|
|
{
|
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
|
|
|
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(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";
|
|
}
|