fleshout backend

This commit is contained in:
KS Jannette
2026-08-03 04:23:32 -04:00
parent a00282b1e6
commit 092ef17439
19 changed files with 500 additions and 52 deletions

View File

@@ -0,0 +1,8 @@
namespace Backend.Gateway;
public static class ApiRoutes
{
public const string V1 = "v1";
public const string Health = "/health";
}

View File

@@ -0,0 +1,14 @@
namespace Backend.Gateway;
public static class GatewayApplicationBuilderExtensions
{
// CORS must run before the rate limiter so that rejected requests still carry
// the headers the browser needs to surface a 429 to the client.
public static IApplicationBuilder UseApiGateway(this IApplicationBuilder app)
{
app.UseCors(GatewayServiceCollectionExtensions.CorsPolicyName);
app.UseRateLimiter();
return app;
}
}

View File

@@ -0,0 +1,19 @@
namespace Backend.Gateway;
public sealed class CorsOptions
{
public const string SectionName = "Cors";
public string[] AllowedOrigins { get; set; } = [];
}
public sealed class RateLimitOptions
{
public const string SectionName = "RateLimiting";
public int PermitLimit { get; set; } = 100;
public int WindowSeconds { get; set; } = 60;
public int QueueLimit { get; set; }
}

View File

@@ -0,0 +1,64 @@
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
namespace Backend.Gateway;
public static class GatewayServiceCollectionExtensions
{
public const string CorsPolicyName = "DefaultCorsPolicy";
public static IServiceCollection AddApiGateway(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<CorsOptions>(configuration.GetSection(CorsOptions.SectionName));
services.Configure<RateLimitOptions>(configuration.GetSection(RateLimitOptions.SectionName));
var corsSettings = configuration.GetSection(CorsOptions.SectionName).Get<CorsOptions>()
?? new CorsOptions();
var rateLimitSettings = configuration.GetSection(RateLimitOptions.SectionName).Get<RateLimitOptions>()
?? new RateLimitOptions();
services.AddCors(options => options.AddPolicy(CorsPolicyName, policy =>
{
if (corsSettings.AllowedOrigins.Length == 0)
{
return;
}
policy.WithOrigins(corsSettings.AllowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod();
}));
services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.GlobalLimiter = CreateGlobalLimiter(rateLimitSettings);
});
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
return services;
}
// Health checks are exempt so that infrastructure probes are never throttled.
private static PartitionedRateLimiter<HttpContext> CreateGlobalLimiter(RateLimitOptions settings) =>
PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
if (context.Request.Path.StartsWithSegments(ApiRoutes.Health))
{
return RateLimitPartition.GetNoLimiter("health");
}
var partitionKey = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions
{
PermitLimit = settings.PermitLimit,
Window = TimeSpan.FromSeconds(settings.WindowSeconds),
QueueLimit = settings.QueueLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
});
});
}