using Microsoft.AspNetCore.Mvc; using StellaOps.Notifier.Worker.Correlation; namespace StellaOps.Notifier.WebService.Endpoints; /// /// API endpoints for throttle configuration management. /// public static class ThrottleEndpoints { /// /// Maps throttle configuration endpoints. /// public static IEndpointRouteBuilder MapThrottleEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v2/throttles") .WithTags("Throttles") .WithOpenApi(); group.MapGet("/config", GetConfigurationAsync) .WithName("GetThrottleConfiguration") .WithSummary("Get throttle configuration") .WithDescription("Returns the throttle configuration for the tenant."); group.MapPut("/config", UpdateConfigurationAsync) .WithName("UpdateThrottleConfiguration") .WithSummary("Update throttle configuration") .WithDescription("Creates or updates the throttle configuration for the tenant."); group.MapDelete("/config", DeleteConfigurationAsync) .WithName("DeleteThrottleConfiguration") .WithSummary("Delete throttle configuration") .WithDescription("Deletes the throttle configuration for the tenant, reverting to defaults."); group.MapPost("/evaluate", EvaluateAsync) .WithName("EvaluateThrottle") .WithSummary("Evaluate throttle duration") .WithDescription("Returns the effective throttle duration for an event kind."); return app; } private static async Task GetConfigurationAsync( [FromHeader(Name = "X-Tenant-Id")] string? tenantId, [FromServices] IThrottleConfigurationService throttleService, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "X-Tenant-Id header is required." }); } var config = await throttleService.GetConfigurationAsync(tenantId, cancellationToken); if (config is null) { return Results.Ok(new ThrottleConfigurationApiResponse { TenantId = tenantId, DefaultDurationSeconds = 900, // 15 minutes default Enabled = true, EventKindOverrides = new Dictionary(), IsDefault = true }); } return Results.Ok(MapToApiResponse(config)); } private static async Task UpdateConfigurationAsync( [FromBody] ThrottleConfigurationApiRequest request, [FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader, [FromHeader(Name = "X-Actor")] string? actor, [FromServices] IThrottleConfigurationService throttleService, CancellationToken cancellationToken) { var tenantId = request.TenantId ?? tenantIdHeader; if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "Tenant ID is required via X-Tenant-Id header or request body." }); } if (request.DefaultDurationSeconds is <= 0) { return Results.BadRequest(new { error = "Default duration must be a positive value in seconds." }); } var config = new ThrottleConfiguration { TenantId = tenantId, DefaultDuration = TimeSpan.FromSeconds(request.DefaultDurationSeconds ?? 900), EventKindOverrides = request.EventKindOverrides? .ToDictionary(kvp => kvp.Key, kvp => TimeSpan.FromSeconds(kvp.Value)), MaxEventsPerWindow = request.MaxEventsPerWindow, BurstWindowDuration = request.BurstWindowDurationSeconds.HasValue ? TimeSpan.FromSeconds(request.BurstWindowDurationSeconds.Value) : null, Enabled = request.Enabled ?? true }; var updated = await throttleService.UpsertConfigurationAsync(config, actor, cancellationToken); return Results.Ok(MapToApiResponse(updated)); } private static async Task DeleteConfigurationAsync( [FromHeader(Name = "X-Tenant-Id")] string? tenantId, [FromHeader(Name = "X-Actor")] string? actor, [FromServices] IThrottleConfigurationService throttleService, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "X-Tenant-Id header is required." }); } var deleted = await throttleService.DeleteConfigurationAsync(tenantId, actor, cancellationToken); if (!deleted) { return Results.NotFound(new { error = "No throttle configuration exists for this tenant." }); } return Results.NoContent(); } private static async Task EvaluateAsync( [FromBody] ThrottleEvaluateApiRequest request, [FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader, [FromServices] IThrottleConfigurationService throttleService, CancellationToken cancellationToken) { var tenantId = request.TenantId ?? tenantIdHeader; if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "Tenant ID is required via X-Tenant-Id header or request body." }); } if (string.IsNullOrWhiteSpace(request.EventKind)) { return Results.BadRequest(new { error = "Event kind is required." }); } var duration = await throttleService.GetEffectiveThrottleDurationAsync( tenantId, request.EventKind, cancellationToken); return Results.Ok(new ThrottleEvaluateApiResponse { EventKind = request.EventKind, EffectiveDurationSeconds = (int)duration.TotalSeconds }); } private static ThrottleConfigurationApiResponse MapToApiResponse(ThrottleConfiguration config) => new() { TenantId = config.TenantId, DefaultDurationSeconds = (int)config.DefaultDuration.TotalSeconds, EventKindOverrides = config.EventKindOverrides? .ToDictionary(kvp => kvp.Key, kvp => (int)kvp.Value.TotalSeconds) ?? new Dictionary(), MaxEventsPerWindow = config.MaxEventsPerWindow, BurstWindowDurationSeconds = config.BurstWindowDuration.HasValue ? (int)config.BurstWindowDuration.Value.TotalSeconds : null, Enabled = config.Enabled, CreatedAt = config.CreatedAt, CreatedBy = config.CreatedBy, UpdatedAt = config.UpdatedAt, UpdatedBy = config.UpdatedBy, IsDefault = false }; } #region API Request/Response Models /// /// Request to create or update throttle configuration. /// public sealed class ThrottleConfigurationApiRequest { public string? TenantId { get; set; } public int? DefaultDurationSeconds { get; set; } public Dictionary? EventKindOverrides { get; set; } public int? MaxEventsPerWindow { get; set; } public int? BurstWindowDurationSeconds { get; set; } public bool? Enabled { get; set; } } /// /// Request to evaluate throttle duration. /// public sealed class ThrottleEvaluateApiRequest { public string? TenantId { get; set; } public string? EventKind { get; set; } } /// /// Response for throttle configuration. /// public sealed class ThrottleConfigurationApiResponse { public required string TenantId { get; set; } public required int DefaultDurationSeconds { get; set; } public required Dictionary EventKindOverrides { get; set; } public int? MaxEventsPerWindow { get; set; } public int? BurstWindowDurationSeconds { get; set; } public required bool Enabled { get; set; } public DateTimeOffset CreatedAt { get; set; } public string? CreatedBy { get; set; } public DateTimeOffset UpdatedAt { get; set; } public string? UpdatedBy { get; set; } public bool IsDefault { get; set; } } /// /// Response for throttle evaluation. /// public sealed class ThrottleEvaluateApiResponse { public required string EventKind { get; set; } public required int EffectiveDurationSeconds { get; set; } } #endregion