Refactored project to module structure
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Moonlight.Frontend.Infrastructure.Configuration;
|
||||
|
||||
public class NavigationAssemblyOptions
|
||||
{
|
||||
public List<Assembly> Assemblies { get; private set; } = new();
|
||||
}
|
||||
46
Moonlight.Frontend/Infrastructure/Helpers/Formatter.cs
Normal file
46
Moonlight.Frontend/Infrastructure/Helpers/Formatter.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Moonlight.Frontend.Admin.Users.Shared;
|
||||
|
||||
namespace Moonlight.Frontend.Infrastructure.Hooks;
|
||||
|
||||
public interface IPermissionProvider
|
||||
{
|
||||
public Task<PermissionCategory[]> GetPermissionsAsync();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Moonlight.Frontend.Infrastructure.Models;
|
||||
|
||||
namespace Moonlight.Frontend.Infrastructure.Hooks;
|
||||
|
||||
public interface ISidebarProvider
|
||||
{
|
||||
public Task<SidebarItem[]> GetItemsAsync();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Moonlight.Frontend.Infrastructure.Hooks;
|
||||
|
||||
public abstract class LayoutMiddlewareBase : ComponentBase
|
||||
{
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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")
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
19
Moonlight.Frontend/Infrastructure/Models/SidebarItem.cs
Normal file
19
Moonlight.Frontend/Infrastructure/Models/SidebarItem.cs
Normal 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; }
|
||||
}
|
||||
97
Moonlight.Frontend/Infrastructure/Partials/App.razor
Normal file
97
Moonlight.Frontend/Infrastructure/Partials/App.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
126
Moonlight.Frontend/Infrastructure/Partials/AppSidebar.razor
Normal file
126
Moonlight.Frontend/Infrastructure/Partials/AppSidebar.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Moonlight.Frontend/Infrastructure/Partials/MainLayout.razor
Normal file
52
Moonlight.Frontend/Infrastructure/Partials/MainLayout.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
78
Moonlight.Frontend/Infrastructure/Partials/NavUser.razor
Normal file
78
Moonlight.Frontend/Infrastructure/Partials/NavUser.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
23
Moonlight.Frontend/Infrastructure/Partials/NotFound.razor
Normal file
23
Moonlight.Frontend/Infrastructure/Partials/NotFound.razor
Normal 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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user