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"
|
||||
]
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ using Yavsc.Models.Blog;
|
|||
using Yavsc.Server.Exceptions;
|
||||
using Yavsc.Server.Helpers;
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
[Authorize("BlogScope")]
|
||||
[Produces("application/json")]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Blog;
|
||||
namespace Yavsc.Controllers
|
||||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
[Produces("application/json")]
|
||||
[Route("api/blogtags")]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using Yavsc.Models;
|
|||
using Yavsc.Models.Blog;
|
||||
using Yavsc.Server.Helpers;
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
|
|
|
|||
|
|
@ -2,20 +2,15 @@
|
|||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yavsc.Models;
|
||||
|
||||
namespace Yavsc.ApiControllers
|
||||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Yavsc.Helpers;
|
||||
using Yavsc.Models.FileSystem;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Yavsc.Attributes.Validation;
|
||||
using System.IO;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Exceptions;
|
||||
using Yavsc.Server.Helpers;
|
||||
using Yavsc.Abstract.Helpers;
|
||||
using Yavsc.Server.Models.FileSystem;
|
||||
|
||||
[Authorize,Route("api/fs")]
|
||||
public partial class FileSystemApiController : Controller
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ using Yavsc.Services;
|
|||
using Microsoft.AspNetCore.SignalR;
|
||||
using Yavsc.Server.Helpers;
|
||||
|
||||
namespace Yavsc.ApiControllers
|
||||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
[Authorize, Route("api/stream")]
|
||||
public partial class FileSystemStreamController : Controller
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using System.Linq;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
namespace Yavsc.Blogs.Controllers
|
||||
{
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Yavsc;
|
||||
using Yavsc.Helpers;
|
||||
using Yavsc.Interface;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Services;
|
||||
using Yavsc.Server.Helpers;
|
||||
using Yavsc.Extensions;
|
||||
namespace Yavsc.Blogs;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
|
|
@ -114,15 +114,9 @@ internal class Program
|
|||
.UseAuthentication()
|
||||
.UseAuthorization()
|
||||
.UseCors("default")
|
||||
/* .UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapDefaultControllerRoute()
|
||||
.RequireAuthorization();
|
||||
})*/
|
||||
|
||||
;
|
||||
// app.MapIdentityApi<ApplicationUser>().RequireAuthorization("ApiScope");
|
||||
app.MapDefaultControllerRoute();
|
||||
app.MapIdentityApi<ApplicationUser>().RequireAuthorization("blog");
|
||||
|
||||
app.MapGet("/identity", (HttpContext context) =>
|
||||
new JsonResult(context?.User?.Claims.Select(c => new { c.Type, c.Value }))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>1c73094f-959f-4211-b1a1-6a69b236c283</UserSecretsId>
|
||||
<RootNamespace>Yavsc.Api</RootNamespace>
|
||||
<RootNamespace>Yavsc.Blogs</RootNamespace>
|
||||
<RepositoryUrl>https://github.com/pazof/yavsc</RepositoryUrl>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -666,7 +666,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
|
|||
await _signInManager.SignOutAsync();
|
||||
HttpContext.Session.Clear();
|
||||
_logger.LogInformation(4, "User logged out.");
|
||||
if (returnUrl == null) return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
if (returnUrl == null) return RedirectToAction(nameof(AccountController.Index), "Home");
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ using Yavsc.Services;
|
|||
using Yavsc.Services.Kyc;
|
||||
using Yavsc.Settings;
|
||||
using Yavsc.ViewModels.Auth;
|
||||
using static IdentityServer8.IdentityServerConstants;
|
||||
|
||||
namespace Yavsc.Extensions;
|
||||
|
||||
|
|
@ -360,10 +361,10 @@ public static class HostingExtensions
|
|||
{
|
||||
EnsureDefaultApplicationScopes()(context, _);
|
||||
|
||||
var existingClient = context.Set<Client>().FirstOrDefault(c => c.ClientId == "postit");
|
||||
var existingClient = context.Set<IdentityServer8.EntityFramework.Entities.Client>().FirstOrDefault(c => c.ClientId == "postit");
|
||||
if (existingClient == null)
|
||||
{
|
||||
var client = new Client
|
||||
var client = new IdentityServer8.EntityFramework.Entities.Client
|
||||
{
|
||||
ClientId = "postit",
|
||||
Enabled = true,
|
||||
|
|
@ -373,21 +374,44 @@ public static class HostingExtensions
|
|||
};
|
||||
|
||||
context.Set<Client>().Add(client);
|
||||
context.Set<ClientGrantType>().Add(new ClientGrantType
|
||||
// allow authorization code (interactive) and client credentials (m2m)
|
||||
context.Set<ClientGrantType>().Add(new IdentityServer8.EntityFramework.Entities.ClientGrantType
|
||||
{
|
||||
Client = client,
|
||||
GrantType = "authorization_code"
|
||||
});
|
||||
context.Set<ClientGrantType>().Add(new IdentityServer8.EntityFramework.Entities.ClientGrantType
|
||||
{
|
||||
Client = client,
|
||||
GrantType = "client_credentials"
|
||||
});
|
||||
context.Set<ClientScope>().Add(new ClientScope
|
||||
|
||||
context.Set<ClientScope>().Add(new IdentityServer8.EntityFramework.Entities.ClientScope
|
||||
{
|
||||
Client = client,
|
||||
Scope = "blog"
|
||||
});
|
||||
context.Set<ClientSecret>().Add(new ClientSecret
|
||||
context.Set<ClientScope>().Add(new IdentityServer8.EntityFramework.Entities.ClientScope
|
||||
{
|
||||
Client = client,
|
||||
Value = "postit-secret".Sha256(),
|
||||
Type = IdentityServer8.Models.IdentityServerConstants.SecretTypes.SharedSecret
|
||||
Scope = IdentityServer8.IdentityServerConstants.StandardScopes.OpenId
|
||||
});
|
||||
context.Set<ClientScope>().Add(new IdentityServer8.EntityFramework.Entities.ClientScope
|
||||
{
|
||||
Client = client,
|
||||
Scope = IdentityServer8.IdentityServerConstants.StandardScopes.Profile
|
||||
});
|
||||
|
||||
context.Set<ClientRedirectUri>().Add(new IdentityServer8.EntityFramework.Entities.ClientRedirectUri
|
||||
{
|
||||
Client = client,
|
||||
RedirectUri = "http://127.0.0.1:7890/"
|
||||
});
|
||||
|
||||
context.Set<ClientSecret>().Add(new IdentityServer8.EntityFramework.Entities.ClientSecret
|
||||
{
|
||||
Client = client,
|
||||
Value = "postit-secret".ToSha256(),
|
||||
});
|
||||
|
||||
context.SaveChanges();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Yavsc.Exceptions;
|
|||
using Yavsc.Helpers;
|
||||
using Yavsc.Abstract.Helpers;
|
||||
using ImageMagick;
|
||||
using Yavsc.Server.Models.FileSystem;
|
||||
namespace Yavsc.Server.Helpers
|
||||
{
|
||||
public static class FileSystemHelpers
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
using Yavsc.Abstract.FileSystem;
|
||||
|
||||
namespace Yavsc.Models.FileSystem
|
||||
namespace Yavsc.Server.Models.FileSystem
|
||||
{
|
||||
public class FileReceivedInfo : IFileReceivedInfo
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Yavsc.Attributes.Validation;
|
||||
namespace Yavsc.Models.FileSystem
|
||||
namespace Yavsc.Server.Models.FileSystem
|
||||
{
|
||||
public class RenameFileQuery
|
||||
{
|
||||
|
|
@ -11,6 +11,7 @@ using Yavsc.Helpers;
|
|||
using Yavsc.Models;
|
||||
using Yavsc.Models.FileSystem;
|
||||
using Yavsc.Server.Helpers;
|
||||
using Yavsc.Server.Models.FileSystem;
|
||||
|
||||
namespace Yavsc.ViewModels.Streaming
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ using Microsoft.Extensions.Hosting;
|
|||
internal class Program
|
||||
{
|
||||
public static IHost? AppHost { get; private set; }
|
||||
public static IConfigurationRoot? AppConfiguration { get; private set; }
|
||||
public static IHostEnvironment? AppEnvironment { get; private set; }
|
||||
public static IConfigurationRoot AppConfiguration { get; private set; }
|
||||
public static IHostEnvironment AppEnvironment { get; private set; }
|
||||
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue