Starting updating mooncore dependency usage

This commit is contained in:
2025-02-04 17:09:07 +01:00
parent 1a4864ba00
commit bf5a744499
38 changed files with 1099 additions and 748 deletions

View File

@@ -1,21 +0,0 @@
using Microsoft.AspNetCore.Components;
using Moonlight.Client.Interfaces;
using Moonlight.Client.Services;
namespace Moonlight.Client.Implementations;
public class AuthenticationUiHandler : IAppLoader, IAppScreen
{
public int Priority => 0;
public Task<bool> ShouldRender(IServiceProvider serviceProvider)
=> Task.FromResult(false);
public RenderFragment Render() => throw new NotImplementedException();
public async Task Load(IServiceProvider serviceProvider)
{
var identityService = serviceProvider.GetRequiredService<IdentityService>();
await identityService.Check();
}
}

View File

@@ -24,10 +24,10 @@
<PackageReference Include="Blazor-ApexCharts" Version="4.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all"/>
<PackageReference Include="MoonCore" Version="1.8.1" />
<PackageReference Include="MoonCore" Version="1.8.2" />
<PackageReference Include="MoonCore.Blazor" Version="1.2.8" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.2.6" />
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.2.9" />
</ItemGroup>
<!--

View File

@@ -1,34 +0,0 @@
using System.Text;
using Microsoft.JSInterop;
namespace Moonlight.Client.Services;
public class DownloadService
{
private readonly IJSRuntime JsRuntime;
public DownloadService(IJSRuntime jsRuntime)
{
JsRuntime = jsRuntime;
}
public async Task DownloadStream(string fileName, Stream stream)
{
using var streamRef = new DotNetStreamReference(stream);
await JsRuntime.InvokeVoidAsync("moonlight.utils.download", fileName, streamRef);
}
public async Task DownloadBytes(string fileName, byte[] bytes)
{
var ms = new MemoryStream(bytes);
await DownloadStream(fileName, ms);
ms.Close();
await ms.DisposeAsync();
}
public async Task DownloadString(string fileName, string content) =>
await DownloadBytes(fileName, Encoding.UTF8.GetBytes(content));
}

View File

@@ -1,83 +0,0 @@
using MoonCore.Attributes;
using MoonCore.Blazor.Services;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Client.Services;
[Scoped]
public class IdentityService
{
public string Username { get; private set; } = "";
public string Email { get; private set; } = "";
public string[] Permissions { get; private set; } = [];
public bool IsLoggedIn { get; private set; } = false;
private readonly HttpApiClient HttpApiClient;
private readonly LocalStorageService LocalStorageService;
public IdentityService(HttpApiClient httpApiClient, LocalStorageService localStorageService)
{
HttpApiClient = httpApiClient;
LocalStorageService = localStorageService;
}
public async Task Check()
{
IsLoggedIn = false;
var response = await HttpApiClient.GetJson<CheckResponse>("api/auth/check");
Username = response.Username;
Email = response.Email;
Permissions = response.Permissions;
IsLoggedIn = true;
//await OnStateChanged?.Invoke();
}
public async Task Logout()
{
await LocalStorageService.SetString("AccessToken", "unset");
await LocalStorageService.SetString("RefreshToken", "unset");
await LocalStorageService.Set("ExpiresAt", DateTime.MinValue);
}
public bool HasPermission(string requiredPermission)
{
// Check for wildcard permission
if (Permissions.Contains("*"))
return true;
var requiredSegments = requiredPermission.Split('.');
// Check if the user has the exact permission or a wildcard match
foreach (var permission in Permissions)
{
var permissionSegments = permission.Split('.');
// Iterate over the segments of the required permission
for (var i = 0; i < requiredSegments.Length; i++)
{
// If the current segment matches or is a wildcard, continue to the next segment
if (i < permissionSegments.Length && requiredSegments[i] == permissionSegments[i] ||
permissionSegments[i] == "*")
{
// If we've reached the end of the permissionSegments array, it means we've found a match
if (i == permissionSegments.Length - 1)
return true; // Found an exact match or a wildcard match
}
else
{
// If we reach here, it means the segments don't match and we break out of the loop
break;
}
}
}
// No matching permission found
return false;
}
}

