Updated MoonCore dependencies. Switched to asp.net core native authentication scheme abstractions. Updated claim usage in frontend

This commit is contained in:
2025-08-20 16:16:31 +02:00
parent 60178dc54b
commit 3cc48fb8f7
42 changed files with 1459 additions and 858 deletions

View File

@@ -0,0 +1,19 @@
using MoonCore.Blazor.FlyonUi.Exceptions;
namespace Moonlight.Client.Implementations;
public class LogErrorFilter : IGlobalErrorFilter
{
private readonly ILogger<LogErrorFilter> Logger;
public LogErrorFilter(ILogger<LogErrorFilter> logger)
{
Logger = logger;
}
public Task<bool> HandleException(Exception ex)
{
Logger.LogError(ex, "Global error processed");
return Task.FromResult(false);
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Components;
using MoonCore.Blazor.FlyonUi.Exceptions;
using MoonCore.Exceptions;
namespace Moonlight.Client.Implementations;
public class UnauthenticatedErrorFilter : IGlobalErrorFilter
{
private readonly NavigationManager Navigation;
public UnauthenticatedErrorFilter(NavigationManager navigation)
{
Navigation = navigation;
}
public Task<bool> HandleException(Exception ex)
{
if (ex is not HttpApiException { Status: 401 })
return Task.FromResult(false);
Navigation.NavigateTo("/api/auth/logout", true);
return Task.FromResult(true);
}
}

View File

@@ -22,9 +22,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazor-ApexCharts" Version="6.0.0" />
<PackageReference Include="MoonCore" Version="1.9.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
<PackageReference Include="MoonCore" Version="1.9.6" />
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.9" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.1.5" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<Compile Remove="storage\**\*" />

View File

@@ -1,119 +0,0 @@
using System.Security.Claims;
using System.Web;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using MoonCore.Blazor.FlyonUi.Auth;
using MoonCore.Blazor.Services;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Requests.Auth;
using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Client.Services;
public class RemoteAuthStateManager : AuthenticationStateManager
{
private readonly NavigationManager NavigationManager;
private readonly HttpApiClient HttpApiClient;
private readonly LocalStorageService LocalStorageService;
private readonly ILogger<RemoteAuthStateManager> Logger;
public RemoteAuthStateManager(
HttpApiClient httpApiClient,
LocalStorageService localStorageService,
NavigationManager navigationManager,
ILogger<RemoteAuthStateManager> logger
)
{
HttpApiClient = httpApiClient;
LocalStorageService = localStorageService;
NavigationManager = navigationManager;
Logger = logger;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
=> await LoadAuthState();
public override async Task HandleLogin()
{
var uri = new Uri(NavigationManager.Uri);
var codeParam = HttpUtility.ParseQueryString(uri.Query).Get("code");
if (string.IsNullOrEmpty(codeParam)) // If this is true, we need to log in the user
{
await StartLogin();
}
else
{
try
{
var loginCompleteData = await HttpApiClient.PostJson<LoginCompleteResponse>(
"api/auth/complete",
new LoginCompleteRequest()
{
Code = codeParam
}
);
await LocalStorageService.SetString("AccessToken", loginCompleteData.AccessToken);
NavigationManager.NavigateTo("/");
NotifyAuthenticationStateChanged(LoadAuthState());
}
catch (HttpApiException e)
{
Logger.LogError("Unable to complete login: {e}", e);
await StartLogin();
}
}
}
public override async Task Logout()
{
if (await LocalStorageService.ContainsKey("AccessToken"))
await LocalStorageService.SetString("AccessToken", "");
NotifyAuthenticationStateChanged(LoadAuthState());
}
#region Utilities
private async Task StartLogin()
{
var loginStartData = await HttpApiClient.GetJson<LoginStartResponse>("api/auth/start");
NavigationManager.NavigateTo(loginStartData.Url, true);
}
private async Task<AuthenticationState> LoadAuthState()
{
AuthenticationState newState;
try
{
var checkData = await HttpApiClient.GetJson<CheckResponse>("api/auth/check");
newState = new(new ClaimsPrincipal(
new ClaimsIdentity(
[
new Claim("username", checkData.Username),
new Claim("email", checkData.Email),
new Claim("permissions", string.Join(";", checkData.Permissions))
],
"RemoteAuthStateManager"
)
));
}
catch (HttpApiException)
{
newState = new(new ClaimsPrincipal(
new ClaimsIdentity()
));
}
return newState;
}
#endregion
}

View File

@@ -0,0 +1,45 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Client.Services;
public class RemoteAuthStateProvider : AuthenticationStateProvider
{
private readonly HttpApiClient ApiClient;
public RemoteAuthStateProvider(HttpApiClient apiClient)
{
ApiClient = apiClient;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
ClaimsPrincipal principal;
try
{
var claims = await ApiClient.GetJson<AuthClaimResponse[]>(
"api/auth/check"
);
principal = new ClaimsPrincipal(
new ClaimsIdentity(
claims.Select(x => new Claim(x.Type, x.Value)),
"RemoteAuthentication"
)
);
}
catch (HttpApiException e)
{
if (e.Status != 401 && e.Status != 403)
throw;
principal = new ClaimsPrincipal();
}
return new AuthenticationState(principal);
}
}

View File

@@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Blazor.FlyonUi.Auth;
using MoonCore.Blazor.FlyonUi.Exceptions;
using MoonCore.Permissions;
using Moonlight.Client.Implementations;
using Moonlight.Client.Services;
namespace Moonlight.Client.Startup;
@@ -12,11 +14,12 @@ public partial class Startup
WebAssemblyHostBuilder.Services.AddAuthorizationCore();
WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState();
WebAssemblyHostBuilder.Services.AddAuthenticationStateManager<RemoteAuthStateManager>();
WebAssemblyHostBuilder.Services.AddScoped<AuthenticationStateProvider, RemoteAuthStateProvider>();
WebAssemblyHostBuilder.Services.AddScoped<IGlobalErrorFilter, UnauthenticatedErrorFilter>();
WebAssemblyHostBuilder.Services.AddAuthorizationPermissions(options =>
{
options.ClaimName = "permissions";
options.ClaimName = "Permissions";
options.Prefix = "permissions:";
});

View File

@@ -25,27 +25,11 @@ public partial class Startup
WebAssemblyHostBuilder.Services.AddScoped(sp =>
{
var httpClient = sp.GetRequiredService<HttpClient>();
var httpApiClient = new HttpApiClient(httpClient);
var localStorageService = sp.GetRequiredService<LocalStorageService>();
httpApiClient.OnConfigureRequest += async request =>
{
var accessToken = await localStorageService.GetString("AccessToken");
if (string.IsNullOrEmpty(accessToken))
return;
request.Headers.Add("Authorization", $"Bearer {accessToken}");
};
return httpApiClient;
return new HttpApiClient(httpClient);
});
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
WebAssemblyHostBuilder.Services.AddFileManagerOperations();
WebAssemblyHostBuilder.Services.AddFlyonUiServices();
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
WebAssemblyHostBuilder.Services.AddScoped<ThemeService>();

View File

@@ -1,4 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using MoonCore.Blazor.FlyonUi.Exceptions;
using MoonCore.Logging;
using Moonlight.Client.Implementations;
namespace Moonlight.Client.Startup;
@@ -7,6 +10,7 @@ public partial class Startup
private Task SetupLogging()
{
var loggerFactory = new LoggerFactory();
loggerFactory.AddAnsiConsole();
Logger = loggerFactory.CreateLogger<Startup>();
@@ -19,6 +23,8 @@ public partial class Startup
WebAssemblyHostBuilder.Logging.ClearProviders();
WebAssemblyHostBuilder.Logging.AddAnsiConsole();
WebAssemblyHostBuilder.Services.AddScoped<IGlobalErrorFilter, LogErrorFilter>();
return Task.CompletedTask;
}
}

View File

@@ -28,8 +28,8 @@ public partial class Startup
WebAssemblyHostBuilder = builder;
await PrintVersion();
await SetupLogging();
await SetupLogging();
await LoadConfiguration();
await InitializePlugins();

View File

@@ -0,0 +1,534 @@
!bg-base-100
!border-base-content/40
!border-none
!flex
!font-medium
!font-semibold
!h-2.5
!justify-between
!me-1.5
!ms-auto
!px-2.5
!py-0.5
!rounded-full
!rounded-xs
!text-sm
!w-2.5
*:[grid-area:1/1]
*:first:rounded-tl-lg
*:last:rounded-tr-lg
-left-4
-ml-4
-translate-x-full
-translate-y-1/2
absolute
accordion
accordion-bordered
accordion-content
accordion-toggle
active
active-tab:bg-primary
active-tab:hover:text-primary-content
active-tab:text-primary-content
advance-select-menu
advance-select-option
advance-select-tag
advance-select-toggle
alert
alert-error
alert-outline
alert-soft
align-bottom
align-middle
animate-bounce
animate-ping
aria-[current='page']:text-bg-soft-primary
avatar
badge
badge-error
badge-info
badge-outline
badge-primary
badge-soft
badge-success
bg-background
bg-background/60
bg-base-100
bg-base-150
bg-base-200
bg-base-200!
bg-base-200/50
bg-base-300
bg-base-300/45
bg-base-300/50
bg-base-300/60
bg-error
bg-info
bg-primary
bg-primary/5
bg-success
bg-transparent
bg-warning
block
blur
border
border-0
border-2
border-b
border-base-content
border-base-content/20
border-base-content/25
border-base-content/40
border-base-content/5
border-dashed
border-t
border-transparent
bottom-0
bottom-full
break-words
btn
btn-accent
btn-active
btn-circle
btn-disabled
btn-error
btn-info
btn-outline
btn-primary
btn-secondary
btn-sm
btn-soft
btn-square
btn-success
btn-text
btn-warning
card
card-alert
card-body
card-border
card-footer
card-header
card-title
carousel
carousel-body
carousel-next
carousel-prev
carousel-slide
chat
chat-avatar
chat-bubble
chat-footer
chat-header
chat-receiver
chat-sender
checkbox
checkbox-primary
checkbox-xs
col-span-1
collapse
combo-box-selected:block
combo-box-selected:dropdown-active
complete
container
contents
cursor-default
cursor-not-allowed
cursor-pointer
diff
disabled
divide-base-150/60
divide-y
divider
drop-shadow
dropdown
dropdown-active
dropdown-disabled
dropdown-item
dropdown-menu
dropdown-open:opacity-100
dropdown-open:rotate-180
dropdown-toggle
duration-300
duration-500
ease-in-out
ease-linear
end-3
file-upload-complete:progress-success
fill-base-content
fill-black
fill-gray-200
filter
filter-reset
fixed
flex
flex-1
flex-col
flex-grow
flex-nowrap
flex-row
flex-shrink-0
flex-wrap
focus-visible:outline-none
focus-within:border-primary
focus:border-primary
focus:outline-1
focus:outline-none
focus:outline-primary
focus:ring-0
font-bold
font-inter
font-medium
font-normal
font-semibold
gap-0.5
gap-1
gap-1.5
gap-2
gap-3
gap-4
gap-5
gap-6
gap-x-1
gap-x-2
gap-x-3
gap-y-1
gap-y-2.5
gap-y-3
grid
grid-cols-1
grid-cols-4
grid-flow-col
grow
grow-0
h-12
h-2
h-3
h-32
h-64
h-8
h-auto
h-full
h-screen
helper-text
hidden
hover:bg-primary/5
hover:bg-transparent
hover:text-base-content
hover:text-base-content/60
hover:text-primary
image-full
inline
inline-block
inline-flex
inline-grid
input
input-floating
input-floating-label
input-lg
input-md
input-sm
input-xl
inset-0
inset-y-0
inset-y-2
invisible
is-invalid
is-valid
isolate
italic
items-center
items-end
items-start
join
join-item
justify-between
justify-center
justify-end
justify-start
justify-stretch
label-text
leading-3
leading-3.5
leading-6
leading-none
left-0
lg:bg-base-100/20
lg:flex
lg:gap-y-0
lg:grid-cols-2
lg:hidden
lg:justify-end
lg:justify-start
lg:min-w-0
lg:p-10
lg:pb-5
lg:pl-64
lg:pr-3.5
lg:pt-5
lg:ring-1
lg:ring-base-content/10
lg:rounded-lg
lg:shadow-xs
link
link-animated
link-hover
list-disc
list-inside
list-none
loading
loading-lg
loading-sm
loading-spinner
loading-xl
loading-xs
lowercase
m-10
mask
max-h-52
max-lg:flex-col
max-lg:hidden
max-w-7xl
max-w-80
max-w-full
max-w-lg
max-w-sm
max-w-xl
mb-0.5
mb-1
mb-1.5
mb-2
mb-2.5
mb-3
mb-4
mb-5
md:min-w-md
md:table-cell
md:text-3xl
me-1
me-1.5
me-2
me-2.5
me-5
menu
menu-active
menu-disabled
menu-dropdown
menu-dropdown-show
menu-horizontal
menu-title
min-h-0
min-h-svh
min-w-0
min-w-28
min-w-48
min-w-60
min-w-[100px]
min-w-sm
ml-3
ml-4
modal
modal-content
modal-dialog
modal-middle
modal-title
mr-4
ms-0.5
ms-1
ms-2
ms-3
ms-auto
mt-1
mt-1.5
mt-10
mt-12
mt-2
mt-2.5
mt-3
mt-3.5
mt-4
mt-5
mt-8
mx-1
mx-auto
my-3
my-auto
object-cover
opacity-0
opacity-100
open
origin-top-left
outline
outline-0
overflow-hidden
overflow-x-auto
overflow-y-auto
overlay-open:duration-50
overlay-open:opacity-100
p-0.5
p-1
p-2
p-3
p-4
p-5
p-6
p-8
pin-input
placeholder-base-content/60
pointer-events-auto
pointer-events-none
progress
progress-bar
progress-indeterminate
progress-primary
pt-0
pt-0.5
pt-3
px-1.5
px-2
px-2.5
px-3
px-4
px-5
py-0.5
py-1.5
py-2
py-2.5
py-6
radial-progress
radio
range
relative
resize
ring-0
ring-1
ring-white/10
rounded-box
rounded-field
rounded-full
rounded-lg
rounded-md
rounded-t-lg
row-active
row-hover
rtl:!mr-0
select
select-disabled:opacity-40
select-disabled:pointer-events-none
select-floating
select-floating-label
selected
selected:select-active
shadow-base-300/20
shadow-lg
shadow-md
shadow-xs
shrink-0
size-10
size-4
size-5
size-8
skeleton
skeleton-animated
sm:auto-cols-max
sm:flex
sm:items-center
sm:items-end
sm:justify-between
sm:justify-end
sm:max-w-2xl
sm:max-w-3xl
sm:max-w-4xl
sm:max-w-5xl
sm:max-w-6xl
sm:max-w-7xl
sm:max-w-lg
sm:max-w-md
sm:max-w-xl
sm:mb-0
sm:mt-5
sm:mt-6
sm:p-6
sm:py-2
sm:text-sm/5
space-x-1
space-y-1
space-y-4
sr-only
static
status
status-error
sticky
switch
tab
tab-active
table
table-pin-cols
table-pin-rows
tabs
tabs-bordered
tabs-lg
tabs-lifted
tabs-md
tabs-sm
tabs-xl
tabs-xs
text-2xl
text-4xl
text-accent
text-base
text-base-content
text-base-content/40
text-base-content/50
text-base-content/60
text-base-content/70
text-base-content/80
text-base/6
text-center
text-error
text-error-content
text-gray-400
text-info
text-info-content
text-left
text-lg
text-primary
text-primary-content
text-sm
text-sm/5
text-success
text-success-content
text-warning
text-warning-content
text-xl
text-xs
text-xs/5
textarea
textarea-floating
textarea-floating-label
theme-controller
tooltip
tooltip-content
top-0
top-1/2
top-full
transform
transition
transition-all
transition-opacity
translate-x-0
truncate
underline
uppercase
validate
w-0
w-0.5
w-12
w-4
w-56
w-64
w-fit
w-full
whitespace-nowrap
z-10
z-40
z-50

View File

@@ -1,8 +1,13 @@
@using Moonlight.Client.UI.Layouts
@using Moonlight.Client.Services
@using Moonlight.Client.UI.Partials
@inject ApplicationAssemblyService ApplicationAssemblyService
<ApplicationRouter DefaultLayout="@typeof(MainLayout)"
AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="ApplicationAssemblyService.Assemblies" />
AdditionalAssemblies="ApplicationAssemblyService.Assemblies">
<LoginTemplate>
<LoginSelector />
</LoginTemplate>
</ApplicationRouter>

View File

@@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization
<div class="col-span-12 md:col-span-6">
<div class="font-medium leading-[1.1] tracking-tight">
@@ -18,7 +19,6 @@
protected override async Task OnInitializedAsync()
{
var identity = await AuthState;
var usernameClaim = identity.User.Claims.ToArray().First(x => x.Type == "username");
Username = usernameClaim.Value;
Username = identity.User.FindFirst(ClaimTypes.Name)!.Value;
}
}

View File

@@ -1,13 +1,11 @@
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using MoonCore.Blazor.FlyonUi.Auth
@using Moonlight.Client.Interfaces
@using Moonlight.Client.Models
@using Moonlight.Client.UI.Layouts
@inject NavigationManager Navigation
@inject AuthenticationStateManager AuthStateManager
@inject IEnumerable<ISidebarItemProvider> SidebarItemProviders
@inject IAuthorizationService AuthorizationService
@@ -210,8 +208,8 @@
var authState = await AuthState;
Identity = authState.User;
Username = Identity.Claims.First(x => x.Type == "username").Value;
Email = Identity.Claims.First(x => x.Type == "email").Value;
Username = Identity.FindFirst(ClaimTypes.Name)!.Value;
Email = Identity.FindFirst(ClaimTypes.Email)!.Value;
var sidebarItems = new List<SidebarItem>();
@@ -260,8 +258,9 @@
return Task.CompletedTask;
}
private async Task Logout()
private Task Logout()
{
await AuthStateManager.Logout();
Navigation.NavigateTo("/api/auth/logout", true);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,87 @@
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Responses.Auth
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
<div class="flex h-screen justify-center items-center">
<div class="sm:max-w-lg">
<div class="w-full card card-body text-center">
<LazyLoader EnableDefaultSpacing="false" Load="Load">
@if (ShowSelection)
{
<h5 class="card-title mb-2.5">Login to MoonCore</h5>
<p class="mb-4">Choose a login provider to start using the app</p>
<div class="flex flex-col w-full mt-5 gap-y-2.5">
@foreach (var scheme in AuthSchemes)
{
var config = Configs.GetValueOrDefault(scheme.Identifier);
if (config == null) // Ignore all schemes which have no ui configured
continue;
<button @onclick="() => Start(scheme)" class="btn btn-text w-full" style="background-color: @(config.Color)">
<img src="@config.IconUrl"
alt="scheme icon"
class="size-5 object-cover fill-base-content"/>
Sign in with @scheme.DisplayName
</button>
}
</div>
}
else
{
<div class="flex justify-center">
<span class="loading loading-spinner loading-xl"></span>
</div>
}
</LazyLoader>
</div>
</div>
</div>
@code
{
private AuthSchemeResponse[] AuthSchemes;
private Dictionary<string, AuthSchemeConfig> Configs = new();
private bool ShowSelection = false;
protected override void OnInitialized()
{
Configs["LocalAuth"] = new AuthSchemeConfig()
{
Color = "#7636e3",
IconUrl = "/placeholder.jpg"
};
}
private async Task Load(LazyLoader arg)
{
AuthSchemes = await ApiClient.GetJson<AuthSchemeResponse[]>(
"api/auth"
);
// If we only have one auth scheme available
// we want to auto redirect the user without
// showing the selection screen
if (AuthSchemes.Length == 1)
await Start(AuthSchemes[0]);
else
ShowSelection = true;
}
private Task Start(AuthSchemeResponse scheme)
{
Navigation.NavigateTo($"/api/auth/{scheme.Identifier}", true);
return Task.CompletedTask;
}
record AuthSchemeConfig
{
public string Color { get; set; }
public string IconUrl { get; set; }
}
}

View File

@@ -4,6 +4,7 @@
@using MoonCore.Helpers
@using Moonlight.Client.Implementations
@using MoonCore.Blazor.FlyonUi.Files.Manager
@using MoonCore.Blazor.FlyonUi.Files.Manager.Operations
@attribute [Authorize(Policy = "permissions:admin.system.overview")]
@@ -13,7 +14,8 @@
<NavTabs Index="2" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks"/>
</div>
<FileManager FsAccess="FsAccess" TransferChunkSize="TransferChunkSize" UploadLimit="UploadLimit"/>
<FileManager OnConfigure="OnConfigure" FsAccess="FsAccess" TransferChunkSize="TransferChunkSize"
UploadLimit="UploadLimit"/>
@code
{
@@ -21,9 +23,21 @@
private static readonly long TransferChunkSize = ByteConverter.FromMegaBytes(20).Bytes;
private static readonly long UploadLimit = ByteConverter.FromGigaBytes(20).Bytes;
protected override void OnInitialized()
{
FsAccess = new SystemFsAccess(ApiClient);
}
private void OnConfigure(FileManagerOptions options)
{
options.AddMultiOperation<DeleteOperation>();
options.AddMultiOperation<MoveOperation>();
options.AddMultiOperation<DownloadOperation>();
options.AddSingleOperation<RenameOperation>();
options.AddToolbarOperation<CreateFileOperation>();
options.AddToolbarOperation<CreateFolderOperation>();
}
}