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>

View file

@ -306,7 +306,7 @@ public static class HostingExtensions
sql => sql.MigrationsAssembly(migrationsAssembly));
}
b.UseSeeding(EnsureDefaultApplicationScopes());
b.UseSeeding(EnsureDefaultConfiguration());
};
})
.AddOperationalStore(options =>
@ -351,7 +351,47 @@ public static class HostingExtensions
context.SaveChanges();
}
}
};
}
private static Action<DbContext, bool> EnsureDefaultConfiguration()
{
return (context, _) =>
{
EnsureDefaultApplicationScopes()(context, _);
var existingClient = context.Set<Client>().FirstOrDefault(c => c.ClientId == "postit");
if (existingClient == null)
{
var client = new Client
{
ClientId = "postit",
Enabled = true,
RequireClientSecret = true,
ProtocolType = "oidc",
RequireConsent = false,
};
context.Set<Client>().Add(client);
context.Set<ClientGrantType>().Add(new ClientGrantType
{
Client = client,
GrantType = "client_credentials"
});
context.Set<ClientScope>().Add(new ClientScope
{
Client = client,
Scope = "blog"
});
context.Set<ClientSecret>().Add(new ClientSecret
{
Client = client,
Value = "postit-secret".Sha256(),
Type = IdentityServer8.Models.IdentityServerConstants.SecretTypes.SharedSecret
});
context.SaveChanges();
}
};
}

View file

@ -5,8 +5,8 @@
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
<link rel="icon" type="image/x-icon" href="~/favicon.ico" asp-append-version="true"/>
<link rel="shortcut icon" type="image/x-icon" href="~/favicon.ico" asp-append-version="true"/>
<link rel="icon" type="image/svg+xml" href="~/images/yavsc-logo.svg" asp-append-version="true" />
<link rel="alternate icon" type="image/x-icon" href="@SiteSettings.Value.FavIcon" asp-append-version="true" />
<link rel="stylesheet" href="~/lib/jquery-ui/jquery-ui.min.css">
<link rel="stylesheet" href="~/lib/bootstrap.quartz.min.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>

View file

@ -1,5 +1,8 @@
<nav class="navbar navbar-expand-sm navbar-dark bg-dark" aria-label="Yavsc">
<a class="navbar-brand" href="#">@Config.SiteSetup.Title</a>
<a class="navbar-brand" href="/">
<img src="~/images/yavsc-logo.svg" alt="Yavsc" style="height:32px; margin-right:8px; display:inline-block; vertical-align:middle;" />
@Config.SiteSetup.Title
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbar" aria-controls="navbar"
aria-expanded="true" aria-label="Toggle navigation">
@ -15,4 +18,3 @@
</ul>
</div>
</nav>

View file

@ -12,11 +12,12 @@
"YavscConnection": "Server=[YOURSERVERNAME];Port=5432;Database=[YOURDBNAME];Username=[YOURDBUSERNAME];Password=[YOURDBPASSW];"
},
"Site": {
"Title": "Yavsc",
"Title": "yavsc",
"Slogan": "Yavsc!",
"StyleSheet": "/css/default.css",
"Authority": "https://127.0.0.1:5001/",
"Banner": "/images/arts/concert.jpg",
"Authority": "https://localhost:5001",
"ExternalUrl": "https://localhost:5001",
"Banner": "~/images/arts/PillarOfCreationOil.svg",
"Owner": {
"Name": "[Site owner's name]",
"EMail": "[Site owner's e-mail address]"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Before After
Before After

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