Refactored project to module structure

This commit is contained in:
2026-03-12 22:50:15 +01:00
parent 93de9c5d00
commit 1257e8b950
219 changed files with 1231 additions and 1259 deletions

View File

@@ -0,0 +1,27 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Frontend.Infrastructure.Hooks;
namespace Moonlight.Frontend.Infrastructure.Configuration;
public class LayoutMiddlewareOptions
{
private readonly List<Type> InnerComponents = new();
public IReadOnlyList<Type> Components => InnerComponents;
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : LayoutMiddlewareBase
{
InnerComponents.Add(typeof(T));
}
public void Insert<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(int index)
where T : LayoutMiddlewareBase
{
InnerComponents.Insert(index, typeof(T));
}
public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
where T : LayoutMiddlewareBase
{
InnerComponents.Remove(typeof(T));
}
}

View File

@@ -0,0 +1,40 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
namespace Moonlight.Frontend.Infrastructure.Configuration;
public class LayoutPageOptions
{
private readonly List<LayoutPageComponent> InnerComponents = new();
public IReadOnlyList<LayoutPageComponent> Components => InnerComponents;
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(LayoutPageSlot slot, int order)
where T : ComponentBase
{
Add(typeof(T), slot, order);
}
public void Add(Type componentType, LayoutPageSlot slot, int order)
{
InnerComponents.Add(new LayoutPageComponent(componentType, order, slot));
}
public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
where T : ComponentBase
{
Remove(typeof(T));
}
public void Remove(Type componentType)
{
InnerComponents.RemoveAll(x => x.ComponentType == componentType);
}
}
public record LayoutPageComponent(Type ComponentType, int Order, LayoutPageSlot Slot);
public enum LayoutPageSlot
{
Header = 0,
Footer = 1
}

View File

@@ -0,0 +1,8 @@
using System.Reflection;
namespace Moonlight.Frontend.Infrastructure.Configuration;
public class NavigationAssemblyOptions
{
public List<Assembly> Assemblies { get; private set; } = new();
}

View File

@@ -0,0 +1,46 @@
namespace Moonlight.Frontend.Infrastructure.Helpers;
public static class Formatter
{
public static string FormatSize(long bytes, double conversionStep = 1024)
{
if (bytes == 0) return "0 B";
string[] units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
var unitIndex = 0;
double size = bytes;
while (size >= conversionStep && unitIndex < units.Length - 1)
{
size /= conversionStep;
unitIndex++;
}
var decimals = unitIndex == 0 ? 0 : 2;
return $"{Math.Round(size, decimals)} {units[unitIndex]}";
}
public static string FormatDuration(TimeSpan timeSpan)
{
var abs = timeSpan.Duration(); // Handle negative timespans
if (abs.TotalSeconds < 1)
return $"{abs.TotalMilliseconds:F0}ms";
if (abs.TotalMinutes < 1)
return $"{abs.TotalSeconds:F1}s";
if (abs.TotalHours < 1)
return $"{abs.Minutes}m {abs.Seconds}s";
if (abs.TotalDays < 1)
return $"{abs.Hours}h {abs.Minutes}m";
if (abs.TotalDays < 365)
return $"{abs.Days}d {abs.Hours}h";
var years = (int)(abs.TotalDays / 365);
var days = abs.Days % 365;
return days > 0 ? $"{years}y {days}d" : $"{years}y";
}
}

View File

@@ -0,0 +1,29 @@
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.Forms;
using Moonlight.Shared.Shared;
namespace Moonlight.Frontend.Infrastructure.Helpers;
public static class ProblemDetailsHelper
{
public static async Task HandleProblemDetailsAsync(HttpResponseMessage response, object model,
ValidationMessageStore validationMessageStore)
{
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>();
if (problemDetails == null)
{
response.EnsureSuccessStatusCode(); // Trigger exception when unable to parse
}
else
{
if (!string.IsNullOrEmpty(problemDetails.Detail))
validationMessageStore.Add(new FieldIdentifier(model, string.Empty), problemDetails.Detail);
if (problemDetails.Errors != null)
foreach (var error in problemDetails.Errors)
foreach (var message in error.Value)
validationMessageStore.Add(new FieldIdentifier(model, error.Key), message);
}
}
}

View File

