using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Notify.WebService.Constants; using StellaOps.Notify.WebService.Extensions; using StellaOps.Notifier.Worker.Correlation; using static StellaOps.Localization.T; namespace StellaOps.Notify.WebService.Endpoints; /// /// API endpoints for operator override management. /// public static class OperatorOverrideEndpoints { /// /// Maps operator override endpoints. /// public static IEndpointRouteBuilder MapOperatorOverrideEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v2/overrides") .WithTags("Overrides") .WithOpenApi() .RequireAuthorization(NotifierPolicies.NotifyViewer) .RequireTenant(); group.MapGet("/", ListOverridesAsync) .WithName("ListOperatorOverrides") .WithSummary("List active operator overrides") .WithDescription(_t("notifier.override.list_description")); group.MapGet("/{overrideId}", GetOverrideAsync) .WithName("GetOperatorOverride") .WithSummary("Get an operator override") .WithDescription(_t("notifier.override.get_description")); group.MapPost("/", CreateOverrideAsync) .WithName("CreateOperatorOverride") .WithSummary("Create an operator override") .WithDescription(_t("notifier.override.create_description")) .RequireAuthorization(NotifierPolicies.NotifyOperator); group.MapPost("/{overrideId}/revoke", RevokeOverrideAsync) .WithName("RevokeOperatorOverride") .WithSummary("Revoke an operator override") .WithDescription(_t("notifier.override.revoke_description")) .RequireAuthorization(NotifierPolicies.NotifyOperator); group.MapPost("/check", CheckOverrideAsync) .WithName("CheckOperatorOverride") .WithSummary("Check for applicable override") .WithDescription(_t("notifier.override.check_description")); return app; } private static async Task ListOverridesAsync( [FromHeader(Name = "X-Tenant-Id")] string? tenantId, [FromServices] IOperatorOverrideService overrideService, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = _t("notifier.error.tenant_id_missing") }); } var overrides = await overrideService.ListActiveOverridesAsync(tenantId, cancellationToken); return Results.Ok(overrides.Select(MapToApiResponse)); } private static async Task GetOverrideAsync( string overrideId, [FromHeader(Name = "X-Tenant-Id")] string? tenantId, [FromServices] IOperatorOverrideService overrideService, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = _t("notifier.error.tenant_id_missing") }); } var @override = await overrideService.GetOverrideAsync(tenantId, overrideId, cancellationToken); if (@override is null) { return Results.NotFound(new { error = _t("notifier.error.override_not_found", overrideId) }); } return Results.Ok(MapToApiResponse(@override)); } private static async Task CreateOverrideAsync( [FromBody] OperatorOverrideApiRequest request, [FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader, [FromHeader(Name = "X-Actor")] string? actorHeader, [FromServices] IOperatorOverrideService overrideService, CancellationToken cancellationToken) { var tenantId = request.TenantId ?? tenantIdHeader; if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = _t("notifier.error.tenant_required") }); } var actor = request.Actor ?? actorHeader; if (string.IsNullOrWhiteSpace(actor)) { return Results.BadRequest(new { error = _t("notifier.error.actor_required") }); } if (string.IsNullOrWhiteSpace(request.Reason)) { return Results.BadRequest(new { error = _t("notifier.error.reason_required") }); } if (request.DurationMinutes is null or <= 0) { return Results.BadRequest(new { error = _t("notifier.error.duration_required") }); } var createRequest = new OperatorOverrideCreate { Type = MapOverrideType(request.Type), Reason = request.Reason, Duration = TimeSpan.FromMinutes(request.DurationMinutes.Value), EffectiveFrom = request.EffectiveFrom, EventKinds = request.EventKinds, CorrelationKeys = request.CorrelationKeys, MaxUsageCount = request.MaxUsageCount }; try { var created = await overrideService.CreateOverrideAsync(tenantId, createRequest, actor, cancellationToken); return Results.Created($"/api/v2/overrides/{created.OverrideId}", MapToApiResponse(created)); } catch (ArgumentException ex) { return Results.BadRequest(new { error = ex.Message }); } catch (InvalidOperationException ex) { return Results.Conflict(new { error = ex.Message }); } } private static async Task RevokeOverrideAsync( string overrideId, [FromBody] RevokeOverrideApiRequest? request, [FromHeader(Name = "X-Tenant-Id")] string? tenantId, [FromHeader(Name = "X-Actor")] string? actorHeader, [FromServices] IOperatorOverrideService overrideService, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = _t("notifier.error.tenant_id_missing") }); } var actor = request?.Actor ?? actorHeader; if (string.IsNullOrWhiteSpace(actor)) { return Results.BadRequest(new { error = _t("notifier.error.actor_required") }); } var revoked = await overrideService.RevokeOverrideAsync( tenantId, overrideId, actor, request?.Reason, cancellationToken); if (!revoked) { return Results.NotFound(new { error = _t("notifier.error.override_not_found_or_inactive", overrideId) }); } return Results.NoContent(); } private static async Task CheckOverrideAsync( [FromBody] CheckOverrideApiRequest request, [FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader, [FromServices] IOperatorOverrideService overrideService, CancellationToken cancellationToken) { var tenantId = request.TenantId ?? tenantIdHeader; if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = _t("notifier.error.tenant_required") }); } if (string.IsNullOrWhiteSpace(request.EventKind)) { return Results.BadRequest(new { error = _t("notifier.error.event_kind_required") }); } var result = await overrideService.CheckOverrideAsync( tenantId, request.EventKind, request.CorrelationKey, cancellationToken); return Results.Ok(new CheckOverrideApiResponse { HasOverride = result.HasOverride, BypassedTypes = MapOverrideTypeToStrings(result.BypassedTypes), Override = result.Override is not null ? MapToApiResponse(result.Override) : null }); } private static OverrideType MapOverrideType(string? type) => type?.ToLowerInvariant() switch { "quiethours" or "quiet_hours" => OverrideType.QuietHours, "throttle" => OverrideType.Throttle, "maintenance" => OverrideType.Maintenance, "all" or _ => OverrideType.All }; private static List MapOverrideTypeToStrings(OverrideType type) { var result = new List(); if (type.HasFlag(OverrideType.QuietHours)) result.Add("quiet_hours"); if (type.HasFlag(OverrideType.Throttle)) result.Add("throttle"); if (type.HasFlag(OverrideType.Maintenance)) result.Add("maintenance"); return result; } private static OperatorOverrideApiResponse MapToApiResponse(OperatorOverride @override) => new() { OverrideId = @override.OverrideId, TenantId = @override.TenantId, Type = MapOverrideTypeToStrings(@override.Type), Reason = @override.Reason, EffectiveFrom = @override.EffectiveFrom, ExpiresAt = @override.ExpiresAt, EventKinds = @override.EventKinds.ToList(), CorrelationKeys = @override.CorrelationKeys.ToList(), MaxUsageCount = @override.MaxUsageCount, UsageCount = @override.UsageCount, Status = @override.Status.ToString().ToLowerInvariant(), CreatedBy = @override.CreatedBy, CreatedAt = @override.CreatedAt, RevokedBy = @override.RevokedBy, RevokedAt = @override.RevokedAt, RevocationReason = @override.RevocationReason }; } #region API Request/Response Models /// /// Request to create an operator override. /// public sealed class OperatorOverrideApiRequest { public string? TenantId { get; set; } public string? Actor { get; set; } public string? Type { get; set; } public string? Reason { get; set; } public int? DurationMinutes { get; set; } public DateTimeOffset? EffectiveFrom { get; set; } public List? EventKinds { get; set; } public List? CorrelationKeys { get; set; } public int? MaxUsageCount { get; set; } } /// /// Request to revoke an operator override. /// public sealed class RevokeOverrideApiRequest { public string? Actor { get; set; } public string? Reason { get; set; } } /// /// Request to check for applicable override. /// public sealed class CheckOverrideApiRequest { public string? TenantId { get; set; } public string? EventKind { get; set; } public string? CorrelationKey { get; set; } } /// /// Response for an operator override. /// public sealed class OperatorOverrideApiResponse { public required string OverrideId { get; set; } public required string TenantId { get; set; } public required List Type { get; set; } public required string Reason { get; set; } public required DateTimeOffset EffectiveFrom { get; set; } public required DateTimeOffset ExpiresAt { get; set; } public required List EventKinds { get; set; } public required List CorrelationKeys { get; set; } public int? MaxUsageCount { get; set; } public required int UsageCount { get; set; } public required string Status { get; set; } public required string CreatedBy { get; set; } public required DateTimeOffset CreatedAt { get; set; } public string? RevokedBy { get; set; } public DateTimeOffset? RevokedAt { get; set; } public string? RevocationReason { get; set; } } /// /// Response for override check. /// public sealed class CheckOverrideApiResponse { public required bool HasOverride { get; set; } public required List BypassedTypes { get; set; } public OperatorOverrideApiResponse? Override { get; set; } } #endregion