From 733679a376f93872ae7e2a07248e04b513ba636c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 13 May 2026 13:41:13 -0400 Subject: [PATCH] feat(ui/api-keys): grant API method access on edit page Admins can now check/uncheck which API methods this key is approved to invoke directly on /admin/api-keys/{id}/edit, instead of having to bounce through the Design role's API method editor. Membership is diffed against the initial state and applied by mutating ApprovedApiKeyIds on each affected ApiMethod in the same SaveChangesAsync. --- .../Components/Pages/Admin/ApiKeyForm.razor | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/ScadaLink.CentralUI/Components/Pages/Admin/ApiKeyForm.razor b/src/ScadaLink.CentralUI/Components/Pages/Admin/ApiKeyForm.razor index 3e2dbd6..b47872d 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Admin/ApiKeyForm.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Admin/ApiKeyForm.razor @@ -57,6 +57,37 @@ + @if (IsEditMode) + { +
+ + @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}"; +
+ + +
+ } +
+
+ Callers using this key can invoke any checked method. +
+ } +
+ } @if (_formError != null) {
@_formError
@@ -81,6 +112,10 @@ private bool _loading = true; private bool _saved; + private List _allMethods = new(); + private HashSet _initialMethodIds = new(); + private HashSet _selectedMethodIds = new(); + private ToastNotification _toast = default!; protected override async Task OnInitializedAsync() @@ -97,6 +132,12 @@ else { _formName = _editingKey.Name; + _allMethods = (await InboundApiRepository.GetAllApiMethodsAsync()).ToList(); + _initialMethodIds = _allMethods + .Where(m => ParseApprovedKeyIds(m.ApprovedApiKeyIds).Contains(_editingKey.Id)) + .Select(m => m.Id) + .ToHashSet(); + _selectedMethodIds = new HashSet(_initialMethodIds); } } } @@ -118,6 +159,22 @@ { _editingKey.Name = _formName.Trim(); await InboundApiRepository.UpdateApiKeyAsync(_editingKey); + + var changedIds = _selectedMethodIds + .Except(_initialMethodIds) + .Concat(_initialMethodIds.Except(_selectedMethodIds)) + .ToHashSet(); + foreach (var method in _allMethods.Where(m => changedIds.Contains(m.Id))) + { + var ids = ParseApprovedKeyIds(method.ApprovedApiKeyIds); + if (_selectedMethodIds.Contains(method.Id)) ids.Add(_editingKey.Id); + else ids.Remove(_editingKey.Id); + method.ApprovedApiKeyIds = ids.Count == 0 + ? null + : string.Join(",", ids.OrderBy(x => x)); + await InboundApiRepository.UpdateApiMethodAsync(method); + } + await InboundApiRepository.SaveChangesAsync(); NavigationManager.NavigateTo("/admin/api-keys"); } @@ -139,6 +196,22 @@ private void GoBack() => NavigationManager.NavigateTo("/admin/api-keys"); + private void ToggleMethod(int methodId, bool isChecked) + { + if (isChecked) _selectedMethodIds.Add(methodId); + else _selectedMethodIds.Remove(methodId); + } + + private static HashSet ParseApprovedKeyIds(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return new HashSet(); + return value.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(s => int.TryParse(s.Trim(), out var id) ? id : -1) + .Where(id => id > 0) + .ToHashSet(); + } + private async Task CopyKeyToClipboard() { if (_newlyCreatedKeyValue == null) return;