logo + oidc client PostIt
This commit is contained in:
parent
fa7d6242f1
commit
164bd928aa
12 changed files with 197 additions and 17 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 172 KiB |
BIN
src/PostIt/PostIt/Assets/yavsc-logo.ico
Normal file
BIN
src/PostIt/PostIt/Assets/yavsc-logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 303 KiB |
21
src/PostIt/PostIt/Assets/yavsc-logo.svg
Normal file
21
src/PostIt/PostIt/Assets/yavsc-logo.svg
Normal 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 |
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue