Refactored project to module structure
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user