View File

@@ -0,0 +1,124 @@
using System.Security.Claims;
using System.Web;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Auth;
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");
var url = $"{loginStartData.Endpoint}" +
$"?client_id={loginStartData.ClientId}" +
$"&redirect_uri={loginStartData.RedirectUri}" +
$"&response_type=code";
NavigationManager.NavigateTo(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", checkData.Permissions)
],
"RemoteAuthStateManager"
)
));
}
catch (HttpApiException)
{
newState = new(new ClaimsPrincipal(
new ClaimsIdentity()
));
}
return newState;
}
#endregion
}

View File

@@ -3,11 +3,9 @@ using System.Text.Json;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using MoonCore.Blazor.Extensions;
using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Extensions;
using MoonCore.Blazor.Tailwind.Forms;
using MoonCore.Blazor.Tailwind.Forms.Components;
using MoonCore.Blazor.Tailwind.Auth;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.PluginFramework.Extensions;
@@ -16,7 +14,6 @@ using Moonlight.Client.Implementations;
using Moonlight.Client.Interfaces;
using Moonlight.Client.Services;
using Moonlight.Client.UI;
using Moonlight.Client.UI.Forms;
using Moonlight.Shared.Misc;
namespace Moonlight.Client;
@@ -64,8 +61,7 @@ public class Startup
await RegisterLogging();
await RegisterBase();
await RegisterOAuth2();
await RegisterFormComponents();
await RegisterAuthentication();
await RegisterInterfaces();
await HookPluginBuild();
@@ -132,9 +128,28 @@ public class Startup
BaseAddress = new Uri(Configuration.ApiUrl)
}
);
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;
});
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
WebAssemblyHostBuilder.Services.AddScoped<DownloadService>();
WebAssemblyHostBuilder.Services.AddMoonCoreBlazorTailwind();
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
@@ -143,23 +158,6 @@ public class Startup
return Task.CompletedTask;
}
private Task RegisterOAuth2()
{
WebAssemblyHostBuilder.AddTokenAuthentication();
WebAssemblyHostBuilder.AddOAuth2();
return Task.CompletedTask;
}
private Task RegisterFormComponents()
{
FormComponentRepository.Set<string, StringComponent>();
FormComponentRepository.Set<int, IntComponent>();
FormComponentRepository.Set<DateTime, DateComponent>();
return Task.CompletedTask;
}
#region Asset Loading
private async Task LoadAssets()
@@ -339,4 +337,18 @@ public class Startup
}
#endregion
#region Authentication
private Task RegisterAuthentication()
{
WebAssemblyHostBuilder.Services.AddAuthorizationCore();
WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState();
WebAssemblyHostBuilder.Services.AddAuthenticationStateManager<RemoteAuthStateManager>();
return Task.CompletedTask;
}
#endregion
}

View File

@@ -7,8 +7,10 @@
"-m-3",
"-mx-2",
"-mx-4",
"-rotate-90",
"-translate-x-1/2",
"-translate-x-full",
"-translate-y-1/2",
"absolute",
"align-middle",
"animate-spin",
@@ -40,8 +42,12 @@
"block",
"border",
"border-0",
"border-2",
"border-b",
"border-b-2",
"border-dashed",
"border-gray-100/10",
"border-gray-600",
"border-gray-700",
"border-gray-700/60",
"border-none",
@@ -64,6 +70,7 @@
"dark:disabled:text-gray-600",
"dark:group-hover:text-gray-400",
"dark:text-gray-100",
"dark:text-gray-400",
"dark:text-gray-500",
"disabled:bg-gray-100",
"disabled:bg-gray-800",
@@ -90,6 +97,7 @@
"flex",
"flex-1",
"flex-col",
"flex-grow",
"flex-nowrap",
"flex-row",
"flex-shrink-0",
@@ -120,6 +128,7 @@
"gap-x-5",
"gap-x-6",
"gap-y-2",
"gap-y-3",
"gap-y-5",
"gap-y-7",
"gap-y-8",
@@ -138,13 +147,16 @@
"h-4",
"h-5",
"h-6",
"h-64",
"h-8",
"h-[20vh]",
"hidden",
"hover:bg-gray-600",
"hover:bg-gray-700",
"hover:bg-gray-800",
"hover:bg-primary-600",
"hover:border-b-2",
"hover:border-gray-500",
"hover:border-gray-600",
"hover:border-primary-500",
"hover:text-gray-100",
@@ -191,6 +203,7 @@
"lg:z-50",
"list-disc",
"m-1",
"m-10",
"m-3",
"max-h-56",
"max-w-3xl",
@@ -211,10 +224,15 @@
"md:grid-cols-3",
"md:h-[40vh]",
"md:items-center",
"md:ms-2",
"md:space-x-2",
"md:space-y-0",
"md:table-cell",
"md:text-3xl",
"me-1",
"me-2",
"me-2.5",
"me-3",
"min-h-full",
"min-w-60",
"ml-2",
@@ -228,6 +246,7 @@
"mr-6",
"ms-0.5",
"ms-1",
"ms-2",
"ms-3",
"mt-1",
"mt-10",
@@ -236,14 +255,14 @@
"mt-3",
"mt-4",
"mt-5",
"mt-6",
"mt-8",
"mt-auto",
"mx-0.5",
"mx-2",
"mx-5",
"mx-auto",
"my-1",
"my-3",
"my-4",
"my-8",
"opacity-0",
"opacity-100",
@@ -262,6 +281,7 @@
"p-5",
"pb-3",
"pb-4",
"pb-6",
"pl-12",
"pl-2",
"pl-3",
@@ -273,9 +293,11 @@
"pr-1",
"pr-3",
"pr-8",
"ps-4",
"pt-0.5",
"pt-5",
"pt-6",
"px-1",
"px-2",
"px-3",
"px-4",
@@ -309,8 +331,11 @@
"shadow-sm",
"shadow-xl",
"shrink-0",
"size-52",
"size-full",
"sm:-mx-6",
"sm:auto-cols-max",
"sm:block",
"sm:col-span-1",
"sm:col-span-2",
"sm:col-span-3",
@@ -344,6 +369,7 @@
"sm:pb-4",
"sm:px-6",
"sm:text-sm",
"space-x-0.5",
"space-x-1",
"space-x-2",
"space-x-5",
@@ -353,8 +379,10 @@
"space-y-4",
"space-y-8",
"sr-only",
"start-1/2",
"static",
"sticky",
"stroke-current",
"table",
"table-auto",
"text-2xl",
@@ -396,6 +424,7 @@
"to-gray-800",
"to-primary-600",
"top-0",
"top-1/2",
"transform",
"transition",
"transition-all",
@@ -413,6 +442,7 @@
"w-24",
"w-32",
"w-4",
"w-40",
"w-5",
"w-6",
"w-8",

View File

@@ -4,27 +4,6 @@
@inject ApplicationAssemblyService ApplicationAssemblyService
<ErrorLogger>
<OAuth2AuthenticationHandler>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="ApplicationAssemblyService.NavigationAssemblies">
<Found Context="routeData">
<CascadingValue Name="TargetPageType" Value="routeData.PageType">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</CascadingValue>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<div class="flex flex-col justify-center text-center">
<img class="h-48 mt-5 mb-3" src="/svg/notfound.svg" alt="Not found illustration"/>
<h3 class="mt-2 font-semibold text-white text-lg">Page not found</h3>
<p class="mt-1 text-gray-300">
The page you requested does not exist
</p>
</div>
</LayoutView>
</NotFound>
</Router>
</OAuth2AuthenticationHandler>
</ErrorLogger>
<ApplicationRouter DefaultLayout="@typeof(MainLayout)"
AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="ApplicationAssemblyService.NavigationAssemblies" />

View File

@@ -1,3 +0,0 @@
@inherits BaseFormComponent<DateTime>
<input @bind="Binder.Value" type="date" autocomplete="off" class="form-input w-full">

View File

