WIP PostIt login
This commit is contained in:
parent
18ce58e84a
commit
f3a3b63595
13 changed files with 103 additions and 160 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
|
@ -31,33 +32,41 @@ public partial class App : Application
|
|||
{
|
||||
return;
|
||||
}
|
||||
var settings = new Settings();
|
||||
// Synchronous: Settings.Load is intentionally non-async so we
|
||||
// don't deadlock the Avalonia UI thread. .Wait() on an async
|
||||
// method would block here forever on the await inside the
|
||||
// file read.
|
||||
settings.Load();
|
||||
|
||||
var tokenStore = new TokenStore(System.IO.Path.Combine(
|
||||
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
|
||||
"PostIt", "tokens.json"));
|
||||
var client = new BlogApiClient(new YavscApiClient(settings, tokenStore));
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var blog = BuildBlogClient(out var settings);
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = new MainPageViewModel(blog, settings)
|
||||
DataContext = new MainPageViewModel(client, settings)
|
||||
};
|
||||
}
|
||||
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
|
||||
{
|
||||
singleViewFactoryApplicationLifetime.MainViewFactory = () =>
|
||||
{
|
||||
var blog = BuildBlogClient(out var settings);
|
||||
return new MainPage { DataContext = new MainPageViewModel(blog, settings) };
|
||||
return new MainPage { DataContext = new MainPageViewModel(client, settings) };
|
||||
};
|
||||
}
|
||||
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
||||
{
|
||||
var blog = BuildBlogClient(out var settings);
|
||||
singleViewPlatform.MainView = new MainPage
|
||||
{
|
||||
DataContext = new MainPageViewModel(blog, settings)
|
||||
DataContext = new MainPageViewModel(client, settings)
|
||||
};
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException("ApplicationLifetime not supported.");
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private bool TryHandOffCustomSchemeUrl()
|
||||
|
|
@ -90,19 +99,4 @@ public partial class App : Application
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the (Settings, BlogApiClient) pair used by all UI
|
||||
/// lifetimes. A single TokenStore is shared so a login performed
|
||||
/// by the LoginPage is observable to the MainPage (and vice-versa)
|
||||
/// without going through disk on every API call.
|
||||
/// </summary>
|
||||
private static BlogApiClient BuildBlogClient(out Settings settings)
|
||||
{
|
||||
settings = new Settings();
|
||||
try { settings.Load().GetAwaiter().GetResult(); } catch { /* fall back to embedded defaults */ }
|
||||
var tokenStore = new TokenStore(System.IO.Path.Combine(
|
||||
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
|
||||
"PostIt", "tokens.json"));
|
||||
return new BlogApiClient(new YavscApiClient(settings, tokenStore));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
|
|
@ -9,7 +8,6 @@ using PostIt.Services;
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
[assembly: InternalsVisibleTo("PostIt.Tests")]
|
||||
|
||||
|
|
@ -69,6 +67,7 @@ public partial class Settings : ObservableObject
|
|||
|
||||
[ObservableProperty]
|
||||
public partial string[] Scopes { get; set; }
|
||||
public bool Loaded { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Build OidcClient options configured for Authorization Code + PKCE
|
||||
|
|
@ -77,12 +76,14 @@ public partial class Settings : ObservableObject
|
|||
/// </summary>
|
||||
internal OidcClientOptions GetOidcClientOptions(IdentityModel.OidcClient.Browser.IBrowser? browser = null)
|
||||
{
|
||||
if (!Loaded) Load();
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = Authentication.Authority,
|
||||
ClientId = Authentication.ClientId,
|
||||
RedirectUri = RedirectUri,
|
||||
Scope = string.Join(' ', this.Scopes),
|
||||
TokenClientCredentialStyle = IdentityModel.Client.ClientCredentialStyle.PostBody
|
||||
// PKCE is enabled by default when no client_secret is provided.
|
||||
};
|
||||
|
||||
|
|
@ -92,8 +93,9 @@ public partial class Settings : ObservableObject
|
|||
return options;
|
||||
}
|
||||
|
||||
internal async Task Load()
|
||||
internal void Load()
|
||||
{
|
||||
if (Loaded) return;
|
||||
string configDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"PostIt"
|
||||
|
|
@ -123,10 +125,17 @@ public partial class Settings : ObservableObject
|
|||
|
||||
try
|
||||
{
|
||||
// Synchronous read on purpose: Settings.Load() is called from
|
||||
// synchronous startup paths (App.axaml.cs, ViewModel ctors,
|
||||
// tests) and bridging to async here with .Wait() / .GetAwaiter()
|
||||
// .GetResult() deadlocks the Avalonia UI thread because the
|
||||
// continuation can't resume on the same thread. The settings
|
||||
// file is a few KiB at most; async I/O gains nothing here.
|
||||
using var stream = configFileInfo.OpenRead();
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = await reader.ReadToEndAsync();
|
||||
var json = reader.ReadToEnd();
|
||||
ApplyJson(json, $"user file {configFileInfo.FullName}");
|
||||
Loaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -182,4 +191,4 @@ public partial class Settings : ObservableObject
|
|||
Console.Error.WriteLine($"🩎 Error applying settings from {source}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,8 +139,10 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
{
|
||||
// Load settings eagerly so RegisterUrl / ForgotPasswordUrl are
|
||||
// populated as soon as the page renders (XAML bindings fire
|
||||
// before the user clicks Login).
|
||||
try { Settings.Load().GetAwaiter().GetResult(); }
|
||||
// before the user clicks Login). Settings.Load is synchronous
|
||||
// on purpose; calling .GetAwaiter().GetResult() on it would
|
||||
// deadlock the UI thread on the await inside the file read.
|
||||
try { Settings.Load(); }
|
||||
catch { /* settings may be missing in tests/dev; LoginAsync will surface real errors */ }
|
||||
}
|
||||
|
||||
|
|
@ -173,7 +175,7 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
if (SettingsLoadOverride is not null)
|
||||
await SettingsLoadOverride().ConfigureAwait(false);
|
||||
else
|
||||
await Settings.Load().ConfigureAwait(false);
|
||||
Settings.Load();
|
||||
|
||||
// Guard: refuse to call OidcClient when the authority is
|
||||
// empty. IdentityModel would otherwise build a bogus
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@
|
|||
"offline_access",
|
||||
"blogs"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
"ClientId": "postit",
|
||||
"Authority": "https://yavsc.pschneider.fr/"
|
||||
},
|
||||
"RedirectUri": "postit://callback",
|
||||
"DarkMode": true,
|
||||
"ApiUrl": "https://blogs.pschneider.fr/api/v1/",
|
||||
"Scopes": [
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
"offline_access",
|
||||
"blogs"
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue