From 1c2dc458036a752eeccc72985b07452909568160 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 13 May 2026 07:12:44 -0400 Subject: [PATCH] feat(ui/api-methods): pick approved API keys when editing a method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ApiMethod entity had an ApprovedApiKeyIds column and ApiKeyValidator read it, but no UI/CLI/seed code ever wrote to it. Result: any inbound POST /api/{method} was rejected with 403 "API key not approved for this method" regardless of which key was sent. Add an "Approved API Keys" subsection to the method form, between Timeout and Parameters: vertical list of checkboxes, one per ApiKey row (with a "Disabled" badge for disabled keys, and a link to /admin/api-keys when none exist). OnInitializedAsync loads all keys and parses the existing comma-separated IDs; Save() serializes the selected set back to the entity on both create and edit paths. Re-uses IInboundApiRepository.GetAllApiKeysAsync — no repo or migration changes needed. --- .../Pages/Design/ApiMethodForm.razor | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/ApiMethodForm.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/ApiMethodForm.razor index 039f253..414ee97 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/ApiMethodForm.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/ApiMethodForm.razor @@ -28,6 +28,40 @@ +
+ + @if (_allKeys.Count == 0) + { +
+ No API keys configured. + Create one to authorize callers for this method. +
+ } + else + { +
+ @foreach (var key in _allKeys) + { + var checkboxId = $"approved-key-{key.Id}"; +
+ + +
+ } +
+
+ Callers must present a checked key in the X-API-Key header to invoke this method. +
+ } +
(); private ApiMethod? _existing; + private List _allKeys = new(); + private HashSet _selectedKeyIds = new(); protected override async Task OnInitializedAsync() { + try + { + _allKeys = (await InboundApiRepository.GetAllApiKeysAsync()).ToList(); + } + catch (Exception ex) { _formError = ex.Message; } + if (Id.HasValue) { try @@ -92,6 +134,7 @@ _timeoutSeconds = _existing.TimeoutSeconds; _params = _existing.ParameterDefinitions; _returns = _existing.ReturnDefinition; + _selectedKeyIds = ParseApprovedKeyIds(_existing.ApprovedApiKeyIds); } } catch (Exception ex) { _formError = ex.Message; } @@ -99,6 +142,25 @@ _loading = false; } + 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 void ToggleKey(int keyId, bool isChecked) + { + if (isChecked) _selectedKeyIds.Add(keyId); + else _selectedKeyIds.Remove(keyId); + } + + private string? SerializeApprovedKeyIds() => + _selectedKeyIds.Count == 0 ? null : string.Join(",", _selectedKeyIds.OrderBy(id => id)); + private async Task Save() { _formError = null; @@ -110,12 +172,14 @@ try { + var approvedKeyIds = SerializeApprovedKeyIds(); if (_existing != null) { _existing.Script = _script; _existing.TimeoutSeconds = _timeoutSeconds; _existing.ParameterDefinitions = _params?.Trim(); _existing.ReturnDefinition = _returns?.Trim(); + _existing.ApprovedApiKeyIds = approvedKeyIds; await InboundApiRepository.UpdateApiMethodAsync(_existing); } else @@ -124,7 +188,8 @@ { TimeoutSeconds = _timeoutSeconds, ParameterDefinitions = _params?.Trim(), - ReturnDefinition = _returns?.Trim() + ReturnDefinition = _returns?.Trim(), + ApprovedApiKeyIds = approvedKeyIds }; await InboundApiRepository.AddApiMethodAsync(m); }