@if (_saved)
{
@:API Key Created
}
else if (IsEditMode)
{
@:Edit API Key
}
else
{
@:Add API Key
}
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit Log
pre-filtered to this API key's inbound calls. Inbound audit rows record
the key Name as Actor and live on the ApiInbound channel. *@
@if (IsEditMode && !string.IsNullOrWhiteSpace(_formName))
{
Recent audit activity
}
@* Name is fixed on edit โ the seam has no rename. *@
@if (_allMethods.Count == 0)
{
No API methods configured.
Create one to grant access.
}
else
{
@foreach (var method in _allMethods.OrderBy(m => m.Name))
{
var checkboxId = $"method-access-{method.Id}";
ToggleMethod(method.Name, (bool)e.Value!)" />
}
Callers using this key can invoke any checked method. At least one is required.
}
@if (_formError != null)
{
@_formError
}
}
@code {
// Inbound-API key re-arch (C3): this form drives the IInboundApiKeyAdmin seam.
// Keys are identified by an opaque string KeyId; method access is a set of method
// NAMES (scopes) carried on the key, replacing the old ApiMethod.ApprovedApiKeyIds CSV.
// The list of all methods still comes from IInboundApiRepository (methods stay in SQL).
[Parameter] public string? KeyId { get; set; }
private bool IsEditMode => _editingKey != null;
private InboundApiKeyInfo? _editingKey;
private string _formName = string.Empty;
private string? _formError;
private string? _errorMessage;
private string? _newlyCreatedToken;
private string? _newlyCreatedKeyId;
private bool _loading = true;
private bool _saved;
private List _allMethods = new();
// Selection set is method NAMES (scopes), not method ids.
private HashSet _selectedMethodNames = new(StringComparer.Ordinal);
private ToastNotification _toast = default!;
protected override async Task OnInitializedAsync()
{
try
{
// Methods always come from SQL Server (methods stay on the repository).
_allMethods = (await InboundApiRepository.GetAllApiMethodsAsync()).ToList();
if (!string.IsNullOrWhiteSpace(KeyId))
{
// No single-key getter on the seam โ locate this key in the full list.
var all = await ApiKeyAdmin.ListAsync();
_editingKey = all.FirstOrDefault(k => string.Equals(k.KeyId, KeyId, StringComparison.Ordinal));
if (_editingKey == null)
{
_errorMessage = $"API key '{KeyId}' not found.";
}
else
{
_formName = _editingKey.Name;
var methods = await ApiKeyAdmin.GetMethodsForKeyAsync(KeyId);
_selectedMethodNames = new HashSet(methods, StringComparer.Ordinal);
}
}
}
catch (Exception ex)
{
_errorMessage = $"Failed to load API key: {ex.Message}";
}
_loading = false;
}
private async Task SaveKey()
{
_formError = null;
if (!IsEditMode && string.IsNullOrWhiteSpace(_formName))
{
_formError = "Name is required.";
return;
}
// The seam/server reject empty scope sets; validate in the UI for a clear message.
if (_selectedMethodNames.Count == 0)
{
_formError = "Select at least one API method for this key.";
return;
}
try
{
if (_editingKey != null)
{
// Edit: name is fixed; only the method-scope set is mutable.
var ok = await ApiKeyAdmin.SetMethodsAsync(_editingKey.KeyId, _selectedMethodNames.ToList());
if (!ok)
{
_formError = $"API key '{_editingKey.Name}' was not found. Reload and retry.";
return;
}
NavigationManager.NavigateTo("/admin/api-keys");
}
else
{
var created = await ApiKeyAdmin.CreateAsync(_formName.Trim(), _selectedMethodNames.ToList());
_newlyCreatedKeyId = created.KeyId;
_newlyCreatedToken = created.Token; // shown once; never persisted client-side.
_saved = true;
}
}
catch (Exception ex)
{
_formError = $"Save failed: {ex.Message}";
}
}
private void GoBack() => NavigationManager.NavigateTo("/admin/api-keys");
private void ToggleMethod(string methodName, bool isChecked)
{
if (isChecked) _selectedMethodNames.Add(methodName);
else _selectedMethodNames.Remove(methodName);
}
private async Task CopyKeyToClipboard()
{
if (_newlyCreatedToken == null) return;
try
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", _newlyCreatedToken);
_toast.ShowSuccess("Copied to clipboard.");
}
catch
{
_toast.ShowError("Copy failed.");
}
}
}