4 Commits

13 changed files with 277 additions and 22 deletions

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Constants;
public class FrontendSettingConstants
{
public const string Name = "Moonlight.Frontend.Name";
}

View File

@@ -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<ActionResult<WhiteLabelingDto>> GetAsync()
{
var dto = new WhiteLabelingDto
{
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"
};
return dto;
}
[HttpPost]
public async Task<ActionResult<WhiteLabelingDto>> PostAsync([FromBody] SetWhiteLabelingDto request)
{
await SettingsService.SetValueAsync(FrontendSettingConstants.Name, request.Name);
await FrontendService.ResetCacheAsync();
var dto = new WhiteLabelingDto
{
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"
};
return dto;
}
}

View File

@@ -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<Theme> ThemeRepository;
private readonly IOptions<FrontendOptions> Options;
private readonly SettingsService SettingsService;
private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}";
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options)
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options, SettingsService settingsService)
{
Cache = cache;
ThemeRepository = themeRepository;
Options = options;
SettingsService = settingsService;
}
public async Task<FrontendConfiguration> 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<string>(FrontendSettingConstants.Name);
var config = new FrontendConfiguration(name ?? "Moonlight", theme?.CssContent);
Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes));

View File

@@ -0,0 +1,35 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
namespace Moonlight.Frontend.Configuration;
public class SystemSettingsOptions
{
public IReadOnlyList<SystemSettingsPage> Components => InnerComponents;
private readonly List<SystemSettingsPage> InnerComponents = new();
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TIcon,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>(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, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type iconComponent, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] 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([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType)
=> InnerComponents.RemoveAll(x => x.ComponentType == componentType);
}
public record SystemSettingsPage(
string Name,
string Description,
int Order,
Type IconComponentType,
Type ComponentType
);

View File

@@ -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"),

View File

@@ -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;
@@ -31,5 +33,14 @@ public partial class Startup
{
options.Assemblies.Add(typeof(Startup).Assembly);
});
builder.Services.Configure<SystemSettingsOptions>(options =>
{
options.Add<TextCursorInputIcon, WhiteLabelingSetting>(
"White Labeling",
"Settings for white labeling your moonlight instance",
0
);
});
}
}

View File

@@ -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
<LazyLoader Load="LoadAsync">
<EnhancedEditForm Model="Request" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator />
<FieldSet>
<FormValidationSummary />
<FieldGroup>
<Field>
<FieldLabel>Name</FieldLabel>
<TextInputField @bind-Value="Request.Name"/>
</Field>
</FieldGroup>
</FieldSet>
<SubmitButton ClassName="mt-3">
<SaveIcon/>
Save
</SubmitButton>
</EnhancedEditForm>
</LazyLoader>
@code
{
private SetWhiteLabelingDto Request;
private async Task LoadAsync(LazyLoader _)
{
var dto = await HttpClient.GetFromJsonAsync<WhiteLabelingDto>(
"api/admin/system/settings/whiteLabeling",
SerializationContext.Default.Options
);
Request = new SetWhiteLabelingDto()
{
Name = dto!.Name
};
}
private async Task<bool> 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;
}
}

View File

@@ -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
<Tabs DefaultValue="@(Tab ?? "settings")" OnValueChanged="OnTabChanged">
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
<TabsTrigger Value="settings">
<TabsTrigger Value="settings" Disabled="@(!SettingsResult.Succeeded)">
<CogIcon />
Settings
</TabsTrigger>
@@ -27,7 +23,7 @@
<KeyIcon/>
API & API Keys
</TabsTrigger>
<TabsTrigger Value="diagnose">
<TabsTrigger Value="diagnose" Disabled="@(!DiagnoseResult.Succeeded)">
<HeartPulseIcon/>
Diagnose
</TabsTrigger>
@@ -36,19 +32,18 @@
Instance
</TabsTrigger>
</TabsList>
@if (SettingsResult.Succeeded)
{
<TabsContent Value="settings">
<Card ClassName="mt-5">
<CardFooter ClassName="justify-end">
<Button>
<SaveIcon />
Save changes
</Button>
</CardFooter>
</Card>
<Settings />
</TabsContent>
}
@if (DiagnoseResult.Succeeded)
{
<TabsContent Value="diagnose">
<Diagnose />
</TabsContent>
}
@if (ApiKeyAccess.Succeeded)
{
<TabsContent Value="apiKeys">
@@ -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)

View File

@@ -0,0 +1,52 @@
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Configuration
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Sidebars
@inject IOptions<SystemSettingsOptions> Options
<div class="mt-5 flex flex-col md:flex-row gap-5">
<Card ClassName="flex py-2 grow-0 min-w-56 max-h-[65vh] md:min-h-[65vh]">
<CardContent ClassName="px-2 flex flex-col gap-y-1 h-full max-h-[65vh] overflow-y-auto scrollbar-thin">
@foreach (var menuPage in Pages)
{
<SidebarMenuButton @onclick="() => Navigate(menuPage)" IsActive="@(CurrentPage == menuPage)" ClassName="overflow-visible">
<DynamicComponent Type="@menuPage.IconComponentType" />
<span>@menuPage.Name</span>
</SidebarMenuButton>
}
</CardContent>
</Card>
@if (CurrentPage != null)
{
<Card ClassName="flex grow">
<CardHeader>
<CardTitle>@CurrentPage.Name</CardTitle>
<CardDescription>@CurrentPage.Description</CardDescription>
</CardHeader>
<CardContent>
<DynamicComponent Type="@CurrentPage.ComponentType" />
</CardContent>
</Card>
}
</div>
@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;
}

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.Settings;
public class SetWhiteLabelingDto
{
[Required]
public string Name { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Shared.Http.Responses.Admin.Settings;
public class WhiteLabelingDto
{
public string Name { get; set; }
}

View File

@@ -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
{

View File

@@ -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)}";
}
}