diff --git a/src/AprsMap.Client.Remote/RemoteEngineService.cs b/src/AprsMap.Client.Remote/RemoteEngineService.cs index 081abc5..7b12fc6 100644 --- a/src/AprsMap.Client.Remote/RemoteEngineService.cs +++ b/src/AprsMap.Client.Remote/RemoteEngineService.cs @@ -566,6 +566,15 @@ public Task SaveApiKeyAsync(string key, CancellationToken cancella ); } + /// + public Task CheckApiKeyAsync(string key, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(key); + return WithEngineErrors(() => + _connection.InvokeAsync(EngineHub.CheckApiKey, key, cancellationToken) + ); + } + /// public Task RemoveApiKeyAsync(CancellationToken cancellationToken = default) => WithEngineErrors(() => _connection.InvokeAsync(EngineHub.RemoveApiKey, cancellationToken)); diff --git a/src/AprsMap.Contracts/EngineHub.cs b/src/AprsMap.Contracts/EngineHub.cs index 32b7562..d4ba26b 100644 --- a/src/AprsMap.Contracts/EngineHub.cs +++ b/src/AprsMap.Contracts/EngineHub.cs @@ -238,6 +238,9 @@ public static class EngineHub /// Hub method for . public const string SaveApiKey = "SaveApiKey"; + /// Hub method for . + public const string CheckApiKey = "CheckApiKey"; + /// Hub method for . public const string RemoveApiKey = "RemoveApiKey"; diff --git a/src/AprsMap.Contracts/IEngineService.cs b/src/AprsMap.Contracts/IEngineService.cs index 2d4917a..3170da0 100644 --- a/src/AprsMap.Contracts/IEngineService.cs +++ b/src/AprsMap.Contracts/IEngineService.cs @@ -504,6 +504,14 @@ Task GetDestinationAsync( /// As for . Task SaveApiKeyAsync(string key, CancellationToken cancellationToken = default); + /// + /// Whether is the gateway's current API key (#447), saving nothing: + /// when it is, when it isn't, and + /// when no key is saved. The operator's only. + /// + /// As for . + Task CheckApiKeyAsync(string key, CancellationToken cancellationToken = default); + /// Removes the API key: API access is off (behavior 1). /// As for . Task RemoveApiKeyAsync(CancellationToken cancellationToken = default); diff --git a/src/AprsMap.Core/Engine.ApiKey.cs b/src/AprsMap.Core/Engine.ApiKey.cs index 0352069..732da9e 100644 --- a/src/AprsMap.Core/Engine.ApiKey.cs +++ b/src/AprsMap.Core/Engine.ApiKey.cs @@ -21,6 +21,10 @@ public Task GenerateApiKeyAsync(CancellationToken cancellationToken = de public Task SaveApiKeyAsync(string key, CancellationToken cancellationToken = default) => Task.FromException(NotHere()); + /// + public Task CheckApiKeyAsync(string key, CancellationToken cancellationToken = default) => + Task.FromException(NotHere()); + /// public Task RemoveApiKeyAsync(CancellationToken cancellationToken = default) => Task.FromException(NotHere()); diff --git a/src/AprsMap.Server/EngineHubEndpoint.cs b/src/AprsMap.Server/EngineHubEndpoint.cs index 92c6873..652cdcf 100644 --- a/src/AprsMap.Server/EngineHubEndpoint.cs +++ b/src/AprsMap.Server/EngineHubEndpoint.cs @@ -255,6 +255,17 @@ public ApiKeyStatus SaveApiKey(string key) } } + /// Whether a key is the current one (#447), for the operator checking a key an app has; saves nothing. + [HubMethodName(EngineHub.CheckApiKey)] + public ApiKeyCheck CheckApiKey(string key) + { + RequireOperator(); + ArgumentNullException.ThrowIfNull(key); + return !apiKeys.Status.Set ? ApiKeyCheck.None + : apiKeys.Check(key.Trim()) is not null ? ApiKeyCheck.Accepted + : ApiKeyCheck.Refused; + } + [HubMethodName(EngineHub.RemoveApiKey)] public void RemoveApiKey() { diff --git a/src/AprsMap.UI/Localization/Strings.resx b/src/AprsMap.UI/Localization/Strings.resx index d66cfc4..cc5cf93 100644 --- a/src/AprsMap.UI/Localization/Strings.resx +++ b/src/AprsMap.UI/Localization/Strings.resx @@ -2598,6 +2598,18 @@ In a Docker container: Let apps in with an API key + + Test + + + That's the gateway's current key. + + + That isn't the gateway's current key. Apps using it can only watch. + + + No key is saved, so apps can only watch. + Saved. Apps using the old key are disconnected and can only watch until they have this one. diff --git a/src/AprsMap.UI/Settings/GatewayAccessSectionViewModel.cs b/src/AprsMap.UI/Settings/GatewayAccessSectionViewModel.cs index a0646b4..410584f 100644 --- a/src/AprsMap.UI/Settings/GatewayAccessSectionViewModel.cs +++ b/src/AprsMap.UI/Settings/GatewayAccessSectionViewModel.cs @@ -32,7 +32,7 @@ internal GatewayAccessSectionViewModel(IEngineService engine, Localizer text, Ti /// The key being set: generated, typed or pasted; empty once it's saved. [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(ApplyCommand), nameof(CopyCommand))] + [NotifyCanExecuteChangedFor(nameof(ApplyCommand), nameof(CopyCommand), nameof(TestCommand))] public partial string Key { get; set; } = string.Empty; /// Apps may connect with an API key; turned off and applied, the saved key is removed. @@ -112,6 +112,25 @@ private async Task GenerateAsync() private bool HasKey => Key.Length > 0; + /// + /// Tests the key in the field against the gateway's current one, saving nothing (#447): the way to check a key an app + /// or a password manager has, since the gateway keeps only its hash. + /// + [RelayCommand(CanExecute = nameof(HasKey))] + private async Task TestAsync() + { + try + { + var check = await _engine.CheckApiKeyAsync(Key.Trim()); + HasProblem = check != ApiKeyCheck.Accepted; + Message = _text[$"GatewayAccess.Test.{check}"]; + } + catch (Exception ex) when (ex is UnauthorizedAccessException or InvalidOperationException) + { + Problem(ex); + } + } + /// Copies the key in the field: the only time it can be seen (behavior 6). [RelayCommand(CanExecute = nameof(HasKey))] private Task CopyAsync() => CopyToClipboard?.Invoke(Key) ?? Task.CompletedTask; diff --git a/src/AprsMap.UI/Views/SettingsView.axaml b/src/AprsMap.UI/Views/SettingsView.axaml index 5b4f661..776c20b 100644 --- a/src/AprsMap.UI/Views/SettingsView.axaml +++ b/src/AprsMap.UI/Views/SettingsView.axaml @@ -2011,12 +2011,19 @@ /> -