Login settings
This commit is contained in:
parent
164bd928aa
commit
1a0556695c
37 changed files with 504 additions and 190 deletions
|
|
@ -1,23 +0,0 @@
|
|||
<Project>
|
||||
<!-- https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management -->
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Avalonia packages -->
|
||||
<!-- Important: keep version in sync! -->
|
||||
<PackageVersion Include="Avalonia" Version="12.0.4" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.4" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.4" />
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.0.4" />
|
||||
<PackageVersion Include="Avalonia.iOS" Version="12.0.4" />
|
||||
<PackageVersion Include="Avalonia.Browser" Version="12.0.4" />
|
||||
<PackageVersion Include="Avalonia.Android" Version="12.0.4" />
|
||||
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
|
||||
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.15" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<Solution>
|
||||
<Project Path="PostIt.Android/PostIt.Android.csproj">
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="PostIt.Browser/PostIt.Browser.csproj" />
|
||||
<Project Path="PostIt.Desktop/PostIt.Desktop.csproj" />
|
||||
<Project Path="PostIt.iOS/PostIt.iOS.csproj">
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="PostIt/PostIt.csproj" />
|
||||
</Solution>
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:PostIt"
|
||||
x:Class="PostIt.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
x:Class="PostIt.App">
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
|
@ -14,4 +12,4 @@
|
|||
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
||||
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
</Application>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,16 @@ using System.Linq;
|
|||
using Avalonia.Markup.Xaml;
|
||||
using PostIt.ViewModels;
|
||||
using PostIt.Views;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace PostIt;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
|
@ -39,4 +44,4 @@ public partial class App : Application
|
|||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@
|
|||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="IdentityModel.OidcClient" />
|
||||
|
||||
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ using System.Collections.Generic;
|
|||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel.OidcClient;
|
||||
using PostIt.Models;
|
||||
|
||||
namespace PostIt.Services;
|
||||
|
|
@ -14,8 +16,8 @@ public sealed class BlogApiClient : IDisposable
|
|||
private readonly HttpClient _httpClient;
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
public BlogApiClient(string baseUrl, string? bearerToken = null)
|
||||
: this(CreateHttpClient(baseUrl, bearerToken))
|
||||
public BlogApiClient(string baseUrl, string? accessToken = null)
|
||||
: this(CreateHttpClient(baseUrl, accessToken))
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -28,18 +30,13 @@ public sealed class BlogApiClient : IDisposable
|
|||
};
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient(string baseUrl, string? bearerToken)
|
||||
private static HttpClient CreateHttpClient(string baseUrl, string? accessToken)
|
||||
{
|
||||
var client = new HttpClient
|
||||
var client = new HttpClient { BaseAddress = new Uri(baseUrl) };
|
||||
if (!string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bearerToken))
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken.Trim());
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
|
|
|||
56
src/PostIt/PostIt/Services/LoopbackBrowser.cs
Normal file
56
src/PostIt/PostIt/Services/LoopbackBrowser.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel.OidcClient.Browser;
|
||||
|
||||
namespace PostIt.Services;
|
||||
|
||||
public class LoopbackBrowser : IBrowser
|
||||
{
|
||||
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!Uri.TryCreate(options.EndUrl, UriKind.Absolute, out var endUri))
|
||||
{
|
||||
return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = "Invalid end URL" };
|
||||
}
|
||||
|
||||
var prefix = endUri.GetLeftPart(UriPartial.Path);
|
||||
if (!prefix.EndsWith("/")) prefix += "/";
|
||||
|
||||
using var listener = new HttpListener();
|
||||
listener.Prefixes.Add(prefix);
|
||||
listener.Start();
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(options.StartUrl) { UseShellExecute = true });
|
||||
|
||||
var context = await listener.GetContextAsync().ConfigureAwait(false);
|
||||
var response = context.Response;
|
||||
var responseString = "<html><body>Authentication complete. You can close this window.</body></html>";
|
||||
var buffer = Encoding.UTF8.GetBytes(responseString);
|
||||
response.ContentLength64 = buffer.Length;
|
||||
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
response.OutputStream.Close();
|
||||
|
||||
var raw = context.Request.Url!.ToString();
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.Success,
|
||||
Response = raw
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = ex.Message };
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { listener.Stop(); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/PostIt/PostIt/Settings/AuthenticationSettings.cs
Normal file
17
src/PostIt/PostIt/Settings/AuthenticationSettings.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
|
||||
public partial class AuthenticationSettings : ObservableObject
|
||||
{
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Authority { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string ClientId { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string ClientSecret { get; set; }
|
||||
|
||||
|
||||
}
|
||||
84
src/PostIt/PostIt/Settings/Settings.cs
Normal file
84
src/PostIt/PostIt/Settings/Settings.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using IdentityModel.OidcClient;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PostIt;
|
||||
|
||||
public partial class Settings : ObservableObject
|
||||
{
|
||||
const string SettingsFileName = "settings.json";
|
||||
IStorageFolder? folder = null;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AuthenticationSettings Authentication { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool DarkMode { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string ApiUrl { get; set; } = "https://blogs.pschneider.fr/api/v1/";
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string[] Scopes { get; set; }
|
||||
|
||||
internal OidcClientOptions GetOidcClientOptions()
|
||||
{
|
||||
return new OidcClientOptions
|
||||
{
|
||||
Authority = Authentication.Authority,
|
||||
ClientId = Authentication.ClientId,
|
||||
ClientSecret = Authentication.ClientSecret,
|
||||
Scope = string.Join(' ', this.Scopes)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
internal async Task Load(IStorageProvider storageProvider)
|
||||
{
|
||||
var configFile = await storageProvider.TryGetFileFromPathAsync(
|
||||
Path.Combine(AppContext.BaseDirectory, SettingsFileName));
|
||||
|
||||
if (configFile is null)
|
||||
{
|
||||
Console.Error.WriteLine("🩎 No settings file found.");
|
||||
return; // no settings file
|
||||
}
|
||||
try {
|
||||
using var stream = await configFile.OpenReadAsync();
|
||||
|
||||
|
||||
using var reader = new StreamReader( stream);
|
||||
var json = await reader.ReadToEndAsync();
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
Console.Error.WriteLine("🩎 Settings file is empty.");
|
||||
return ;
|
||||
}
|
||||
|
||||
var settings = JsonSerializer.Deserialize<Settings>(json);
|
||||
|
||||
if (settings is null)
|
||||
{
|
||||
Console.Error.WriteLine("🩎 Settings file is invalid.");
|
||||
return ;
|
||||
}
|
||||
this.Authentication = settings.Authentication;
|
||||
this.DarkMode = settings.DarkMode;
|
||||
this.ApiUrl = settings.ApiUrl;
|
||||
this.Scopes = settings.Scopes;
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"🩎 Error loading settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -13,49 +11,50 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||
using CommunityToolkit.Mvvm.Input;
|
||||
using PostIt.Models;
|
||||
using PostIt.Services;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _authority = "https://localhost:5001";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _clientId = "postit";
|
||||
public partial string StatusMessage { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private string _clientSecret = "postit-secret";
|
||||
public partial string SearchText { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private string _scope = "blog";
|
||||
public partial string BearerToken { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private string _apiUrl = "http://localhost:5000";
|
||||
public partial ObservableCollection<BlogPost> Posts { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private string _searchText = string.Empty;
|
||||
public partial ObservableCollection<BlogPost> FilteredPosts { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _bearerToken;
|
||||
public partial BlogPost? SelectedPost{ get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<BlogPost> _posts = new();
|
||||
public partial bool IsBusy{ get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<BlogPost> _filteredPosts = new();
|
||||
|
||||
ThemeVariant themeVariant = ThemeVariant.Default;
|
||||
|
||||
[ObservableProperty]
|
||||
private BlogPost? _selectedPost;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = "Ready";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isBusy;
|
||||
public partial Settings Settings { get; private set; }
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
SearchText = string.Empty;
|
||||
Posts = new ObservableCollection<BlogPost>();
|
||||
FilteredPosts = new ObservableCollection<BlogPost>();
|
||||
SelectedPost = null;
|
||||
BearerToken = string.Empty;
|
||||
IsBusy = false;
|
||||
StatusMessage = "Ready";
|
||||
Settings = new Settings();
|
||||
}
|
||||
|
||||
partial void OnSearchTextChanged(string value)
|
||||
|
|
@ -74,7 +73,7 @@ public partial class MainViewModel : ViewModelBase
|
|||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task LoadPostsAsync()
|
||||
internal async Task LoadPosts()
|
||||
{
|
||||
await ExecuteAsync(async () =>
|
||||
{
|
||||
|
|
@ -92,26 +91,51 @@ public partial class MainViewModel : ViewModelBase
|
|||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Search()
|
||||
internal void Search()
|
||||
{
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task LoginAsync()
|
||||
internal async Task Login()
|
||||
{
|
||||
await ExecuteAsync(async () =>
|
||||
{
|
||||
// Try interactive OIDC login first
|
||||
try
|
||||
{
|
||||
|
||||
var loginWin = new PostIt.Views.LoginWindow();
|
||||
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."
|
||||
? "Bearer token acquired (client credentials)."
|
||||
: $"Token acquired with warning: {tokenResponse.ErrorDescription}";
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task SaveAsync()
|
||||
internal async Task Save()
|
||||
{
|
||||
if (SelectedPost is null)
|
||||
{
|
||||
|
|
@ -146,7 +170,7 @@ public partial class MainViewModel : ViewModelBase
|
|||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task DeleteAsync()
|
||||
internal async Task Delete()
|
||||
{
|
||||
if (SelectedPost is null || SelectedPost.Id == 0)
|
||||
{
|
||||
|
|
@ -165,7 +189,7 @@ public partial class MainViewModel : ViewModelBase
|
|||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void New()
|
||||
internal void New()
|
||||
{
|
||||
SelectedPost = new BlogPost
|
||||
{
|
||||
|
|
@ -178,6 +202,7 @@ public partial class MainViewModel : ViewModelBase
|
|||
StatusMessage = "New blog post ready.";
|
||||
}
|
||||
|
||||
|
||||
private async Task RefreshPostsAsync()
|
||||
{
|
||||
using var client = CreateClient();
|
||||
|
|
@ -198,6 +223,8 @@ public partial class MainViewModel : ViewModelBase
|
|||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
if (Posts is null) return;
|
||||
|
||||
var query = SearchText?.Trim();
|
||||
var filtered = string.IsNullOrWhiteSpace(query)
|
||||
? Posts.OrderByDescending(p => p.DateModified)
|
||||
|
|
@ -234,7 +261,7 @@ public partial class MainViewModel : ViewModelBase
|
|||
private async Task<TokenResponse> RequestClientCredentialsTokenAsync()
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
var discoveryUrl = Authority.TrimEnd('/') + "/.well-known/openid-configuration";
|
||||
var discoveryUrl = Settings.Authentication.Authority.TrimEnd('/') + "/.well-known/openid-configuration";
|
||||
var discoveryDocument = await client.GetFromJsonAsync<DiscoveryDocument>(discoveryUrl, JsonOptions);
|
||||
|
||||
if (discoveryDocument is null || string.IsNullOrWhiteSpace(discoveryDocument.TokenEndpoint))
|
||||
|
|
@ -247,9 +274,9 @@ public partial class MainViewModel : ViewModelBase
|
|||
Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "client_credentials",
|
||||
["client_id"] = ClientId,
|
||||
["client_secret"] = ClientSecret,
|
||||
["scope"] = Scope,
|
||||
["client_id"] = Settings.Authentication.ClientId,
|
||||
["client_secret"] = Settings.Authentication.ClientSecret,
|
||||
["scope"] = string.Join(' ', Settings.Scopes),
|
||||
})
|
||||
};
|
||||
|
||||
|
|
@ -278,7 +305,7 @@ public partial class MainViewModel : ViewModelBase
|
|||
};
|
||||
|
||||
private BlogApiClient CreateClient()
|
||||
=> new BlogApiClient(ApiUrl, BearerToken);
|
||||
=> new BlogApiClient(Settings.ApiUrl, BearerToken);
|
||||
|
||||
private void UpdateCommandStates()
|
||||
{
|
||||
|
|
|
|||
19
src/PostIt/PostIt/ViewModels/SettingsViewModel.cs
Normal file
19
src/PostIt/PostIt/ViewModels/SettingsViewModel.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public partial class SettingsViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool DarkMode { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Authority { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string ClientId { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string ClientSecret { get; set; }
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Avalonia.Styling;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public abstract class ViewModelBase : ObservableObject
|
||||
public abstract partial class ViewModelBase : ObservableObject
|
||||
{
|
||||
}
|
||||
|
|
|
|||
13
src/PostIt/PostIt/Views/LoginWindow.axaml
Normal file
13
src/PostIt/PostIt/Views/LoginWindow.axaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="PostIt.Views.LoginWindow"
|
||||
Width="480" Height="160" WindowStartupLocation="CenterOwner"
|
||||
Title="Login">
|
||||
<StackPanel Margin="12" Spacing="12">
|
||||
<TextBlock Name="StatusText" Text="Starting authentication..." />
|
||||
<ProgressBar IsIndeterminate="True" />
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Name="CancelButton" Content="Cancel" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
33
src/PostIt/PostIt/Views/LoginWindow.axaml.cs
Normal file
33
src/PostIt/PostIt/Views/LoginWindow.axaml.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using IdentityModel.OidcClient;
|
||||
using PostIt.Services;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class LoginWindow : Window
|
||||
{
|
||||
private TaskCompletionSource<LoginResult?> _tcs = new();
|
||||
|
||||
public LoginWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
CancelButton.Click += (_, __) => Close(null);
|
||||
}
|
||||
|
||||
public async Task<LoginResult?> StartLoginAsync(OidcClientOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new OidcClient(options);
|
||||
var loginResult = await client.LoginAsync(new LoginRequest());
|
||||
return loginResult;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,35 +16,16 @@
|
|||
<StackPanel Margin="12" Spacing="12">
|
||||
<TextBlock Text="PostIt Blog API Interface" FontSize="20" FontWeight="Bold" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,1*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Text="Authority" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Text="{Binding Authority, Mode=TwoWay}" PlaceholderText="https://localhost:5001" />
|
||||
|
||||
<TextBlock Grid.Row="1" Text="Client ID" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ClientId, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Text="Client secret" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding ClientSecret, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="3" Text="Scope" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Scope, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="4" Text="API URL" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding ApiUrl, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="5" Text="Bearer token" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding BearerToken, Mode=TwoWay}" PlaceholderText="Token for blog scope" />
|
||||
|
||||
<StackPanel Grid.Row="6" Grid.ColumnSpan="2" Orientation="Horizontal" Spacing="8">
|
||||
<Button Command="{Binding LoginCommand}" Content="Acquire token" />
|
||||
<Button Command="{Binding LoadPostsCommand}" Content="Load posts" />
|
||||
<Button Command="{Binding SearchCommand}" Content="Filter" />
|
||||
<Button Command="{Binding NewCommand}" Content="New post" />
|
||||
<Button Command="{Binding SaveCommand}" Content="Save" />
|
||||
<Button Command="{Binding DeleteCommand}" Content="Delete" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Command="{Binding Login}" Content="Acquire token" />
|
||||
<Button Command="{Binding LoadPosts}" Content="Load posts" />
|
||||
<Button Command="{Binding Search}" Content="Filter" />
|
||||
<Button Command="{Binding New}" Content="New post" />
|
||||
<Button Command="{Binding Save}" Content="Save" />
|
||||
<Button Command="{Binding Delete}" Content="Delete" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="2*,3*" RowDefinitions="*" ColumnSpacing="12">
|
||||
<Border BorderBrush="Gray" BorderThickness="1" Padding="8">
|
||||
<ListBox ItemsSource="{Binding FilteredPosts}" SelectedItem="{Binding SelectedPost, Mode=TwoWay}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ public partial class MainView : UserControl
|
|||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,9 +4,10 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="using:PostIt.Views"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="PostIt.Views.MainWindow"
|
||||
x:DataType="vm:MainViewModel"
|
||||
Icon="/Assets/yavsc-logo.ico"
|
||||
Title="PostIt">
|
||||
<views:MainView />
|
||||
Title="PostIt"
|
||||
>
|
||||
<views:MainView x:Name="MainView"/>
|
||||
</Window>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,35 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Styling;
|
||||
using PostIt.ViewModels;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
internal Settings Settings { get; private set; }
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
this.Settings = new Settings();
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is MainViewModel vm)
|
||||
{
|
||||
Task.Run(async () => await this.Settings.Load(this.StorageProvider)).Wait();
|
||||
this.RequestedThemeVariant = Settings.DarkMode ? ThemeVariant.Dark : ThemeVariant.Light;
|
||||
MainView.DataContext = new MainViewModel();
|
||||
vm.Settings.Load(this.StorageProvider).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
src/PostIt/PostIt/Views/SettingsView.axaml
Normal file
29
src/PostIt/PostIt/Views/SettingsView.axaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<Window
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="cl:avalonia.Controls"
|
||||
x:Class="PostIt.Views.SettingsView"
|
||||
xmlns:vm="using:PostIt.ViewModels"
|
||||
x:DataType="vm:SettingsViewModel"
|
||||
Title="Settings"
|
||||
Width="400"
|
||||
Height="300">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="Authority"/>
|
||||
<TextBox Grid.Row="1" x:Name="AuthorityTextBox" Text="{Binding Authority, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="ClientId"/>
|
||||
<TextBox Grid.Row="3" x:Name="ClientIdTextBox" Text="{Binding ClientId, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Text="ClientSecret"/>
|
||||
<TextBox Grid.Row="5" x:Name="ClientSecretTextBox" Text="{Binding ClientSecret, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
12
src/PostIt/PostIt/Views/SettingsView.axaml.cs
Normal file
12
src/PostIt/PostIt/Views/SettingsView.axaml.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace PostIt.Views;
|
||||
public partial class SettingsView: Window
|
||||
{
|
||||
public SettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
16
src/PostIt/PostIt/settings.json
Normal file
16
src/PostIt/PostIt/settings.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"Authentication": {
|
||||
"ClientId": "postit",
|
||||
"ClientSecret": "postit-secret",
|
||||
"Authority": "https://blogs.pschneider.fr/auth/realms/master",
|
||||
},
|
||||
"DarkMode": false,
|
||||
"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