logo + oidc client PostIt

This commit is contained in:
Paul Schneider 2026-06-10 11:10:15 +01:00
commit 164bd928aa
12 changed files with 197 additions and 17 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Yavsc — Yet Another Very Small Company">
<title>Yavsc</title>
<desc>Yet Another Very Small Company</desc>
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0EA5E9"/>
<stop offset="100%" stop-color="#1E40AF"/>
</linearGradient>
</defs>
<!-- Rounded square badge -->
<rect x="8" y="8" width="224" height="224" rx="44" ry="44" fill="url(#g)"/>
<!-- Stylized Y formed by three strokes converging -->
<g fill="none" stroke="#ffffff" stroke-width="22" stroke-linecap="round" stroke-linejoin="round">
<path d="M70 60 L120 130"/>
<path d="M170 60 L120 130"/>
<path d="M120 130 L120 188"/>
</g>
<!-- Small dot: the "very small" wink -->
<circle cx="120" cy="46" r="8" fill="#FACC15"/>
</svg>

After

Width:  |  Height:  |  Size: 903 B

View file

@ -2,6 +2,12 @@
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;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@ -12,6 +18,18 @@ namespace PostIt.ViewModels;
public partial class MainViewModel : ViewModelBase
{
[ObservableProperty]
private string _authority = "https://localhost:5001";
[ObservableProperty]
private string _clientId = "postit";
[ObservableProperty]
private string _clientSecret = "postit-secret";
[ObservableProperty]
private string _scope = "blog";
[ObservableProperty]
private string _apiUrl = "http://localhost:5000";
@ -79,6 +97,19 @@ public partial class MainViewModel : ViewModelBase
ApplyFilter();
}
[RelayCommand]
public async Task LoginAsync()
{
await ExecuteAsync(async () =>
{
var tokenResponse = await RequestClientCredentialsTokenAsync();
BearerToken = tokenResponse.AccessToken;
StatusMessage = string.IsNullOrWhiteSpace(tokenResponse.Error)
? "Bearer token acquired."
: $"Token acquired with warning: {tokenResponse.ErrorDescription}";
});
}
[RelayCommand]
public async Task SaveAsync()
{
@ -200,6 +231,52 @@ public partial class MainViewModel : ViewModelBase
}
}
private async Task<TokenResponse> RequestClientCredentialsTokenAsync()
{
using var client = new HttpClient();
var discoveryUrl = Authority.TrimEnd('/') + "/.well-known/openid-configuration";
var discoveryDocument = await client.GetFromJsonAsync<DiscoveryDocument>(discoveryUrl, JsonOptions);
if (discoveryDocument is null || string.IsNullOrWhiteSpace(discoveryDocument.TokenEndpoint))
{
throw new InvalidOperationException("Unable to discover the token endpoint from the authority.");
}
var request = new HttpRequestMessage(HttpMethod.Post, discoveryDocument.TokenEndpoint)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = ClientId,
["client_secret"] = ClientSecret,
["scope"] = Scope,
})
};
var response = await client.SendAsync(request).ConfigureAwait(false);
var payload = await response.Content.ReadFromJsonAsync<TokenResponse>(JsonOptions);
if (payload is null)
{
throw new InvalidOperationException("Invalid token response from the identity provider.");
}
if (!response.IsSuccessStatusCode)
{
var message = string.IsNullOrWhiteSpace(payload.ErrorDescription)
? payload.Error ?? "Unknown token error"
: payload.ErrorDescription;
throw new InvalidOperationException(message);
}
return payload;
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private BlogApiClient CreateClient()
=> new BlogApiClient(ApiUrl, BearerToken);
@ -213,4 +290,12 @@ public partial class MainViewModel : ViewModelBase
private bool CanSave() => SelectedPost is not null && !IsBusy;
private bool CanDelete() => SelectedPost is not null && SelectedPost.Id != 0 && !IsBusy;
private sealed record DiscoveryDocument([property: JsonPropertyName("token_endpoint")] string? TokenEndpoint);
private sealed record TokenResponse(
[property: JsonPropertyName("access_token")] string? AccessToken,
[property: JsonPropertyName("token_type")] string? TokenType,
[property: JsonPropertyName("expires_in")] int ExpiresIn,
[property: JsonPropertyName("error")] string? Error,
[property: JsonPropertyName("error_description")] string? ErrorDescription);
}

View file

@ -16,17 +16,27 @@
<StackPanel Margin="12" Spacing="12">
<TextBlock Text="PostIt Blog API Interface" FontSize="20" FontWeight="Bold" />
<Grid ColumnDefinitions="Auto,1*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="8" RowSpacing="8">
<TextBlock Text="API URL" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Text="{Binding ApiUrl, Mode=TwoWay}" />
<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="Bearer token" VerticalAlignment="Center" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding BearerToken, Mode=TwoWay}" PlaceholderText="Optional token for blog scope" />
<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="Search" VerticalAlignment="Center" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SearchText, Mode=TwoWay}" PlaceholderText="Search title, article, author" />
<TextBlock Grid.Row="2" Text="Client secret" VerticalAlignment="Center" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding ClientSecret, Mode=TwoWay}" />
<StackPanel Grid.Row="3" Grid.ColumnSpan="2" Orientation="Horizontal" Spacing="8">
<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" />

View file

@ -6,7 +6,7 @@
xmlns:views="using:PostIt.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PostIt.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Icon="/Assets/yavsc-logo.ico"
Title="PostIt">
<views:MainView />
</Window>