Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/ApiKeys.razor
T

180 lines
6.7 KiB
Plaintext

@page "/admin/api-keys"
@using ZB.MOM.WW.ScadaBridge.Security
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Security
@attribute [Authorize(Policy = AuthorizationPolicies.RequireAdmin)]
@inject IInboundApiKeyAdmin ApiKeyAdmin
@inject NavigationManager NavigationManager
@inject IDialogService Dialog
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">API Key Management</h4>
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/admin/api-keys/create")'>Add API Key</button>
</div>
<ToastNotification @ref="_toast" />
@if (_loading)
{
<LoadingSpinner IsLoading="true" />
}
else if (_errorMessage != null)
{
<div class="alert alert-danger">@_errorMessage</div>
}
else
{
<div class="mb-3" style="max-width: 320px;">
<input class="form-control form-control-sm"
placeholder="Filter by name…"
@bind="_search" @bind:event="oninput" />
</div>
@if (_keys.Count == 0)
{
<p class="text-muted text-center">No API keys configured.</p>
}
else if (!FilteredKeys.Any())
{
<p class="text-muted small">No API keys match the filter.</p>
}
else
{
<table class="table table-sm table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Key ID</th>
<th>Name</th>
<th>Methods</th>
<th style="width: 160px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var key in FilteredKeys)
{
<tr @key="key.KeyId">
<td><code>@TruncateKeyId(key.KeyId)</code></td>
<td>
@key.Name
@if (!key.Enabled)
{
<span class="badge bg-secondary ms-1">Disabled</span>
}
</td>
<td>@key.Methods.Count</td>
<td>
<div class="d-flex gap-1">
<button class="btn btn-outline-primary btn-sm py-0 px-2"
@onclick='() => NavigationManager.NavigateTo($"/admin/api-keys/{key.KeyId}/edit")'>Edit</button>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm py-0 px-2"
data-bs-toggle="dropdown"
aria-label="@($"More actions for {key.Name}")">⋮</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item"
@onclick="() => ToggleKey(key)">
@(key.Enabled ? "Disable" : "Enable")
</button>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<button class="dropdown-item text-danger"
@onclick="() => DeleteKey(key)">
Delete
</button>
</li>
</ul>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
}
}
</div>
@code {
// Inbound-API key re-arch (C3): this page reads keys from the IInboundApiKeyAdmin seam
// (string KeyId, method-scopes) rather than the SQL Server ApiKey entity. The seam has no
// retrievable hash, so the old masked Key-Hash column is gone; KeyId identifies each row.
private List<InboundApiKeyInfo> _keys = new();
private bool _loading = true;
private string? _errorMessage;
private string _search = string.Empty;
private ToastNotification _toast = default!;
private IEnumerable<InboundApiKeyInfo> FilteredKeys =>
string.IsNullOrWhiteSpace(_search)
? _keys
: _keys.Where(k =>
k.Name?.Contains(_search, StringComparison.OrdinalIgnoreCase) ?? false);
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
_loading = true;
_errorMessage = null;
try
{
_keys = (await ApiKeyAdmin.ListAsync()).ToList();
}
catch (Exception ex)
{
_errorMessage = $"Failed to load API keys: {ex.Message}";
}
_loading = false;
}
// Show a short, recognizable prefix of the opaque KeyId rather than the full 32-char value.
private static string TruncateKeyId(string keyId)
{
if (string.IsNullOrEmpty(keyId)) return keyId;
return keyId.Length <= 12 ? keyId : keyId[..12] + "…";
}
private async Task ToggleKey(InboundApiKeyInfo key)
{
try
{
var newEnabled = !key.Enabled;
// The seam persists; there is no separate SaveChangesAsync.
await ApiKeyAdmin.SetEnabledAsync(key.KeyId, newEnabled);
_toast.ShowSuccess($"API key '{key.Name}' {(newEnabled ? "enabled" : "disabled")}.");
await LoadDataAsync();
}
catch (Exception ex)
{
_toast.ShowError($"Toggle failed: {ex.Message}");
}
}
private async Task DeleteKey(InboundApiKeyInfo key)
{
var confirmed = await Dialog.ConfirmAsync(
"Delete API Key",
$"Delete API key '{key.Name}'? This cannot be undone.",
danger: true);
if (!confirmed) return;
try
{
await ApiKeyAdmin.DeleteAsync(key.KeyId);
_toast.ShowSuccess($"API key '{key.Name}' deleted.");
await LoadDataAsync();
}
catch (Exception ex)
{
_toast.ShowError($"Delete failed: {ex.Message}");
}
}
}