Login settings

This commit is contained in:
Paul Schneider 2026-06-10 16:59:23 +01:00
commit 1a0556695c
37 changed files with 504 additions and 190 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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();
}
}
}

View file

@ -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>

View file

@ -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;
}

View 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 { }
}
}
}

View 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; }
}

View 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}");
}
}
}

View file

@ -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()
{

View 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; }
}

View file

@ -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
{
}

View 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>

View 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;
}
}
}

View file

@ -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">

View file

@ -10,4 +10,5 @@ public partial class MainView : UserControl
{
InitializeComponent();
}
}

View file

@ -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>

View file

@ -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();
}
}
}

View 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>

View file

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace PostIt.Views;
public partial class SettingsView: Window
{
public SettingsView()
{
InitializeComponent();
}
}

View 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"
]
}

View file

@ -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")]

View file

@ -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")]

View file

@ -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")]

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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 }))
);

View file

@ -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>

View file

@ -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);
}

View file

@ -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();

View file

@ -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

View file

@ -23,7 +23,7 @@
using Yavsc.Abstract.FileSystem;
namespace Yavsc.Models.FileSystem
namespace Yavsc.Server.Models.FileSystem
{
public class FileReceivedInfo : IFileReceivedInfo
{

View file

@ -1,5 +1,5 @@
using Yavsc.Attributes.Validation;
namespace Yavsc.Models.FileSystem
namespace Yavsc.Server.Models.FileSystem
{
public class RenameFileQuery
{

View file

@ -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
{

View file

@ -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)
{