@@ -7,7 +7,6 @@
@inherits LayoutComponentBase
@inject IdentityService IdentityService
@inject IServiceProvider ServiceProvider
@inject ILogger<MainLayout> Logger
@inject IAppLoader[] AppLoaders
@@ -40,13 +39,9 @@ else
<main class="py-10">
<div class="px-4 sm:px-6 lg:px-8">
<ErrorHandler>
<PermissionHandler CheckFunction="CheckPermission">
<CascadingValue Value="this" IsFixed="true">
@Body
</CascadingValue>
</PermissionHandler>
</ErrorHandler>
<CascadingValue Value="this" IsFixed="true">
@Body
</CascadingValue>
</div>
</main>
@@ -142,6 +137,4 @@ else
await InvokeAsync(StateHasChanged);
}
private bool CheckPermission(string permission) => IdentityService.HasPermission(permission);
}

View File

@@ -1,9 +1,10 @@
@using Moonlight.Client.Services
@using Microsoft.AspNetCore.Components.Authorization
@using MoonCore.Blazor.Tailwind.Auth
@using Moonlight.Client.UI.Layouts
@inject IdentityService IdentityService
@inject ToastService ToastService
@inject NavigationManager Navigation
@inject AuthenticationStateManager AuthStateManager
<div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 bg-gray-800/60 backdrop-blur px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
@if (Layout.ShowMobileNavigation)
@@ -26,45 +27,23 @@
<div class="flex justify-between gap-x-4 lg:gap-x-6 w-full">
<div></div>
<div class="flex items-center gap-x-4 lg:gap-x-6">
@*
<button type="button" class="-m-2.5 p-2.5 text-gray-200 hover:text-gray-100">
<span class="sr-only">View notifications</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"/>
</svg>
</button>
*@
<!-- Separator -->
<div class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-900/10" aria-hidden="true"></div>
<!-- Profile dropdown -->
<div class="relative">
<button @onclick="ToggleProfileNav" @onfocusout="ProfileNav_OnFocusOut" type="button" class="-m-1.5 flex items-center p-1.5" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open user menu</span>
<img class="h-8 w-8 rounded-full" src="https://masuowo.xyz/assets/images/avatar.png" alt="">
<span class="hidden lg:flex lg:items-center">
<span class="ml-4 text-sm font-semibold leading-6 text-gray-100" aria-hidden="true">
@("@" + IdentityService.Username)
@("@" + Username)
</span>
<svg class="ml-2 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
</svg>
</span>
</button>
<!--
Dropdown menu, show/hide based on menu state.
Entering: ""
From: "transform scale-95"
To: "transform opacity-100 scale-100"
Leaving: "transition ease-in duration-75"
From: "transform opacity-100 scale-100"
To: "transform opacity-0 scale-95"
-->
<div class="@(ShowProfileNav ? "opacity-100" : "opacity-0 hidden") transition ease-out duration-100 absolute right-0 z-10 mt-2.5 w-44 origin-top-right rounded-md bg-gray-750 py-2 shadow-lg ring-1 ring-gray-100/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
<!-- Active: "bg-gray-50", Not Active: "" -->
<a href="/admin" class="block px-3 py-1 text-sm leading-6 text-gray-100 hover:text-primary-500" role="menuitem" tabindex="-1" id="user-menu-item-0">Your profile</a>
<a @onclick="Logout" @onclick:preventDefault href="#" class="block px-3 py-1 text-sm leading-6 text-gray-100 hover:text-primary-500" role="menuitem" tabindex="-1" id="user-menu-item-1">Sign out</a>
</div>
@@ -76,8 +55,17 @@
@code
{
[Parameter] public MainLayout Layout { get; set; }
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
private bool ShowProfileNav = false;
private string Username;
protected override async Task OnInitializedAsync()
{
var identity = await AuthState;
var usernameClaim = identity.User.Claims.ToArray().First(x => x.Type == "username");
Username = usernameClaim.Value;
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
@@ -109,11 +97,5 @@
}
private async Task Logout()
{
await IdentityService.Logout();
await ToastService.Info("Successfully logged out");
//await Layout.Load();
Navigation.NavigateTo(Navigation.Uri, true);
}
=> await AuthStateManager.Logout();
}

View File

