feat(auth): ScadaBridge CentralUI pages onto IInboundApiKeyAdmin seam (re-arch C3; string keyId, method-scopes replace ApprovedApiKeyIds, token-once display, approved-keys<->scopes inversion)

This commit is contained in:
Joseph Doherty
2026-06-02 04:36:50 -04:00
parent 8219b8ee18
commit 107e524914
8 changed files with 576 additions and 164 deletions
@@ -1,9 +1,8 @@
@page "/admin/api-keys"
@using ZB.MOM.WW.ScadaBridge.Security
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Security
@attribute [Authorize(Policy = AuthorizationPolicies.RequireAdmin)]
@inject IInboundApiRepository InboundApiRepository
@inject IInboundApiKeyAdmin ApiKeyAdmin
@inject NavigationManager NavigationManager
@inject IDialogService Dialog
@@ -44,29 +43,29 @@
<table class="table table-sm table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Key ID</th>
<th>Name</th>
<th>Key Hash</th>
<th>Methods</th>
<th style="width: 160px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var key in FilteredKeys)
{
<tr @key="key.Id">
<td>@key.Id</td>
<tr @key="key.KeyId">
<td><code>@TruncateKeyId(key.KeyId)</code></td>
<td>
@key.Name
@if (!key.IsEnabled)
@if (!key.Enabled)
{
<span class="badge bg-secondary ms-1">Disabled</span>
}
</td>
<td><code>@MaskKeyValue(key.KeyHash)</code></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.Id}/edit")'>Edit</button>
@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"
@@ -75,7 +74,7 @@
<li>
<button class="dropdown-item"
@onclick="() => ToggleKey(key)">
@(key.IsEnabled ? "Disable" : "Enable")
@(key.Enabled ? "Disable" : "Enable")
</button>
</li>
<li><hr class="dropdown-divider" /></li>
@@ -98,14 +97,17 @@
</div>
@code {
private List<ApiKey> _keys = new();
// 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<ApiKey> FilteredKeys =>
private IEnumerable<InboundApiKeyInfo> FilteredKeys =>
string.IsNullOrWhiteSpace(_search)
? _keys
: _keys.Where(k =>
@@ -122,7 +124,7 @@
_errorMessage = null;
try
{
_keys = (await InboundApiRepository.GetAllApiKeysAsync()).ToList();
_keys = (await ApiKeyAdmin.ListAsync()).ToList();
}
catch (Exception ex)
{
@@ -131,20 +133,22 @@
_loading = false;
}
private static string MaskKeyValue(string keyValue)
// Show a short, recognizable prefix of the opaque KeyId rather than the full 32-char value.
private static string TruncateKeyId(string keyId)
{
if (keyValue.Length <= 8) return new string('*', keyValue.Length);
return keyValue[..4] + new string('*', keyValue.Length - 8) + keyValue[^4..];
if (string.IsNullOrEmpty(keyId)) return keyId;
return keyId.Length <= 12 ? keyId : keyId[..12] + "…";
}
private async Task ToggleKey(ApiKey key)
private async Task ToggleKey(InboundApiKeyInfo key)
{
try
{
key.IsEnabled = !key.IsEnabled;
await InboundApiRepository.UpdateApiKeyAsync(key);
await InboundApiRepository.SaveChangesAsync();
_toast.ShowSuccess($"API key '{key.Name}' {(key.IsEnabled ? "enabled" : "disabled")}.");
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)
{
@@ -152,7 +156,7 @@
}
}
private async Task DeleteKey(ApiKey key)
private async Task DeleteKey(InboundApiKeyInfo key)
{
var confirmed = await Dialog.ConfirmAsync(
"Delete API Key",
@@ -162,8 +166,7 @@
try
{
await InboundApiRepository.DeleteApiKeyAsync(key.Id);
await InboundApiRepository.SaveChangesAsync();
await ApiKeyAdmin.DeleteAsync(key.KeyId);
_toast.ShowSuccess($"API key '{key.Name}' deleted.");
await LoadDataAsync();
}