180 lines
6.7 KiB
Plaintext
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}");
|
|
}
|
|
}
|
|
|
|
}
|