@@ -0,0 +1,8 @@
using Moonlight.Frontend.Admin.Users.Shared;
namespace Moonlight.Frontend.Infrastructure.Hooks;
public interface IPermissionProvider
{
public Task<PermissionCategory[]> GetPermissionsAsync();
}

View File

@@ -0,0 +1,8 @@
using Moonlight.Frontend.Infrastructure.Models;
namespace Moonlight.Frontend.Infrastructure.Hooks;
public interface ISidebarProvider
{
public Task<SidebarItem[]> GetItemsAsync();
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.Frontend.Infrastructure.Hooks;
public abstract class LayoutMiddlewareBase : ComponentBase
{
[Parameter] public RenderFragment ChildContent { get; set; }
}

View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Authorization;
using Moonlight.Shared;
namespace Moonlight.Frontend.Infrastructure.Implementations;
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var permissionClaim = context.User.FindFirst(x =>
x.Type.Equals(Permissions.ClaimType, StringComparison.OrdinalIgnoreCase) &&
x.Value.Equals(requirement.Identifier, StringComparison.OrdinalIgnoreCase)
);
if (permissionClaim == null)
{
context.Fail(new AuthorizationFailureReason(
this,
$"User does not have the requested permission '{requirement.Identifier}'"
));
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Moonlight.Shared;
namespace Moonlight.Frontend.Infrastructure.Implementations;
public class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
private readonly DefaultAuthorizationPolicyProvider FallbackProvider;
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
{
FallbackProvider = new DefaultAuthorizationPolicyProvider(options);
}
public async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (!policyName.StartsWith(Permissions.Prefix, StringComparison.OrdinalIgnoreCase))
return await FallbackProvider.GetPolicyAsync(policyName);
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName));
return policy.Build();
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
return FallbackProvider.GetDefaultPolicyAsync();
}
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
{
return FallbackProvider.GetFallbackPolicyAsync();
}
}
public class PermissionRequirement : IAuthorizationRequirement
{
public PermissionRequirement(string identifier)
{
Identifier = identifier;
}
public string Identifier { get; }
}

View File

@@ -0,0 +1,49 @@
using LucideBlazor;
using Moonlight.Frontend.Admin.Users.Shared;
using Moonlight.Frontend.Infrastructure.Hooks;
using Moonlight.Shared;
namespace Moonlight.Frontend.Infrastructure.Implementations;
public sealed class PermissionProvider : IPermissionProvider
{
public Task<PermissionCategory[]> GetPermissionsAsync()
{
return Task.FromResult<PermissionCategory[]>([
new PermissionCategory("Users", typeof(UserRoundIcon), [
new Permission(Permissions.Users.Create, "Create", "Create new users"),
new Permission(Permissions.Users.View, "View", "View all users"),
new Permission(Permissions.Users.Edit, "Edit", "Edit user details"),
new Permission(Permissions.Users.Delete, "Delete", "Delete user accounts"),
new Permission(Permissions.Users.Logout, "Logout", "Logout user accounts")
]),
new PermissionCategory("Roles", typeof(UsersRoundIcon), [
new Permission(Permissions.Roles.Create, "Create", "Create new roles"),
new Permission(Permissions.Roles.View, "View", "View all roles"),
new Permission(Permissions.Roles.Edit, "Edit", "Edit role details"),
new Permission(Permissions.Roles.Delete, "Delete", "Delete role accounts"),
new Permission(Permissions.Roles.Members, "Members", "Manage role members")
]),
new PermissionCategory("System", typeof(CogIcon), [
new Permission(Permissions.System.Info, "Info", "View system info"),
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"),
new Permission(Permissions.System.Instance, "Instance",
"Update the moonlight instance and add plugins"),
new Permission(Permissions.System.Settings, "Settings", "Change settings of the instance")
]),
new PermissionCategory("API Keys", typeof(KeyIcon), [
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),
new Permission(Permissions.ApiKeys.View, "View", "View all API keys"),
new Permission(Permissions.ApiKeys.Edit, "Edit", "Edit API key details"),
new Permission(Permissions.ApiKeys.Delete, "Delete", "Delete API keys")
]),
new PermissionCategory("Themes", typeof(PaintRollerIcon), [
new Permission(Permissions.Themes.Create, "Create", "Create new theme"),
new Permission(Permissions.Themes.View, "View", "View all themes"),
new Permission(Permissions.Themes.Edit, "Edit", "Edit themes"),
new Permission(Permissions.Themes.Delete, "Delete", "Delete themes")
])
]);
}
}

