refactoring the login

This commit is contained in:
Paul Schneider 2026-06-20 15:01:03 +01:00
commit 2fd799c09f
13 changed files with 129 additions and 196 deletions

1
.gitignore vendored
View file

@ -23,6 +23,7 @@ package-lock.json
data/ data/
appsettings.*.json appsettings.*.json
appsettings-*.*.json appsettings-*.*.json
*-settings.json
generated/ generated/
*.tmp *.tmp

84
.vscode/launch.json vendored
View file

@ -5,97 +5,17 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": ".NET API", "name": "API",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/src/Api/bin/Debug/net10.0/Api.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Api",
"serverReadyAction": {
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"envFile": "${workspaceFolder}/src/Api/.env"
},
{
"name": "C#: API Debug",
"type": "dotnet", "type": "dotnet",
"request": "launch", "request": "launch",
"projectPath": "${workspaceFolder}/src/Api/Api.csproj" "projectPath": "${workspaceFolder}/src/Api/Api.csproj"
}, },
{ {
"name": "Blogs", "name": "Yavsc.Org",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/Yavsc.Blogs/bin/Debug/net10.0/Yavsc.Blogs.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Yavsc.Blogs/",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
{
"name": "C#: Yavsc.Org Debug",
"type": "dotnet", "type": "dotnet",
"request": "launch", "request": "launch",
"projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj" "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj"
}, },
{
"name": "Web",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-web",
"program": "${workspaceFolder}/src/Yavsc.Web/bin/Debug/net10.0/Yavsc.Web.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Yavsc.Web",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
{
"name": "cli",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/cli/bin/Debug/net10.0/cli.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": "Web",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/Yavsc.Web/bin/Debug/net10.0/Yavsc.Web.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Yavsc.Web",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/src/Yavsc.Web/Views"
}
},
{ {
"name": "PostIt", "name": "PostIt",
"type": "dotnet", "type": "dotnet",

View file

@ -13,6 +13,7 @@
"envsubst", "envsubst",
"Newtonsoft", "Newtonsoft",
"Npgsql", "Npgsql",
"postit",
"pschneider", "pschneider",
"SLNDIR", "SLNDIR",
"validable", "validable",

View file

@ -22,8 +22,8 @@
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" /> <ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="settings.json"> <Content Include="postit-settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -12,7 +12,7 @@ namespace PostIt;
public partial class Settings : ObservableObject public partial class Settings : ObservableObject
{ {
const string SettingsFileName = "settings.json"; const string SettingsFileName = "postit-settings.json";
IStorageFolder? folder = null; IStorageFolder? folder = null;
[ObservableProperty] [ObservableProperty]
@ -42,8 +42,13 @@ public partial class Settings : ObservableObject
internal async Task Load() internal async Task Load()
{ {
String configPath = string configDir = Path.Combine(
Path.Combine(AppContext.BaseDirectory, SettingsFileName); Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"PostIt"
);
Directory.CreateDirectory(configDir);
string configPath = Path.Combine(configDir, SettingsFileName);
FileInfo configFileInfo = new FileInfo(configPath); FileInfo configFileInfo = new FileInfo(configPath);

View file

@ -27,6 +27,7 @@ public class ViewLocator : IDataTemplate
{ {
MainPageViewModel => _services.GetRequiredService<MainPage>(), MainPageViewModel => _services.GetRequiredService<MainPage>(),
SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(), SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(),
LoginPageViewModel => _services.GetRequiredService<LoginPage>(),
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" } _ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
}; };
} }

View file

@ -0,0 +1,67 @@
using CommunityToolkit.Mvvm.Input;
using IdentityModel.OidcClient;
using System;
using System.Threading.Tasks;
namespace PostIt.ViewModels;
public partial class LoginPageViewModel : ViewModelBase
{
public string UserEmail { get; set; }
public string Password { get; set; }
private string _AccessToken;
public string AccessToken { get => _AccessToken; private set => this.SetProperty(ref _AccessToken, value); }
public bool RememberMe { get; set; }
public override bool CanNavigateNext { get => false; protected set => throw new System.NotImplementedException(); }
public override bool CanNavigatePrevious { get => true; protected set => throw new System.NotImplementedException(); }
public Settings Settings { get; }
private string _StatusMessage;
public string StatusMessage { get => _StatusMessage; private set => this.SetProperty(ref _StatusMessage, value); }
private bool _IsBusy;
public bool IsBusy { get=> _IsBusy; private set=> this.SetProperty(ref _IsBusy, value); }
public LoginPageViewModel()
{
Settings = new Settings();
StatusMessage = "Ready";
}
[RelayCommand]
public async Task LoginAsync()
{
try
{
Settings.Load().Wait();
var client = new OidcClient(Settings.GetOidcClientOptions());
var loginResult = await client.LoginAsync(new LoginRequest());
if (loginResult.IsError)
{
StatusMessage = loginResult.Error;
return;
}
StatusMessage = "Interactive token acquired.";
this.IsBusy = false;
AccessToken = loginResult.AccessToken;
/* TODO save or not user log and password
await Task.Run(() =>
{
Settings.Save().Wait();
});*/
}
catch (Exception ex)
{
this.IsBusy = false;
StatusMessage = "Error: "+ex.Message;
}
}
}

View file

@ -107,44 +107,6 @@ public partial class MainPageViewModel : ViewModelBase
ApplyFilter(); ApplyFilter();
} }
[RelayCommand]
internal async Task Login()
{
await ExecuteAsync(async () =>
{
// Try interactive OIDC login first
try
{
var loginWin = new PostIt.Views.LoginPage();
var result = await loginWin.StartLoginAsync(Settings.GetOidcClientOptions());
if (result is not null && !result.IsError && !string.IsNullOrWhiteSpace(result.AccessToken))
{
BearerToken = result.AccessToken;
StatusMessage = "Interactive token acquired.";
return;
}
}
catch
{
// ignore and fallback to client credentials
}
// Fallback to client credentials if interactive fails
var tokenResponse = await RequestClientCredentialsTokenAsync();
if (string.IsNullOrWhiteSpace(tokenResponse.AccessToken))
{
throw new InvalidOperationException("Failed to acquire a token using client credentials.");
}
BearerToken = tokenResponse.AccessToken;
StatusMessage = string.IsNullOrWhiteSpace(tokenResponse.Error)
? "Bearer token acquired (client credentials)."
: $"Token acquired with warning: {tokenResponse.ErrorDescription}";
});
}
[RelayCommand] [RelayCommand]
internal async Task Save() internal async Task Save()
{ {

View file

@ -1,37 +1,39 @@
<ContentPage xmlns="https://github.com/avaloniaui" <ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PostIt.Views.LoginPage" xmlns:vm="using:PostIt.ViewModels"
Header="Login"> x:Class="PostIt.Views.LoginPage"
x:DataType="vm:LoginPageViewModel"
Header="Login">
<StackPanel HorizontalAlignment="Center" <StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Spacing="20"> Spacing="20">
<TextBlock Text="Sign In" <TextBlock Text="Sign In"
FontSize="24" FontSize="24"
HorizontalAlignment="Center"/> HorizontalAlignment="Center"/>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Email"/> <TextBlock Text="Email"/>
<TextBox Name="EmailBox" <TextBox Name="EmailBox"
PlaceholderText="Enter your email"/> PlaceholderText="Enter your email"/>
</StackPanel> </StackPanel>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Password"/> <TextBlock Text="Password"/>
<TextBox Name="PasswordBox" <TextBox Name="PasswordBox"
PlaceholderText="Enter your password" PlaceholderText="Enter your password"
PasswordChar="•"/> PasswordChar="•"/>
</StackPanel> </StackPanel>
<Button Content="Login" <Button Content="Login"
Click="OnLoginClick"/> Click="OnLoginClickAsync"/>
<Button Content="Cancel" <Button Content="Cancel"
Click="OnCancelClick"/> Click="OnCancelClick"/>
<TextBlock Name="StatusText" Text="Starting authentication..." /> <TextBlock Name="StatusText" Text="Starting authentication..." />
<ProgressBar IsIndeterminate="True" /> <ProgressBar IsIndeterminate="{Binding IsBusy}" />
</StackPanel> </StackPanel>
</ContentPage> </ContentPage>

View file

@ -10,6 +10,8 @@ namespace PostIt.Views;
public partial class LoginPage : ContentPage public partial class LoginPage : ContentPage
{ {
private TaskCompletionSource<LoginResult?> _tcs = new(); private TaskCompletionSource<LoginResult?> _tcs = new();
private LoginResult loginResult;
public Settings Settings { get; } public Settings Settings { get; }
@ -19,25 +21,22 @@ public partial class LoginPage : ContentPage
InitializeComponent(); InitializeComponent();
} }
public async Task<LoginResult?> StartLoginAsync(OidcClientOptions options) private async void OnLoginClickAsync(object? sender, RoutedEventArgs e)
{ {
// This is where you specify your actual login auth logic
try try
{ {
Settings.Load().Wait(); Settings.Load().Wait();
var client = new OidcClient(options); var client = new OidcClient(Settings.GetOidcClientOptions());
var loginResult = await client.LoginAsync(new LoginRequest()); var loginResult = await client.LoginAsync(new LoginRequest());
return loginResult; this.loginResult = loginResult;
} }
catch (Exception) catch (Exception)
{ {
return null; this.loginResult = null;
} }
}
private async void OnLoginClick(object? sender, RoutedEventArgs e)
{
// This is where you specify your actual login auth logic
if (Navigation is not null) if (Navigation is not null)
await Navigation.PopModalAsync(); await Navigation.PopModalAsync();
} }

View file

@ -8,7 +8,9 @@
xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
mc:Ignorable="d" mc:Ignorable="d"
x:Class="PostIt.Views.MainPage" x:Class="PostIt.Views.MainPage"
x:DataType="vm:MainPageViewModel" HorizontalAlignment="Center" VerticalAlignment="Center" > x:DataType="vm:MainPageViewModel"
HorizontalAlignment="Center"
VerticalAlignment="Center" >
<Design.DataContext> <Design.DataContext>
<vm:MainPageViewModel /> <vm:MainPageViewModel />
</Design.DataContext> </Design.DataContext>
@ -17,7 +19,6 @@
<TextBlock Text="PostIt Blog API Interface" FontSize="20" FontWeight="Bold" /> <TextBlock Text="PostIt Blog API Interface" FontSize="20" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Spacing="8" > <StackPanel Orientation="Horizontal" Spacing="8" >
<Button Command="{Binding Login}" Content="Acquire token" />
<Button Command="{Binding LoadPosts}" Content="Load posts" /> <Button Command="{Binding LoadPosts}" Content="Load posts" />
<Button Command="{Binding Search}" Content="Filter" /> <Button Command="{Binding Search}" Content="Filter" />
<Button Command="{Binding New}" Content="New post" /> <Button Command="{Binding New}" Content="New post" />

View file

@ -1,26 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
// Stocke le ViewModel de la page actuellement affichée
[ObservableProperty]
private object? _currentPage;
// Instances des pages pour éviter de les recréer à chaque fois (Optionnel)
private readonly HomePageViewModel _homePage = new();
private readonly SettingsPageViewModel _settingsPage = new();
public MainWindowViewModel()
{
// Définir la page de démarrage par défaut
CurrentPage = _homePage;
}
[RelayCommand]
private void NavigateToHome() => CurrentPage = _homePage;
[RelayCommand]
private void NavigateToSettings() => CurrentPage = _settingsPage;
}