From 9d557eea4e865e0e90e9ee6a0a6d966f7b407f3e Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Sat, 21 Feb 2026 22:20:51 +0100 Subject: [PATCH] Implemented extendable system settings tab. Started implementing white labeling settings --- .../Constants/FrontendSettingConstants.cs | 6 ++ .../Admin/Settings/WhiteLabelingController.cs | 49 ++++++++++++ Moonlight.Api/Services/FrontendService.cs | 9 ++- .../Configuration/SystemSettingsOptions.cs | 34 +++++++++ .../Implementations/PermissionProvider.cs | 1 + Moonlight.Frontend/Startup/Startup.Base.cs | 13 +++- .../Admin/Settings/WhiteLabelingSetting.razor | 75 +++++++++++++++++++ .../UI/Admin/Views/Sys/Index.razor | 37 +++++---- .../UI/Admin/Views/Sys/Settings.razor | 52 +++++++++++++ .../Admin/Settings/SetWhiteLabelingDto.cs | 9 +++ .../Admin/Settings/WhiteLabelingDto.cs | 6 ++ Moonlight.Shared/Http/SerializationContext.cs | 6 ++ Moonlight.Shared/Permissions.cs | 1 + 13 files changed, 276 insertions(+), 22 deletions(-) create mode 100644 Moonlight.Api/Constants/FrontendSettingConstants.cs create mode 100644 Moonlight.Api/Http/Controllers/Admin/Settings/WhiteLabelingController.cs create mode 100644 Moonlight.Frontend/Configuration/SystemSettingsOptions.cs create mode 100644 Moonlight.Frontend/UI/Admin/Settings/WhiteLabelingSetting.razor create mode 100644 Moonlight.Frontend/UI/Admin/Views/Sys/Settings.razor create mode 100644 Moonlight.Shared/Http/Requests/Admin/Settings/SetWhiteLabelingDto.cs create mode 100644 Moonlight.Shared/Http/Responses/Admin/Settings/WhiteLabelingDto.cs diff --git a/Moonlight.Api/Constants/FrontendSettingConstants.cs b/Moonlight.Api/Constants/FrontendSettingConstants.cs new file mode 100644 index 00000000..1c4e81aa --- /dev/null +++ b/Moonlight.Api/Constants/FrontendSettingConstants.cs @@ -0,0 +1,6 @@ +namespace Moonlight.Api.Constants; + +public class FrontendSettingConstants +{ + public const string Name = "Moonlight.Frontend.Name"; +} \ No newline at end of file diff --git a/Moonlight.Api/Http/Controllers/Admin/Settings/WhiteLabelingController.cs b/Moonlight.Api/Http/Controllers/Admin/Settings/WhiteLabelingController.cs new file mode 100644 index 00000000..1fee7384 --- /dev/null +++ b/Moonlight.Api/Http/Controllers/Admin/Settings/WhiteLabelingController.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Moonlight.Api.Constants; +using Moonlight.Api.Services; +using Moonlight.Shared; +using Moonlight.Shared.Http.Requests.Admin.Settings; +using Moonlight.Shared.Http.Responses.Admin.Settings; + +namespace Moonlight.Api.Http.Controllers.Admin.Settings; + +[ApiController] +[Authorize(Policy = Permissions.System.Settings)] +[Route("api/admin/system/settings/whiteLabeling")] +public class WhiteLabelingController : Controller +{ + private readonly SettingsService SettingsService; + private readonly FrontendService FrontendService; + + public WhiteLabelingController(SettingsService settingsService, FrontendService frontendService) + { + SettingsService = settingsService; + FrontendService = frontendService; + } + + [HttpGet] + public async Task> GetAsync() + { + var dto = new WhiteLabelingDto + { + Name = await SettingsService.GetValueAsync(FrontendSettingConstants.Name) ?? "Moonlight" + }; + + return dto; + } + + [HttpPost] + public async Task> PostAsync([FromBody] SetWhiteLabelingDto request) + { + await SettingsService.SetValueAsync(FrontendSettingConstants.Name, request.Name); + await FrontendService.ResetCacheAsync(); + + var dto = new WhiteLabelingDto + { + Name = await SettingsService.GetValueAsync(FrontendSettingConstants.Name) ?? "Moonlight" + }; + + return dto; + } +} \ No newline at end of file diff --git a/Moonlight.Api/Services/FrontendService.cs b/Moonlight.Api/Services/FrontendService.cs index 11fd625a..f3fa3eb4 100644 --- a/Moonlight.Api/Services/FrontendService.cs +++ b/Moonlight.Api/Services/FrontendService.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Moonlight.Api.Configuration; +using Moonlight.Api.Constants; using Moonlight.Api.Database; using Moonlight.Api.Database.Entities; using Moonlight.Api.Models; @@ -13,14 +14,16 @@ public class FrontendService private readonly IMemoryCache Cache; private readonly DatabaseRepository ThemeRepository; private readonly IOptions Options; + private readonly SettingsService SettingsService; private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}"; - public FrontendService(IMemoryCache cache, DatabaseRepository themeRepository, IOptions options) + public FrontendService(IMemoryCache cache, DatabaseRepository themeRepository, IOptions options, SettingsService settingsService) { Cache = cache; ThemeRepository = themeRepository; Options = options; + SettingsService = settingsService; } public async Task GetConfigurationAsync() @@ -35,7 +38,9 @@ public class FrontendService .Query() .FirstOrDefaultAsync(x => x.IsEnabled); - var config = new FrontendConfiguration("Moonlight", theme?.CssContent); + var name = await SettingsService.GetValueAsync(FrontendSettingConstants.Name); + + var config = new FrontendConfiguration(name ?? "Moonlight", theme?.CssContent); Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes)); diff --git a/Moonlight.Frontend/Configuration/SystemSettingsOptions.cs b/Moonlight.Frontend/Configuration/SystemSettingsOptions.cs new file mode 100644 index 00000000..8e4e022d --- /dev/null +++ b/Moonlight.Frontend/Configuration/SystemSettingsOptions.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; + +namespace Moonlight.Frontend.Configuration; + +public class SystemSettingsOptions +{ + public IReadOnlyList Components => InnerComponents; + + private readonly List InnerComponents = new(); + + public void Add(string name, string description, int order) + where TIcon : ComponentBase where TComponent : ComponentBase + => Add(name, description, order, typeof(TIcon), typeof(TComponent)); + + public void Add(string name, string description, int order, Type iconComponent, Type component) + => InnerComponents.Add(new SystemSettingsPage(name, description, order, iconComponent, component)); + + public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() + where TComponent : ComponentBase + => Remove(typeof(TComponent)); + + public void Remove(Type componentType) + => InnerComponents.RemoveAll(x => x.ComponentType == componentType); +} + +public record SystemSettingsPage( + string Name, + string Description, + int Order, + [property: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type IconComponentType, + Type ComponentType +); \ No newline at end of file diff --git a/Moonlight.Frontend/Implementations/PermissionProvider.cs b/Moonlight.Frontend/Implementations/PermissionProvider.cs index 739c11ec..d3cc7c73 100644 --- a/Moonlight.Frontend/Implementations/PermissionProvider.cs +++ b/Moonlight.Frontend/Implementations/PermissionProvider.cs @@ -29,6 +29,7 @@ public sealed class PermissionProvider : IPermissionProvider 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"), diff --git a/Moonlight.Frontend/Startup/Startup.Base.cs b/Moonlight.Frontend/Startup/Startup.Base.cs index cf145d53..1c377c50 100644 --- a/Moonlight.Frontend/Startup/Startup.Base.cs +++ b/Moonlight.Frontend/Startup/Startup.Base.cs @@ -1,3 +1,4 @@ +using LucideBlazor; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; @@ -6,6 +7,7 @@ using Moonlight.Frontend.Implementations; using Moonlight.Frontend.Interfaces; using Moonlight.Frontend.Services; using Moonlight.Frontend.UI; +using Moonlight.Frontend.UI.Admin.Settings; using ShadcnBlazor; using ShadcnBlazor.Extras; @@ -19,7 +21,7 @@ public partial class Startup builder.RootComponents.Add("head::after"); builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); - + builder.Services.AddShadcnBlazor(); builder.Services.AddShadcnBlazorExtras(); @@ -31,5 +33,14 @@ public partial class Startup { options.Assemblies.Add(typeof(Startup).Assembly); }); + + builder.Services.Configure(options => + { + options.Add( + "White Labeling", + "Settings for white labeling your moonlight instance", + 0 + ); + }); } } \ No newline at end of file diff --git a/Moonlight.Frontend/UI/Admin/Settings/WhiteLabelingSetting.razor b/Moonlight.Frontend/UI/Admin/Settings/WhiteLabelingSetting.razor new file mode 100644 index 00000000..270416d3 --- /dev/null +++ b/Moonlight.Frontend/UI/Admin/Settings/WhiteLabelingSetting.razor @@ -0,0 +1,75 @@ +@using LucideBlazor +@using Moonlight.Frontend.Helpers +@using Moonlight.Frontend.Services +@using Moonlight.Shared.Http +@using Moonlight.Shared.Http.Requests.Admin.Settings +@using Moonlight.Shared.Http.Responses.Admin.Settings +@using ShadcnBlazor.Extras.Common +@using ShadcnBlazor.Extras.Forms +@using ShadcnBlazor.Extras.Toasts +@using ShadcnBlazor.Fields +@using ShadcnBlazor.Inputs + +@inject HttpClient HttpClient +@inject ToastService ToastService +@inject FrontendService FrontendService + + + + + +
+ + + + + Name + + + +
+ + + + Save + +
+
+ +@code +{ + private SetWhiteLabelingDto Request; + + private async Task LoadAsync(LazyLoader _) + { + var dto = await HttpClient.GetFromJsonAsync( + "api/admin/system/settings/whiteLabeling", + SerializationContext.Default.Options + ); + + Request = new SetWhiteLabelingDto() + { + Name = dto!.Name + }; + } + + private async Task OnValidSubmit(EditContext editContext, ValidationMessageStore validationMessageStore) + { + var response = await HttpClient.PostAsJsonAsync( + "api/admin/system/settings/whiteLabeling", + Request, + SerializationContext.Default.Options + ); + + if (response.IsSuccessStatusCode) + { + await FrontendService.ReloadAsync(); + await ToastService.SuccessAsync("Setting", "Successfully updated white labeling settings"); + + return true; + } + + await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore); + return false; + } +} \ No newline at end of file diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/Index.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/Index.razor index 3f09ac48..78a53dd6 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Sys/Index.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Sys/Index.razor @@ -4,18 +4,14 @@ @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Moonlight.Shared -@using ShadcnBlazor.Buttons -@using ShadcnBlazor.Cards -@using ShadcnBlazor.Inputs @using ShadcnBlazor.Tab -@using ShadcnBlazor.Labels @inject NavigationManager Navigation @inject IAuthorizationService AuthorizationService - + Settings @@ -27,7 +23,7 @@ API & API Keys - + Diagnose @@ -36,19 +32,18 @@ Instance - - - - - - - - - - + @if (SettingsResult.Succeeded) + { + + + + } + @if (DiagnoseResult.Succeeded) + { + + + + } @if (ApiKeyAccess.Succeeded) { @@ -81,6 +76,8 @@ private AuthorizationResult ThemesAccess; private AuthorizationResult InstanceResult; private AuthorizationResult VersionsResult; + private AuthorizationResult SettingsResult; + private AuthorizationResult DiagnoseResult; protected override async Task OnInitializedAsync() { @@ -90,6 +87,8 @@ ThemesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.View); InstanceResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Versions); VersionsResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Instance); + SettingsResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Settings); + DiagnoseResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Diagnose); } private void OnTabChanged(string name) diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/Settings.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/Settings.razor new file mode 100644 index 00000000..79d3f66a --- /dev/null +++ b/Moonlight.Frontend/UI/Admin/Views/Sys/Settings.razor @@ -0,0 +1,52 @@ +@using Microsoft.Extensions.Options +@using Moonlight.Frontend.Configuration +@using ShadcnBlazor.Cards +@using ShadcnBlazor.Sidebars + +@inject IOptions Options + +
+ + + @foreach (var menuPage in Pages) + { + + + @menuPage.Name + + } + + + @if (CurrentPage != null) + { + + + @CurrentPage.Name + @CurrentPage.Description + + + + + + } +
+ +@code +{ + private SystemSettingsPage[] Pages; + private SystemSettingsPage? CurrentPage; + + protected override void OnInitialized() + { + Pages = Options + .Value + .Components + .OrderBy(x => x.Order) + .ToArray(); + + CurrentPage = Pages.FirstOrDefault(); + } + + private void Navigate(SystemSettingsPage page) + => CurrentPage = page; +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Requests/Admin/Settings/SetWhiteLabelingDto.cs b/Moonlight.Shared/Http/Requests/Admin/Settings/SetWhiteLabelingDto.cs new file mode 100644 index 00000000..c0d2fa4a --- /dev/null +++ b/Moonlight.Shared/Http/Requests/Admin/Settings/SetWhiteLabelingDto.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Shared.Http.Requests.Admin.Settings; + +public class SetWhiteLabelingDto +{ + [Required] + public string Name { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Admin/Settings/WhiteLabelingDto.cs b/Moonlight.Shared/Http/Responses/Admin/Settings/WhiteLabelingDto.cs new file mode 100644 index 00000000..85697665 --- /dev/null +++ b/Moonlight.Shared/Http/Responses/Admin/Settings/WhiteLabelingDto.cs @@ -0,0 +1,6 @@ +namespace Moonlight.Shared.Http.Responses.Admin.Settings; + +public class WhiteLabelingDto +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/SerializationContext.cs b/Moonlight.Shared/Http/SerializationContext.cs index 1e8459ee..85ec9019 100644 --- a/Moonlight.Shared/Http/SerializationContext.cs +++ b/Moonlight.Shared/Http/SerializationContext.cs @@ -4,12 +4,14 @@ using Moonlight.Shared.Http.Events; using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Http.Requests.Admin.ContainerHelper; using Moonlight.Shared.Http.Requests.Admin.Roles; +using Moonlight.Shared.Http.Requests.Admin.Settings; using Moonlight.Shared.Http.Requests.Admin.Themes; using Moonlight.Shared.Http.Requests.Admin.Users; using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin.ApiKeys; using Moonlight.Shared.Http.Responses.Admin.Auth; +using Moonlight.Shared.Http.Responses.Admin.Settings; using Moonlight.Shared.Http.Responses.Admin.Themes; using Moonlight.Shared.Http.Responses.Admin.Users; @@ -59,6 +61,10 @@ namespace Moonlight.Shared.Http; [JsonSerializable(typeof(VersionDto))] [JsonSerializable(typeof(ProblemDetails))] +// Settings - White Labeling +[JsonSerializable(typeof(WhiteLabelingDto))] +[JsonSerializable(typeof(SetWhiteLabelingDto))] + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)] public partial class SerializationContext : JsonSerializerContext { diff --git a/Moonlight.Shared/Permissions.cs b/Moonlight.Shared/Permissions.cs index 96d8a1df..304b87e8 100644 --- a/Moonlight.Shared/Permissions.cs +++ b/Moonlight.Shared/Permissions.cs @@ -55,5 +55,6 @@ public static class Permissions public const string Diagnose = $"{Prefix}{Section}.{nameof(Diagnose)}"; public const string Versions = $"{Prefix}{Section}.{nameof(Versions)}"; public const string Instance = $"{Prefix}{Section}.{nameof(Instance)}"; + public const string Settings = $"{Prefix}{Section}.{nameof(Settings)}"; } } \ No newline at end of file