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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
21
src/Yavsc.Org/wwwroot/images/yavsc-logo.svg
Normal file
21
src/Yavsc.Org/wwwroot/images/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 |
Loading…
Add table
Add a link
Reference in a new issue