View File

@@ -0,0 +1,53 @@
using LucideBlazor;
using Moonlight.Frontend.Infrastructure.Hooks;
using Moonlight.Frontend.Infrastructure.Models;
using Moonlight.Shared;
namespace Moonlight.Frontend.Infrastructure.Implementations;
public sealed class SidebarProvider : ISidebarProvider
{
public Task<SidebarItem[]> GetItemsAsync()
{
return Task.FromResult<SidebarItem[]>([
new SidebarItem
{
Name = "Overview",
IconType = typeof(LayoutDashboardIcon),
Path = "/",
IsExactPath = true,
Order = 0
},
new SidebarItem
{
Name = "Overview",
IconType = typeof(LayoutDashboardIcon),
Path = "/admin",
IsExactPath = true,
Group = "Admin",
Order = 0,
Policy = Permissions.System.Info
},
new SidebarItem
{
Name = "Users",
IconType = typeof(UsersRoundIcon),
Path = "/admin/users",
IsExactPath = false,
Group = "Admin",
Order = 10,
Policy = Permissions.Users.View
},
new SidebarItem
{
Name = "System",
IconType = typeof(SettingsIcon),
Path = "/admin/system",
IsExactPath = false,
Group = "Admin",
Order = 20,
Policy = Permissions.System.Info
}
]);
}
}

View File

@@ -0,0 +1,19 @@
using System.Diagnostics.CodeAnalysis;
namespace Moonlight.Frontend.Infrastructure.Models;
public record SidebarItem
{
public string Name { get; init; }
public string Path { get; init; }
public bool IsExactPath { get; init; }
public string? Group { get; init; }
public int Order { get; init; }
// Used to prevent the IL-Trimming from removing this type as its dynamically assigned a type and we
// need it to work properly
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public Type IconType { get; init; }
public string? Policy { get; set; }
}

View File

@@ -0,0 +1,97 @@
@using System.Net
@using System.Reflection
@using LucideBlazor
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Infrastructure.Configuration
@using Moonlight.Frontend.Shared.Auth
@using ShadcnBlazor.Emptys
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Extras.Dialogs
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Portals
@inject NavigationManager Navigation
@inject IOptions<NavigationAssemblyOptions> NavigationOptions
<ErrorBoundary>
<ChildContent>
<AuthorizeView>
<ChildContent>
<LayoutMiddleware>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="Assemblies"
NotFoundPage="typeof(NotFound)">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
<NotAuthorized Context="authRouteViewContext">
<AccessDenied/>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
</Router>
</LayoutMiddleware>
<ToastLauncher/>
<DialogLauncher/>
<AlertDialogLauncher/>
<PortalOutlet/>
</ChildContent>
<Authorizing>
<Authenticating/>
</Authorizing>
<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated ?? false)
{
<AccessDenied/>
}
else
{
var uri = new Uri(Navigation.Uri);
if (uri.LocalPath.StartsWith("/setup"))
{
<Setup/>
}
else
{
<Authentication/>
}
}
</NotAuthorized>
</AuthorizeView>
</ChildContent>
<ErrorContent>
@if (context is HttpRequestException { StatusCode: HttpStatusCode.Unauthorized })
{
<Authentication/>
}
else
{
<div class="m-10">
<Empty>
<EmptyHeader>
<EmptyMedia Variant="EmptyMediaVariant.Icon">
<OctagonAlertIcon ClassName="text-red-500/80"/>
</EmptyMedia>
<EmptyTitle>
Critical Application Error
</EmptyTitle>
<EmptyDescription>
@context.ToString()
</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
}
</ErrorContent>
</ErrorBoundary>
@code
{
private Assembly[] Assemblies;
protected override void OnInitialized()
{
Assemblies = NavigationOptions.Value.Assemblies.ToArray();
}
}

View File

@@ -0,0 +1,8 @@
@using ShadcnBlazor.Sidebars
<header
class="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
style="--header-height: calc(var(--spacing) * 12)">
<div class="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger ClassName="-ml-1"/>
</div>
</header>

