feat(secrets-ui): add/rotate/delete modal + audited gated reveal
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
@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)
|
||||
{
|
||||
<div class="modal-backdrop-inpage" data-testid="delete-modal-backdrop"></div>
|
||||
<div class="modal" role="dialog" aria-modal="true" data-testid="delete-modal">
|
||||
<div class="modal-card">
|
||||
<h3 class="modal-title">Delete secret '@Name.Value'?</h3>
|
||||
<p>This tombstones the secret so the removal can propagate. The action is audited.</p>
|
||||
|
||||
@if (_error is not null)
|
||||
{
|
||||
<p class="field-error s-bad" data-testid="delete-error">@_error</p>
|
||||
}
|
||||
|
||||
<div class="btn-row">
|
||||
<TechButton Variant="ButtonVariant.Danger" Busy="_busy"
|
||||
data-testid="confirm-delete" @onclick="ConfirmAsync">
|
||||
Delete
|
||||
</TechButton>
|
||||
<TechButton Variant="ButtonVariant.Secondary"
|
||||
data-testid="cancel-delete" @onclick="CancelAsync">
|
||||
Cancel
|
||||
</TechButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private string? _error;
|
||||
private bool _busy;
|
||||
|
||||
/// <summary>Whether the modal overlay is rendered.</summary>
|
||||
[Parameter] public bool Visible { get; set; }
|
||||
|
||||
/// <summary>The secret the modal offers to delete.</summary>
|
||||
[Parameter] public SecretName Name { get; set; }
|
||||
|
||||
/// <summary>Raised after the secret has been tombstoned and audited.</summary>
|
||||
[Parameter] public EventCallback OnDeleted { get; set; }
|
||||
|
||||
/// <summary>Raised when the user cancels the deletion.</summary>
|
||||
[Parameter] public EventCallback OnCancelled { get; set; }
|
||||
|
||||
/// <summary>The cascading authentication state, used as the fallback source of the audit actor.</summary>
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private async Task ConfirmAsync()
|
||||
{
|
||||
_error = null;
|
||||
_busy = true;
|
||||
try
|
||||
{
|
||||
string actor = await SecretActorResolver.ResolveAsync(Services, AuthState);
|
||||
|
||||
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);
|
||||
|
||||
await OnDeleted.InvokeAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task CancelAsync() => OnCancelled.InvokeAsync();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user