@@ -4,40 +4,19 @@
@using Moonlight.Client.UI.Layouts
@inject NavigationManager Navigation
@inject IdentityService IdentityService
@inject ISidebarItemProvider[] SidebarItemProviders
@{
var url = new Uri(Navigation.Uri);
}
<div class="relative z-40 lg:hidden transition-opacity @(Layout.ShowMobileNavigation ? "opacity-100" : "opacity-0 hidden")" role="dialog" aria-modal="true">
<div
class="relative z-40 lg:hidden transition-opacity @(Layout.ShowMobileNavigation ? "opacity-100" : "opacity-0 hidden")"
role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-gray-800/80"></div>
<div class="fixed inset-0 flex justify-center bg-gray-900">
<!--
Off-canvas menu, show/hide based on off-canvas menu state.
Entering: "transition ease-in-out duration-300 transform"
From: "-translate-x-full"
To: "translate-x-0"
Leaving: "transition ease-in-out duration-300 transform"
From: "translate-x-0"
To: "-translate-x-full"
-->
<div class="relative flex w-full max-w-xs flex-1">
<!--
Close button, show/hide based on off-canvas menu state.
Entering: "ease-in-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in-out duration-300"
From: "opacity-100"
To: "opacity-0"
-->
<!-- Sidebar component, swap this element with another sidebar if you like -->
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-900 px-6 pb-4">
<div class="flex h-16 shrink-0 items-center">a
<img class="h-8 w-auto" src="/logo.svg" alt="Moonlight">
@@ -46,40 +25,42 @@
<ul role="list" class="flex flex-1 flex-col gap-y-7">
@foreach (var group in Items)
{
<li>
@if (!string.IsNullOrEmpty(group.Key))
{
<div class="text-xs font-semibold leading-6 text-gray-400">
@group.Key
</div>
}
<ul role="list" class="-mx-2 space-y-1">
@foreach (var item in group.Value)
<li>
@if (!string.IsNullOrEmpty(group.Key))
{
var isMatch = item.RequiresExactMatch
? url.LocalPath == item.Path
: url.LocalPath.StartsWith(item.Path);
<li>
@if (isMatch)
{
<a href="@item.Path" class="bg-gray-800 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
@item.Name
</a>
}
else
{
<a href="@item.Path" class="text-gray-300 hover:text-white hover:bg-gray-800 group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
@item.Name
</a>
}
</li>
<div class="text-xs font-semibold leading-6 text-gray-400">
@group.Key
</div>
}
</ul>
</li>
<ul role="list" class="-mx-2 space-y-1">
@foreach (var item in group.Value)
{
var isMatch = item.RequiresExactMatch
? url.LocalPath == item.Path
: url.LocalPath.StartsWith(item.Path);
<li>
@if (isMatch)
{
<a href="@item.Path"
class="bg-gray-800 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
@item.Name
</a>
}
else
{
<a href="@item.Path"
class="text-gray-300 hover:text-white hover:bg-gray-800 group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
@item.Name
</a>
}
</li>
}
</ul>
</li>
}
</ul>
</nav>
@@ -88,12 +69,11 @@
</div>
</div>
<!-- Static sidebar for desktop -->
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<!-- Sidebar component, swap this element with another sidebar if you like -->
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-800/60 px-6 pb-4">
<div class="flex h-16 shrink-0 items-center">
<img class="h-8 w-auto" src="https://gamecp.masuowo.xyz/api/core/asset/Core/svg/logo.svg" alt="Your Company">
<img class="h-8 w-auto" src="https://gamecp.masuowo.xyz/api/core/asset/Core/svg/logo.svg"
alt="Your Company">
</div>
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
@@ -117,14 +97,16 @@
<li>
@if (isMatch)
{
<a href="@item.Path" class="bg-gray-800 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
<a href="@item.Path"
class="bg-gray-800 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
@item.Name
</a>
}
else
{
<a href="@item.Path" class="text-gray-300 hover:text-white hover:bg-gray-800 group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
<a href="@item.Path"
class="text-gray-300 hover:text-white hover:bg-gray-800 group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
@item.Name
</a>
@@ -149,7 +131,7 @@
{
Items = SidebarItemProviders
.SelectMany(x => x.Get())
.Where(x => x.Permission == null || (x.Permission != null && IdentityService.HasPermission(x.Permission)))
//.Where(x => x.Permission == null || (x.Permission != null && IdentityService.HasPermission(x.Permission)))
.GroupBy(x => x.Group ?? "")
.OrderByDescending(x => string.IsNullOrEmpty(x.Key))
.ToDictionary(x => x.Key, x => x.OrderBy(y => y.Priority).ToArray());

View File

@@ -1,5 +1,5 @@
@page "/admin/api"
@*
@using MoonCore.Attributes
@using MoonCore.Helpers
@using MoonCore.Models
@@ -108,4 +108,4 @@
configuration.WithField(x => x.ExpiresAt);
};
}
}
}*@