View File

@@ -0,0 +1,126 @@
@inject NavigationManager Navigation
@inject IAuthorizationService AuthorizationService
@inject FrontendService FrontendService
@inject IEnumerable<ISidebarProvider> Providers
@using System.Text.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Frontend.Infrastructure.Hooks
@using Moonlight.Frontend.Infrastructure.Models
@using Moonlight.Frontend.Shared.Frontend
@using ShadcnBlazor.Sidebars
@implements IDisposable
@{
var url = new Uri(Navigation.Uri);
}
<Sidebar Variant="SidebarVariant.Sidebar" Collapsible="SidebarCollapsible.Icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>
<a href="/" class="flex flex-row items-center">
<img alt="Logo" src="/_content/Moonlight.Frontend/logo.svg" class="size-6"/>
<span class="ms-2.5 text-lg font-semibold">@FrontendConfiguration?.Name</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
@foreach (var group in Items.GroupBy(x => x.Group))
{
<SidebarGroup>
@if (!string.IsNullOrWhiteSpace(group.Key))
{
<SidebarGroupLabel>
@group.Key
</SidebarGroupLabel>
}
<SidebarGroupContent>
<SidebarMenu>
@foreach (var item in group.OrderBy(x => x.Order))
{
var isActive = item.IsExactPath
? item.Path == url.LocalPath
: url.LocalPath.StartsWith(item.Path, StringComparison.OrdinalIgnoreCase);
<SidebarMenuItem @key="item">
<SidebarMenuButton IsActive="@isActive">
<Slot>
<a href="@item.Path" @attributes="context">
<DynamicComponent Type="item.IconType"/>
<span>@item.Name</span>
</a>
</Slot>
</SidebarMenuButton>
</SidebarMenuItem>
}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
}
</SidebarContent>
<SidebarFooter>
<NavUser/>
</SidebarFooter>
</Sidebar>
@code
{
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
private readonly List<SidebarItem> Items = new();
private FrontendConfiguration? FrontendConfiguration;
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
foreach (var provider in Providers)
{
var items = await provider.GetItemsAsync();
foreach (var item in items)
{
if (!string.IsNullOrWhiteSpace(item.Policy))
{
var result = await AuthorizationService.AuthorizeAsync(authState.User, item.Policy);
if (!result.Succeeded)
continue;
}
Items.Add(item);
}
}
Navigation.LocationChanged += OnLocationChanged;
FrontendConfiguration = await FrontendService.GetConfigurationAsync();
Console.WriteLine(JsonSerializer.Serialize(FrontendConfiguration));
}
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
try
{
await InvokeAsync(StateHasChanged);
}
catch (Exception)
{
// Ignored
}
}
public void Dispose()
{
Navigation.LocationChanged -= OnLocationChanged;
}
}

View File

@@ -0,0 +1,33 @@
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Infrastructure.Configuration
@using Moonlight.Frontend.Infrastructure.Hooks
@inject IOptions<LayoutMiddlewareOptions> Options
@Chain
@code
{
[Parameter] public RenderFragment ChildContent { get; set; }
private RenderFragment Chain;
protected override void OnInitialized()
{
Chain = ChildContent;
foreach (var component in Options.Value.Components)
{
// Capture current values
var currentChain = Chain;
var currentComponent = component;
Chain = builder =>
{
builder.OpenComponent(0, currentComponent);
builder.SetKey(component);
builder.AddComponentParameter(1, nameof(LayoutMiddlewareBase.ChildContent), currentChain);
builder.CloseComponent();
};
}
}
}

View File

