@namespace ZB.MOM.WW.Secrets.Ui.Components
@implements IDisposable
@inject ISecretStore Store
@inject IAuditWriter Audit
@inject IServiceProvider Services
@* Components/ConfirmDeleteModal.razor — an in-page Blazor overlay (NOT a JS confirm/alert dialog).
Shown while Visible is true; Confirm tombstones + audits the delete, Cancel dismisses. *@
@if (Visible)
{
Delete secret '@Name.Value'?
This tombstones the secret so the removal can propagate. The action is audited.
@if (_error is not null)
{
@_error
}
Delete
Cancel
}
@code {
private readonly CancellationTokenSource _cts = new();
private string? _error;
private bool _busy;
/// Whether the modal overlay is rendered.
[Parameter] public bool Visible { get; set; }
/// The secret the modal offers to delete.
[Parameter] public SecretName Name { get; set; }
/// Raised after the secret has been tombstoned and audited.
[Parameter] public EventCallback OnDeleted { get; set; }
/// Raised when the user cancels the deletion.
[Parameter] public EventCallback OnCancelled { get; set; }
/// The cascading authentication state, used as the fallback source of the audit actor.
[CascadingParameter] private Task? AuthState { get; set; }
private async Task ConfirmAsync()
{
_error = null;
_busy = true;
bool deleted = false;
try
{
string actor = await SecretActorResolver.ResolveAsync(Services, AuthState);
try
{
await Store.DeleteAsync(Name, actor, _cts.Token);
await Audit.WriteAsync(
new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTimeOffset.UtcNow,
Actor = actor,
Action = "secret.delete",
Outcome = AuditOutcome.Success,
Category = "Secrets",
Target = Name.Value,
},
_cts.Token);
deleted = true;
}
catch (Exception)
{
// An unexpected store fault must still be audited as a Failure and surfaced to the
// operator instead of tearing down the circuit.
_error = "Unable to delete the secret.";
await WriteFailureAuditAsync(actor);
}
}
finally
{
_busy = false;
}
if (deleted)
{
// Evict any cached plaintext so the ${secret:} / ISecretResolver path does not serve a
// stale value after this delete for the remainder of its TTL. Optional seam — a no-op
// when the host has not registered an invalidator.
Services.GetService()?.Invalidate(Name);
await OnDeleted.InvokeAsync();
}
}
private async Task WriteFailureAuditAsync(string actor)
{
try
{
await Audit.WriteAsync(
new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTimeOffset.UtcNow,
Actor = actor,
Action = "secret.delete",
Outcome = AuditOutcome.Failure,
Category = "Secrets",
Target = Name.Value,
DetailsJson = "{\"reason\":\"error\"}",
},
_cts.Token);
}
catch
{
// Best-effort audit of a failure; never let audit itself surface into the event pipeline.
}
}
private Task CancelAsync() => OnCancelled.InvokeAsync();
///
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}