Dashboard: delete revoked API keys + confirm Rotate/Revoke/Delete
Add IApiKeyAdminStore.DeleteAsync that only deletes already-revoked rows (active keys must be revoked first so the revoke event lands in the audit log before the row disappears) and a matching admin-gated DashboardApiKeyManagementService.DeleteAsync. ApiKeysPage now shows Delete on revoked rows in place of the old "No actions" stub, and Rotate/Revoke/Delete all route through ConfirmDialog so each destructive action requires an explicit confirmation step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,15 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
<ConfirmDialog IsOpen="@(PendingAction is not null)"
|
||||
Title="@(PendingAction?.Title ?? string.Empty)"
|
||||
Message="@(PendingAction?.Message ?? string.Empty)"
|
||||
ConfirmLabel="@(PendingAction?.ConfirmLabel ?? "Confirm")"
|
||||
ConfirmButtonClass="@(PendingAction?.ConfirmButtonClass ?? "btn-primary")"
|
||||
IsBusy="IsBusy"
|
||||
OnConfirm="ConfirmPendingAsync"
|
||||
OnCancel="CancelPending" />
|
||||
|
||||
@if (IsCreateDialogOpen)
|
||||
{
|
||||
<div class="modal-backdrop fade show"></div>
|
||||
@@ -171,18 +180,22 @@ else
|
||||
revoked key is not un-revoked as a side effect of rotation. *@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
disabled="@IsBusy"
|
||||
@onclick="() => RotateApiKeyAsync(key.KeyId)">
|
||||
@onclick="() => RequestRotate(key.KeyId)">
|
||||
Rotate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
disabled="@IsBusy"
|
||||
@onclick="() => RevokeApiKeyAsync(key.KeyId)">
|
||||
@onclick="() => RequestRevoke(key.KeyId)">
|
||||
Revoke
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted small">No actions</span>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
disabled="@IsBusy"
|
||||
@onclick="() => RequestDelete(key.KeyId)">
|
||||
Delete
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
@@ -252,24 +265,80 @@ else
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RevokeApiKeyAsync(string keyId)
|
||||
private PendingConfirm? PendingAction { get; set; }
|
||||
|
||||
private void RequestRotate(string keyId)
|
||||
{
|
||||
await RunManagementActionAsync(user => ApiKeyManagementService.RevokeAsync(
|
||||
user,
|
||||
keyId,
|
||||
CancellationToken.None))
|
||||
.ConfigureAwait(false);
|
||||
if (IsBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PendingAction = new PendingConfirm(
|
||||
Title: "Rotate API key?",
|
||||
Message: $"Rotate the secret for key {keyId}? Any client still using the previous secret will start failing immediately.",
|
||||
ConfirmLabel: "Rotate",
|
||||
ConfirmButtonClass: "btn-warning",
|
||||
Action: user => ApiKeyManagementService.RotateAsync(user, keyId, CancellationToken.None));
|
||||
}
|
||||
|
||||
private async Task RotateApiKeyAsync(string keyId)
|
||||
private void RequestRevoke(string keyId)
|
||||
{
|
||||
await RunManagementActionAsync(user => ApiKeyManagementService.RotateAsync(
|
||||
user,
|
||||
keyId,
|
||||
CancellationToken.None))
|
||||
.ConfigureAwait(false);
|
||||
if (IsBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PendingAction = new PendingConfirm(
|
||||
Title: "Revoke API key?",
|
||||
Message: $"Revoke key {keyId}? Clients using it will be rejected on the next request.",
|
||||
ConfirmLabel: "Revoke",
|
||||
ConfirmButtonClass: "btn-danger",
|
||||
Action: user => ApiKeyManagementService.RevokeAsync(user, keyId, CancellationToken.None));
|
||||
}
|
||||
|
||||
private void RequestDelete(string keyId)
|
||||
{
|
||||
if (IsBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PendingAction = new PendingConfirm(
|
||||
Title: "Delete API key?",
|
||||
Message: $"Permanently delete revoked key {keyId}? This removes the row from the auth database — only the audit log will retain the history.",
|
||||
ConfirmLabel: "Delete",
|
||||
ConfirmButtonClass: "btn-danger",
|
||||
Action: user => ApiKeyManagementService.DeleteAsync(user, keyId, CancellationToken.None));
|
||||
}
|
||||
|
||||
private void CancelPending()
|
||||
{
|
||||
if (!IsBusy)
|
||||
{
|
||||
PendingAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConfirmPendingAsync()
|
||||
{
|
||||
if (IsBusy || PendingAction is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardApiKeyManagementResult>> action = PendingAction.Action;
|
||||
PendingAction = null;
|
||||
await RunManagementActionAsync(action).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private sealed record PendingConfirm(
|
||||
string Title,
|
||||
string Message,
|
||||
string ConfirmLabel,
|
||||
string ConfirmButtonClass,
|
||||
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardApiKeyManagementResult>> Action);
|
||||
|
||||
private async Task RunManagementActionAsync(
|
||||
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardApiKeyManagementResult>> action)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user