a simpler UI
This commit is contained in:
parent
84160f0759
commit
2263311e1b
4 changed files with 122 additions and 5 deletions
|
|
@ -101,4 +101,80 @@ public class LoginPageViewModelTests
|
|||
var vm = new LoginPageViewModel(settings);
|
||||
Assert.False(vm.ConfigMissing);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://yavsc.example.com/", "https://yavsc.example.com/.well-known/openid-configuration")]
|
||||
[InlineData("https://yavsc.example.com", "https://yavsc.example.com/.well-known/openid-configuration")]
|
||||
[InlineData("https://yavsc.example.com/sub/", "https://yavsc.example.com/sub/.well-known/openid-configuration")]
|
||||
public void DiscoveryUrl_is_externalurl_plus_well_known(string authority, string expected)
|
||||
{
|
||||
var settings = new PostIt.Settings
|
||||
{
|
||||
Authentication = new AuthenticationSettings { Authority = authority }
|
||||
};
|
||||
var vm = new LoginPageViewModel(settings);
|
||||
Assert.Equal(expected, vm.DiscoveryUrl);
|
||||
// ExternalUrl is the slash-normalised form of Authority.
|
||||
Assert.Equal(expected[..expected.LastIndexOf("/.well-known/openid-configuration")], vm.ExternalUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoveryUrl_is_empty_when_authority_is_unset()
|
||||
{
|
||||
var vm = new LoginPageViewModel(new PostIt.Settings());
|
||||
Assert.Equal(string.Empty, vm.DiscoveryUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoginAsync_failure_message_includes_discovery_url()
|
||||
{
|
||||
// Arrange: settings point at an unreachable authority; the test
|
||||
// browser throws synchronously to guarantee the catch branch runs.
|
||||
var settings = new PostIt.Settings
|
||||
{
|
||||
Authentication = new AuthenticationSettings
|
||||
{
|
||||
Authority = "https://does-not-exist.invalid/",
|
||||
ClientId = "postit-tests"
|
||||
},
|
||||
RedirectUri = "http://127.0.0.1:7890/",
|
||||
Scopes = new[] { "openid" }
|
||||
};
|
||||
|
||||
var vm = new LoginPageViewModel(settings, () => throw new InvalidOperationException("boom"));
|
||||
|
||||
// Act
|
||||
await vm.LoginAsync();
|
||||
|
||||
// Assert: the surfaced error mentions the canonical discovery URL,
|
||||
// so it can be copy-pasted into a browser to diagnose reachability.
|
||||
Assert.NotNull(vm.StatusMessage);
|
||||
Assert.StartsWith("Error:", vm.StatusMessage);
|
||||
Assert.Contains(
|
||||
"https://does-not-exist.invalid/.well-known/openid-configuration",
|
||||
vm.StatusMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoginAsync_reports_discovery_url_when_no_browser_available()
|
||||
{
|
||||
var settings = new PostIt.Settings
|
||||
{
|
||||
Authentication = new AuthenticationSettings
|
||||
{
|
||||
Authority = "https://yavsc.example.com/",
|
||||
ClientId = "postit-tests"
|
||||
},
|
||||
RedirectUri = "http://127.0.0.1:7890/",
|
||||
Scopes = new[] { "openid" }
|
||||
};
|
||||
|
||||
var vm = new LoginPageViewModel(settings, () => null);
|
||||
|
||||
await vm.LoginAsync();
|
||||
|
||||
Assert.Contains(
|
||||
"https://yavsc.example.com/.well-known/openid-configuration",
|
||||
vm.StatusMessage);
|
||||
}
|
||||
}
|
||||
|
|
@ -59,11 +59,20 @@ public partial class Settings : ObservableObject
|
|||
/// (no client secret). The browser implementation should be supplied
|
||||
/// per-platform by the caller.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AuthenticationSettings.Authority"/> is normalised by
|
||||
/// trimming any trailing slash before being handed to <c>OidcClient</c>.
|
||||
/// <c>OidcClient</c> derives the discovery URL from
|
||||
/// <c>Authority + "/.well-known/openid-configuration"</c>; leaving a
|
||||
/// trailing slash in place would produce a double-slash URL that some
|
||||
/// servers reject with 404.
|
||||
/// </remarks>
|
||||
internal OidcClientOptions GetOidcClientOptions(IdentityModel.OidcClient.Browser.IBrowser? browser = null)
|
||||
{
|
||||
var authority = Authentication.Authority?.TrimEnd('/') ?? string.Empty;
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = Authentication.Authority,
|
||||
Authority = authority,
|
||||
ClientId = Authentication.ClientId,
|
||||
RedirectUri = RedirectUri,
|
||||
Scope = string.Join(' ', this.Scopes),
|
||||
|
|
|
|||
|
|
@ -31,6 +31,23 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
public bool HasRegisterUrl => !string.IsNullOrEmpty(RegisterUrl);
|
||||
public bool HasForgotPasswordUrl => !string.IsNullOrEmpty(ForgotPasswordUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Canonical <see cref="Settings.Authentication"/> authority with any trailing
|
||||
/// slash removed. Used as the base for both the OIDC discovery URL and the
|
||||
/// human-facing Account URLs (Register / Forgot password). Empty when the
|
||||
/// authority is not configured.
|
||||
/// </summary>
|
||||
public string ExternalUrl => BuildExternalUrl(string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// OIDC discovery URL the client actually calls during login:
|
||||
/// <c>ExternalUrl + "/.well-known/openid-configuration"</c>. Surfaced in
|
||||
/// <see cref="StatusMessage"/> on failure so the operator can copy it
|
||||
/// verbatim and verify reachability from a browser.
|
||||
/// </summary>
|
||||
public string DiscoveryUrl =>
|
||||
string.IsNullOrEmpty(ExternalUrl) ? string.Empty : ExternalUrl + "/.well-known/openid-configuration";
|
||||
|
||||
/// <summary>
|
||||
/// True when the settings file is missing or <c>Authentication.Authority</c>
|
||||
/// is empty. The LoginPage surfaces a banner in that case and disables
|
||||
|
|
@ -112,12 +129,21 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
? Platform.DefaultRedirectUri
|
||||
: Settings.RedirectUri;
|
||||
|
||||
// Surface the discovery URL the client is about to call, so a
|
||||
// failure (DNS, TLS, 404) can be diagnosed by pasting the URL
|
||||
// straight into a browser. OidcClient computes the discovery
|
||||
// URL as `Authority + /.well-known/openid-configuration`; we
|
||||
// normalise the trailing slash here so the printed URL is
|
||||
// exactly what IdentityModel will fetch.
|
||||
if (!string.IsNullOrEmpty(DiscoveryUrl))
|
||||
StatusMessage = $"Discovering {DiscoveryUrl}";
|
||||
|
||||
var browser = BrowserFactoryOverride is not null
|
||||
? BrowserFactoryOverride.Invoke()
|
||||
: Platform.CreateBrowser?.Invoke();
|
||||
if (browser is null)
|
||||
{
|
||||
StatusMessage = "No browser is available on this platform.";
|
||||
StatusMessage = $"No browser is available on this platform. (discovery: {DiscoveryUrl})";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +152,7 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
|
||||
if (loginResult.IsError)
|
||||
{
|
||||
StatusMessage = loginResult.Error;
|
||||
StatusMessage = $"{loginResult.Error} (discovery: {DiscoveryUrl})";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +170,8 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
catch (Exception ex)
|
||||
{
|
||||
this.IsBusy = false;
|
||||
StatusMessage = "Error: "+ex.Message;
|
||||
var suffix = !string.IsNullOrEmpty(DiscoveryUrl) ? $" (discovery: {DiscoveryUrl})" : string.Empty;
|
||||
StatusMessage = $"Error: {ex.Message}{suffix}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,12 @@
|
|||
IsEnabled="{Binding HasForgotPasswordUrl}"
|
||||
Click="OnForgotPasswordClick"/>
|
||||
|
||||
<TextBlock Name="StatusText" Text="{Binding StatusMessage}" TextWrapping="Wrap" />
|
||||
<TextBox Name="StatusText"
|
||||
Text="{Binding StatusMessage}"
|
||||
TextWrapping="Wrap"
|
||||
IsReadOnly="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"/>
|
||||
<ProgressBar IsIndeterminate="{Binding IsBusy}" />
|
||||
|
||||
</StackPanel>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue