up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for throttle configuration management.
|
||||
/// </summary>
|
||||
public static class ThrottleEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps throttle configuration endpoints.
|
||||
/// </summary>
|
||||
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<IResult> 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<string, int>(),
|
||||
IsDefault = true
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(MapToApiResponse(config));
|
||||
}
|
||||
|
||||
private static async Task<IResult> 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<IResult> 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<IResult> 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<string, int>(),
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Request to create or update throttle configuration.
|
||||
/// </summary>
|
||||
public sealed class ThrottleConfigurationApiRequest
|
||||
{
|
||||
public string? TenantId { get; set; }
|
||||
public int? DefaultDurationSeconds { get; set; }
|
||||
public Dictionary<string, int>? EventKindOverrides { get; set; }
|
||||
public int? MaxEventsPerWindow { get; set; }
|
||||
public int? BurstWindowDurationSeconds { get; set; }
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to evaluate throttle duration.
|
||||
/// </summary>
|
||||
public sealed class ThrottleEvaluateApiRequest
|
||||
{
|
||||
public string? TenantId { get; set; }
|
||||
public string? EventKind { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for throttle configuration.
|
||||
/// </summary>
|
||||
public sealed class ThrottleConfigurationApiResponse
|
||||
{
|
||||
public required string TenantId { get; set; }
|
||||
public required int DefaultDurationSeconds { get; set; }
|
||||
public required Dictionary<string, int> 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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for throttle evaluation.
|
||||
/// </summary>
|
||||
public sealed class ThrottleEvaluateApiResponse
|
||||
{
|
||||
public required string EventKind { get; set; }
|
||||
public required int EffectiveDurationSeconds { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user