@@ -0,0 +1,52 @@
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Infrastructure.Configuration
@using ShadcnBlazor.Extras.Alerts
@using ShadcnBlazor.Sidebars
@inherits LayoutComponentBase
@inject IOptions<LayoutPageOptions> LayoutPageOptions
<SidebarProvider DefaultOpen="true">
<AppSidebar/>
<SidebarInset>
<AppHeader/>
@foreach (var headerComponent in HeaderComponents)
{
<DynamicComponent Type="headerComponent"/>
}
<div class="mx-8 my-8 max-w-full">
<AlertLauncher/>
@Body
</div>
@foreach (var footerComponent in FooterComponents)
{
<DynamicComponent Type="footerComponent"/>
}
</SidebarInset>
</SidebarProvider>
@code
{
private Type[] HeaderComponents;
private Type[] FooterComponents;
protected override void OnInitialized()
{
HeaderComponents = LayoutPageOptions.Value.Components
.Where(x => x.Slot == LayoutPageSlot.Header)
.OrderBy(x => x.Order)
.Select(x => x.ComponentType)
.ToArray();
FooterComponents = LayoutPageOptions.Value.Components
.Where(x => x.Slot == LayoutPageSlot.Footer)
.OrderBy(x => x.Order)
.Select(x => x.ComponentType)
.ToArray();
}
}

View File

@@ -0,0 +1,78 @@
@using System.Security.Claims
@using LucideBlazor
@using Microsoft.AspNetCore.Components.Authorization
@using ShadcnBlazor.Avatars
@using ShadcnBlazor.Dropdowns
@using ShadcnBlazor.Interop
@using ShadcnBlazor.Sidebars
@inject NavigationManager Navigation
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger>
<Slot>
<SidebarMenuButton
Size="SidebarMenuButtonSize.Lg"
ClassName="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
@attributes="context">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src="https://github.com/ghost.png" alt="Ghost"/>
<AvatarFallback className="rounded-lg">GH</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">@Username</span>
<span class="truncate text-xs">@Email</span>
</div>
<ChevronsUpDownIcon ClassName="ml-auto size-4"/>
</SidebarMenuButton>
</Slot>
</DropdownMenuTrigger>
<DropdownMenuContent
ClassName="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
Side="@(SidebarProvider.IsMobile ? PositionSide.Bottom : PositionSide.Right)"
Align="PositionAlignment.End">
<DropdownMenuLabel ClassName="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar ClassName="h-8 w-8 rounded-lg">
<AvatarImage src="https://github.com/ghost.png" alt="Ghost"/>
<AvatarFallback className="rounded-lg">GH</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">@Username</span>
<span class="truncate text-xs">@Email</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator/>
<DropdownMenuItem OnClick="Logout">
<LogOutIcon/>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
@code
{
[CascadingParameter] public SidebarProvider SidebarProvider { get; set; }
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
private string Username;
private string Email;
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
Username = authState.User.FindFirst(ClaimTypes.Name)?.Value ?? "N/A";
Email = authState.User.FindFirst(ClaimTypes.Email)?.Value ?? "N/A";
}
private void Logout()
{
Navigation.NavigateTo("/api/auth/logout", true);
}
}

View File

@@ -0,0 +1,23 @@
@page "/notfound"
@using LucideBlazor
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Emptys
<Empty>
<EmptyHeader>
<EmptyMedia Variant="EmptyMediaVariant.Icon">
<FolderOpenIcon/>
</EmptyMedia>
<EmptyTitle>Page not found</EmptyTitle>
<EmptyDescription>
The page you requested could not be found
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>
<Slot>
<a href="/" @attributes="context">Go to home</a>
</Slot>
</Button>
</EmptyContent>
</Empty>

View File

@@ -0,0 +1,48 @@
using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
using Moonlight.Shared.Shared.Auth;
using SerializationContext = Moonlight.Shared.SerializationContext;
namespace Moonlight.Frontend.Infrastructure.Services;
public class RemoteAuthProvider : AuthenticationStateProvider
{
private readonly HttpClient HttpClient;
private readonly ILogger<RemoteAuthProvider> Logger;
public RemoteAuthProvider(ILogger<RemoteAuthProvider> logger, HttpClient httpClient)
{
Logger = logger;
HttpClient = httpClient;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var claimResponses = await HttpClient.GetFromJsonAsync<ClaimDto[]>(
"api/auth/claims", SerializationContext.Default.Options
);
var claims = claimResponses!.Select(claim => new Claim(claim.Type, claim.Value));
return new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity(claims, "remote"))
);
}
catch (HttpRequestException e)
{
if (e.StatusCode != HttpStatusCode.Unauthorized)
Logger.LogError(e, "An api error occured while requesting claims from api");
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled error occured while requesting claims from api");
}
return new AuthenticationState(new ClaimsPrincipal());
}
}