View File

@@ -0,0 +1,69 @@
@page "/admin/users/create"
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@inject ToastService ToastService
<PageHeader Title="Create User">
<a href="/admin/users" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
Create
</WButton>
</PageHeader>
<div class="mt-5">
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Name</label>
<div class="mt-2">
<input @bind="Request.Username" type="text" autocomplete="off" class="form-input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Version</label>
<div class="mt-2">
<input @bind="Request.Email" type="email" autocomplete="off" class="form-input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Author</label>
<div class="mt-2">
<input @bind="Request.Password" type="password" autocomplete="off" class="form-input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Donate Url</label>
<div class="mt-2">
<input @bind="Request.PermissionsJson" type="text" autocomplete="off" class="form-input w-full">
</div>
</div>
</div>
</HandleForm>
</div>
@code
{
private HandleForm Form;
private CreateUserRequest Request;
protected override void OnInitialized()
{
Request = new();
}
private async Task OnSubmit()
{
await ApiClient.Post("api/users", Request);
await ToastService.Success("Successfully created User");
Navigation.NavigateTo("/admin/users");
}
}

View File

@@ -1,71 +1,55 @@
@page "/admin/users"
@page "/admin/users"
@using MoonCore.Attributes
@using MoonCore.Blazor.Tailwind.Forms.Components
@using MoonCore.Helpers
@using MoonCore.Models
@using Moonlight.Shared.Http.Requests.Admin.Users
@using MoonCore.Blazor.Tailwind.Dt
@using Moonlight.Shared.Http.Responses.Admin.Users
@attribute [RequirePermission("admin.users.read")]
@inject HttpApiClient ApiClient
@inject AlertService AlertService
@inject ToastService ToastService
@inject HttpApiClient HttpApiClient
<PageHeader Title="Users" />
<Crud TItem="UserDetailResponse"
TCreateForm="CreateUserRequest"
TUpdateForm="UpdateUserRequest"
OnConfigure="OnConfigure">
<View>
<Column TItem="UserDetailResponse" Field="@(x => x.Id)" Title="Id" />
<Column TItem="UserDetailResponse" Field="@(x => x.Username)" Title="Username" />
<Column TItem="UserDetailResponse" Field="@(x => x.Email)" Title="Email" />
</View>
</Crud>
<DataTable @ref="Table" TItem="UserDetailResponse" PageSize="15" LoadItemsPaginatedAsync="LoadData">
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Id)" Name="Id" />
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Username)" Name="Username" />
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Email)" Name="Email" />
<DataTableColumn TItem="UserDetailResponse">
<ColumnTemplate>
<div class="flex justify-end">
<a href="/admin/users/update/@(context.Id)" class="text-primary-500 mr-2 sm:mr-3">
<i class="icon-pencil text-base"></i>
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
class="text-danger-500">
<i class="icon-trash text-base"></i>
</a>
</div>
</ColumnTemplate>
</DataTableColumn>
</DataTable>
@code
{
private void OnConfigure(CrudOptions<UserDetailResponse, CreateUserRequest, UpdateUserRequest> crudOptions)
private DataTable<UserDetailResponse> Table;
private async Task<IPagedData<UserDetailResponse>> LoadData(PaginationOptions options)
=> await ApiClient.GetJson<PagedData<UserDetailResponse>>($"api/admin/users?page={options.Page}&pageSize={options.PerPage}");
private async Task Delete(UserDetailResponse detailResponse)
{
crudOptions.ItemName = "User";
crudOptions.ItemLoader = async (page, pageSize)
=> await HttpApiClient.GetJson<PagedData<UserDetailResponse>>($"api/admin/users?page={page}&pageSize={pageSize}");
await AlertService.ConfirmDanger(
"User deletion",
$"Do you really want to delete the user '{detailResponse.Username}'",
async () =>
{
await ApiClient.Delete($"api/admin/users/{detailResponse.Id}");
await ToastService.Success("Successfully deleted user");
crudOptions.SingleItemLoader = async id
=> await HttpApiClient.GetJson<UserDetailResponse>($"api/admin/users/{id}");
crudOptions.QueryIdentifier = response => response.Id.ToString();
crudOptions.OnCreate = async request
=> await HttpApiClient.Post("api/admin/users", request);
crudOptions.OnUpdate = async (item, request)
=> await HttpApiClient.Patch($"api/admin/users/{item.Id}", request);
crudOptions.OnDelete = async item
=> await HttpApiClient.Delete($"api/admin/users/{item.Id}");
crudOptions.OnConfigureCreate = configuration =>
{
configuration.WithField(x => x.Username);
configuration.WithField(x => x.Email);
configuration.WithField(x => x.Password)
.WithComponent<StringComponent>(component => component.Type = "password");
configuration.WithField(x => x.PermissionsJson)
.WithComponent<TagComponent>();
};
crudOptions.OnConfigureUpdate = (_, configuration) =>
{
configuration.WithField(x => x.Username);
configuration.WithField(x => x.Email);
configuration.WithField(x => x.Password, fieldConfiguration =>
{
fieldConfiguration.Description = "Optional. Specify if you want to change this accounts password";
})
.WithComponent<StringComponent>(component => component.Type = "password");
};
await Table.Refresh();
}
);
}
}
}

