refactor(notify): merge Notifier WebService into Notify WebService
- Delete dead Notify Worker (NoOp handler) - Move 51 source files (endpoints, contracts, services, compat stores) - Transform namespaces from Notifier.WebService to Notify.WebService - Update DI registrations, WebSocket support, v2 endpoint mapping - Comment out notifier-web in compose, update gateway routes - Update architecture docs, port registry, rollout matrix - Notifier Worker stays as separate delivery engine container Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for operator override management.
|
||||
/// </summary>
|
||||
public static class OperatorOverrideEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps operator override endpoints.
|
||||
/// </summary>
|
||||
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<IResult> 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<IResult> 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<IResult> 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<IResult> 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<IResult> 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<string> MapOverrideTypeToStrings(OverrideType type)
|
||||
{
|
||||
var result = new List<string>();
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Request to create an operator override.
|
||||
/// </summary>
|
||||
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<string>? EventKinds { get; set; }
|
||||
public List<string>? CorrelationKeys { get; set; }
|
||||
public int? MaxUsageCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to revoke an operator override.
|
||||
/// </summary>
|
||||
public sealed class RevokeOverrideApiRequest
|
||||
{
|
||||
public string? Actor { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to check for applicable override.
|
||||
/// </summary>
|
||||
public sealed class CheckOverrideApiRequest
|
||||
{
|
||||
public string? TenantId { get; set; }
|
||||
public string? EventKind { get; set; }
|
||||
public string? CorrelationKey { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for an operator override.
|
||||
/// </summary>
|
||||
public sealed class OperatorOverrideApiResponse
|
||||
{
|
||||
public required string OverrideId { get; set; }
|
||||
public required string TenantId { get; set; }
|
||||
public required List<string> Type { get; set; }
|
||||
public required string Reason { get; set; }
|
||||
public required DateTimeOffset EffectiveFrom { get; set; }
|
||||
public required DateTimeOffset ExpiresAt { get; set; }
|
||||
public required List<string> EventKinds { get; set; }
|
||||
public required List<string> 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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for override check.
|
||||
/// </summary>
|
||||
public sealed class CheckOverrideApiResponse
|
||||
{
|
||||
public required bool HasOverride { get; set; }
|
||||
public required List<string> BypassedTypes { get; set; }
|
||||
public OperatorOverrideApiResponse? Override { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user