fix(auth): C3 review — surface seam not-found (no silent success), partial-reconcile-failure guidance, create validation order, concurrent-edit reconciler test

This commit is contained in:
Joseph Doherty
2026-06-02 04:46:32 -04:00
parent 107e524914
commit d1191fddf9
4 changed files with 68 additions and 7 deletions
@@ -170,6 +170,12 @@
{
_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)
{
@@ -182,13 +188,16 @@
if (_editingKey != null)
{
// Edit: name is fixed; only the method-scope set is mutable.
await ApiKeyAdmin.SetMethodsAsync(_editingKey.KeyId, _selectedMethodNames.ToList());
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
{
if (string.IsNullOrWhiteSpace(_formName)) { _formError = "Name is required."; return; }
var created = await ApiKeyAdmin.CreateAsync(_formName.Trim(), _selectedMethodNames.ToList());
_newlyCreatedKeyId = created.KeyId;
_newlyCreatedToken = created.Token; // shown once; never persisted client-side.
@@ -146,7 +146,13 @@
{
var newEnabled = !key.Enabled;
// The seam persists; there is no separate SaveChangesAsync.
await ApiKeyAdmin.SetEnabledAsync(key.KeyId, newEnabled);
var ok = await ApiKeyAdmin.SetEnabledAsync(key.KeyId, newEnabled);
if (!ok)
{
_toast.ShowError($"API key '{key.Name}' was not found — it may have been removed. Refreshing.");
await LoadDataAsync();
return;
}
_toast.ShowSuccess($"API key '{key.Name}' {(newEnabled ? "enabled" : "disabled")}.");
await LoadDataAsync();
}
@@ -166,7 +172,13 @@
try
{
await ApiKeyAdmin.DeleteAsync(key.KeyId);
var ok = await ApiKeyAdmin.DeleteAsync(key.KeyId);
if (!ok)
{
_toast.ShowError($"API key '{key.Name}' was not found — it may have been removed. Refreshing.");
await LoadDataAsync();
return;
}
_toast.ShowSuccess($"API key '{key.Name}' deleted.");
await LoadDataAsync();
}
@@ -331,9 +331,21 @@
return false;
}
foreach (var update in plan.Updates)
try
{
await ApiKeyAdmin.SetMethodsAsync(update.KeyId, update.NewMethods);
foreach (var update in plan.Updates)
{
var ok = await ApiKeyAdmin.SetMethodsAsync(update.KeyId, update.NewMethods);
if (!ok)
throw new InvalidOperationException(
$"Key '{NameFor(update.KeyId)}' was not found in the key store.");
}
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Method '{methodName}' was saved, but updating approved-key scopes failed partway: {ex.Message} " +
"Some keys may be partially updated — review them on the API Keys page and retry.", ex);
}
// Selection is now the baseline (matters if save is retried without reload).
@@ -341,6 +353,10 @@
return true;
}
// Returns the display name for a keyId if available from the loaded key list, else the id itself.
private string NameFor(string keyId) =>
_allKeys.FirstOrDefault(k => string.Equals(k.KeyId, keyId, StringComparison.Ordinal))?.Name ?? keyId;
private void GoBack() => NavigationManager.NavigateTo("/design/external-systems");
private void ToggleTestRunPanel() => _showTestRun = !_showTestRun;