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