Identity reloaded

This commit is contained in:
Paul Schneider 2026-07-05 23:56:10 +01:00
commit 333b066e66
33 changed files with 195 additions and 180 deletions

View file

@ -12,7 +12,7 @@ namespace PostIt.Services;
/// The set is deliberately small: each value is a milestone an
/// operator can grep for in logs / StatusMessage, not a heartbeat.
/// </summary>
public enum OidcLoginPhase
public enum OIDCLoginPhase
{
/// <summary>No login in flight (or login has settled).</summary>
Idle,

View file

@ -91,15 +91,15 @@ public class YavscApiClient : IAsyncDisposable
/// <see cref="LoginPageViewModel.StatusMessage"/> for the human
/// text (URLs, error detail).</param>
public async Task LoginInteractiveAsync(
IProgress<OidcLoginPhase>? progress = null,
IProgress<OIDCLoginPhase>? progress = null,
CancellationToken ct = default)
{
progress?.Report(OidcLoginPhase.Discovering);
progress?.Report(OIDCLoginPhase.Discovering);
var browser = Platform.CreateBrowser?.Invoke();
if (browser is null)
{
progress?.Report(OidcLoginPhase.Error);
progress?.Report(OIDCLoginPhase.Error);
throw new InvalidOperationException("No browser is available on this platform.");
}
@ -114,20 +114,20 @@ public class YavscApiClient : IAsyncDisposable
// the moment we ask the browser to open (covers the entire
// user-driven window including the AwaitingCallback wait), and
// the moment we trade the code for tokens.
progress?.Report(OidcLoginPhase.OpeningBrowser);
progress?.Report(OIDCLoginPhase.OpeningBrowser);
var result = await client.LoginAsync(new LoginRequest(), ct).ConfigureAwait(false);
if (result.IsError)
{
progress?.Report(OidcLoginPhase.Error);
progress?.Report(OIDCLoginPhase.Error);
throw new InvalidOperationException($"OIDC login failed: {result.Error}");
}
progress?.Report(OidcLoginPhase.ExchangingCode);
progress?.Report(OIDCLoginPhase.ExchangingCode);
if (string.IsNullOrEmpty(result.RefreshToken))
{
progress?.Report(OidcLoginPhase.Error);
progress?.Report(OIDCLoginPhase.Error);
throw new InvalidOperationException(
"Missing refresh_token — vérifie le scope 'offline_access'.");
}
@ -139,7 +139,7 @@ public class YavscApiClient : IAsyncDisposable
IdToken: result.IdentityToken);
_store.Save(_tokens);
progress?.Report(OidcLoginPhase.Success);
progress?.Report(OIDCLoginPhase.Success);
}
/// <summary>
@ -152,7 +152,7 @@ public class YavscApiClient : IAsyncDisposable
/// phase and returns false so the UI can keep going.
/// </summary>
public async Task<bool> TrySilentLoginAsync(
IProgress<OidcLoginPhase>? progress = null,
IProgress<OIDCLoginPhase>? progress = null,
CancellationToken ct = default)
{
if (!HasValidSession) return false;
@ -161,7 +161,7 @@ public class YavscApiClient : IAsyncDisposable
// Access token still has plenty of life — nothing to do.
if (_tokens.AccessTokenExpiresAt - DateTimeOffset.UtcNow > RefreshSkew)
{
progress?.Report(OidcLoginPhase.Success);
progress?.Report(OIDCLoginPhase.Success);
return true;
}
@ -172,19 +172,19 @@ public class YavscApiClient : IAsyncDisposable
// the user back to the login page.
try
{
progress?.Report(OidcLoginPhase.ExchangingCode);
progress?.Report(OIDCLoginPhase.ExchangingCode);
await ForceRefreshAsync(ct).ConfigureAwait(false);
progress?.Report(OidcLoginPhase.Success);
progress?.Report(OIDCLoginPhase.Success);
return true;
}
catch (RefreshFailedException)
{
progress?.Report(OidcLoginPhase.Idle);
progress?.Report(OIDCLoginPhase.Idle);
return false;
}
catch
{
progress?.Report(OidcLoginPhase.Idle);
progress?.Report(OIDCLoginPhase.Idle);
return false;
}
}

View file

@ -105,8 +105,8 @@ public partial class LoginPageViewModel : ViewModelBase
/// callback hand-off: when AwaitingCallback never resolves,
/// the OS never re-launched PostIt with the postit:// URL.
/// </summary>
private OidcLoginPhase _phase = OidcLoginPhase.Idle;
public OidcLoginPhase Phase
private OIDCLoginPhase _phase = OIDCLoginPhase.Idle;
public OIDCLoginPhase Phase
{
get => _phase;
private set
@ -122,13 +122,13 @@ public partial class LoginPageViewModel : ViewModelBase
/// </summary>
public string PhaseLabel => _phase switch
{
OidcLoginPhase.Idle => "En attente",
OidcLoginPhase.Discovering => "Découverte OIDC…",
OidcLoginPhase.OpeningBrowser => "Ouverture du navigateur…",
OidcLoginPhase.AwaitingCallback => "En attente du callback postit://…",
OidcLoginPhase.ExchangingCode => "Échange du code contre les jetons…",
OidcLoginPhase.Success => "Connecté",
OidcLoginPhase.Error => "Erreur",
OIDCLoginPhase.Idle => "En attente",
OIDCLoginPhase.Discovering => "Découverte OIDC…",
OIDCLoginPhase.OpeningBrowser => "Ouverture du navigateur…",
OIDCLoginPhase.AwaitingCallback => "En attente du callback postit://…",
OIDCLoginPhase.ExchangingCode => "Échange du code contre les jetons…",
OIDCLoginPhase.Success => "Connecté",
OIDCLoginPhase.Error => "Erreur",
_ => _phase.ToString(),
};
@ -275,7 +275,7 @@ public partial class LoginPageViewModel : ViewModelBase
// The progress sink drives Phase / PhaseLabel; StatusMessage
// keeps the text detail (URLs, error messages). Same
// underlying flow, two views.
var progress = new Progress<OidcLoginPhase>(p => Phase = p);
var progress = new Progress<OIDCLoginPhase>(p => Phase = p);
await LoginInteractiveCoreAsync(_api, progress);
IsBusy = false;
@ -299,7 +299,7 @@ public partial class LoginPageViewModel : ViewModelBase
/// </summary>
private async Task LoginInteractiveCoreAsync(
YavscApiClient api,
IProgress<OidcLoginPhase>? progress = null)
IProgress<OIDCLoginPhase>? progress = null)
{
var original = Platform.CreateBrowser;
try