View File

@@ -0,0 +1,69 @@
@page "/users/update/{Id:int}"
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses.Admin.Users
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@inject ToastService ToastService
<LazyLoader Load="Load">
<PageHeader Title="Update User">
<a href="/admin/users" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
Update
</WButton>
</PageHeader>
<div class="mt-5">
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Name</label>
<div class="mt-2">
<input @bind="Request.Username" type="text" autocomplete="off" class="form-input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Version</label>
<div class="mt-2">
<input @bind="Request.Email" type="email" autocomplete="off" class="form-input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Author</label>
<div class="mt-2">
<input @bind="Request.Password" type="password" autocomplete="off" class="form-input w-full">
</div>
</div>
</div>
</HandleForm>
</div>
</LazyLoader>
@code
{
[Parameter] public int Id { get; set; }
private HandleForm Form;
private UpdateUserRequest Request;
private async Task Load(LazyLoader _)
{
var detail = await ApiClient.GetJson<UserDetailResponse>($"api/users/{Id}");
Request = Mapper.Map<UpdateUserRequest>(detail);
}
private async Task OnSubmit()
{
await ApiClient.Patch($"api/admin/users/{Id}", Request);
await ToastService.Success("Successfully updated User");
Navigation.NavigateTo("/admin/users");
}
}

View File

@@ -1,14 +1,26 @@
@page "/"
@using Moonlight.Client.Services
@inject IdentityService IdentityService
@using Microsoft.AspNetCore.Components.Authorization
<div class="font-medium leading-[1.1] tracking-tight">
<div class="animate-shimmer bg-gradient-to-r from-violet-400 via-sky-400 to-purple-400 bg-clip-text font-semibold text-transparent text-3xl" style="animation-duration: 5s; background-size: 200% 100%">
Welcome, @(IdentityService.Username)
Welcome, @(Username)
</div>
<div class="text-gray-200 text-2xl">What do you want to do today?</div>
</div>
<div class="text-primary-500/10"></div>
<div class="text-primary-500/10"></div>
@code
{
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
private string Username;
protected override async Task OnInitializedAsync()
{
var identity = await AuthState;
var usernameClaim = identity.User.Claims.ToArray().First(x => x.Type == "username");
Username = usernameClaim.Value;
}
}

View File

@@ -10,8 +10,6 @@
@using MoonCore.Blazor.Tailwind.Components
@using MoonCore.Blazor.Tailwind.Alerts
@using MoonCore.Blazor.Tailwind.Crud
@using MoonCore.Blazor.Tailwind.Forms
@using MoonCore.Blazor.Tailwind.Helpers
@using MoonCore.Blazor.Tailwind.Modals
@using MoonCore.Blazor.Tailwind.Services