Compare commits
24 Commits
dd44e5bb86
...
v2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dff8c8f6d | |||
| 95a848e571 | |||
| 9d557eea4e | |||
| 94c1aac0ac | |||
| 3bddd64d91 | |||
| 5ad7a6db7b | |||
| 9b9272cd6e | |||
| 31cf34ed04 | |||
| a9b0020131 | |||
| e3b432aae6 | |||
| 06f27605ba | |||
| 0bd138df63 | |||
| d7b725f541 | |||
| 0f26aaf803 | |||
| c45e177001 | |||
| 627e9bb161 | |||
| 1fc33ebf03 | |||
| 64e4d7201e | |||
| 816aa01319 | |||
| 5627e78843 | |||
| 795cec149f | |||
| 83fcb4a921 | |||
| 741a60adc6 | |||
| 6f941a220c |
@@ -34,7 +34,7 @@ jobs:
|
||||
# Publish frontend
|
||||
# We need to build it first so the class list files generate
|
||||
- name: Build project
|
||||
run: dotnet build Moonlight.Frontend --configuration Debug
|
||||
run: dotnet build Hosts/Moonlight.Frontend.Host --configuration Debug
|
||||
|
||||
- name: Build tailwind styles and extract class list
|
||||
working-directory: Hosts/Moonlight.Frontend.Host/Styles
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -405,4 +405,5 @@ FodyWeavers.xsd
|
||||
# Secrets
|
||||
**/.env
|
||||
**/appsettings.json
|
||||
**/appsettings.Development.json
|
||||
**/appsettings.Development.json
|
||||
**/storage
|
||||
6
Hosts/Moonlight.Api.Host/Api.props
Normal file
6
Hosts/Moonlight.Api.Host/Api.props
Normal file
@@ -0,0 +1,6 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<!-- Put your plugin references here -->
|
||||
<!-- E.g. <PackageReference Include="MoonlightServers.Api" Version="2.1.0" /> -->
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,10 +0,0 @@
|
||||
using MoonCore.PluginFramework;
|
||||
using Moonlight.Api.Startup;
|
||||
|
||||
namespace Moonlight.Api.Host;
|
||||
|
||||
[PluginLoader]
|
||||
public partial class AppStartupLoader : IAppStartup
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
# Base image
|
||||
FROM cgr.dev/chainguard/aspnet-runtime:latest AS base
|
||||
FROM git.battlestati.one/moonlight-panel/app_base:moonlight AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
|
||||
# Build dependencies
|
||||
RUN apt-get update; apt-get install nodejs npm -y; apt-get clean
|
||||
# Install required packages
|
||||
RUN apt-get update; apt-get install unzip -y; apt-get clean
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
ENV PATH="/root/.bun/bin:${PATH}"
|
||||
|
||||
# Build options
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
@@ -15,7 +17,7 @@ WORKDIR /src/Hosts/Moonlight.Frontend.Host/Styles
|
||||
|
||||
COPY ["Hosts/Moonlight.Frontend.Host/Styles/package.json", "package.json"]
|
||||
|
||||
RUN npm install
|
||||
RUN bun install
|
||||
|
||||
# Restore nuget packages
|
||||
WORKDIR /src
|
||||
@@ -27,6 +29,9 @@ COPY ["Moonlight.Shared/Moonlight.Shared.csproj", "Moonlight.Shared/"]
|
||||
COPY ["Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj", "Hosts/Moonlight.Frontend.Host/"]
|
||||
COPY ["Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj", "Hosts/Moonlight.Api.Host/"]
|
||||
|
||||
COPY ["Hosts/Moonlight.Frontend.Host/Frontend.props", "Hosts/Moonlight.Frontend.Host/"]
|
||||
COPY ["Hosts/Moonlight.Api.Host/Api.props", "Hosts/Moonlight.Api.Host/"]
|
||||
|
||||
RUN dotnet restore "Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj"
|
||||
RUN dotnet restore "Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj"
|
||||
|
||||
@@ -39,7 +44,7 @@ WORKDIR "/src/Hosts/Moonlight.Frontend.Host"
|
||||
RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
|
||||
|
||||
WORKDIR "/src/Hosts/Moonlight.Frontend.Host/Styles"
|
||||
RUN npm run build
|
||||
RUN bun run build
|
||||
|
||||
# Build projects
|
||||
WORKDIR "/src/Hosts/Moonlight.Api.Host"
|
||||
@@ -67,4 +72,6 @@ WORKDIR /app
|
||||
COPY --from=publish /app/publish-api .
|
||||
COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD ["/usr/bin/curl", "-sf", "-o", "/dev/null", "http://localhost:8080/"]
|
||||
|
||||
ENTRYPOINT ["dotnet", "Moonlight.Api.Host.dll"]
|
||||
@@ -7,15 +7,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
|
||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2" />
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -29,4 +27,5 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="Api.props"/>
|
||||
</Project>
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
using Moonlight.Api.Host;
|
||||
using Moonlight.Api;
|
||||
using SimplePlugin.Generated;
|
||||
|
||||
var appLoader = new AppStartupLoader();
|
||||
appLoader.Initialize();
|
||||
var plugins = PluginRegistry
|
||||
.Modules
|
||||
.OfType<MoonlightPlugin>()
|
||||
.ToArray();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
appLoader.PreBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
appLoader.PostBuild(app);
|
||||
|
||||
appLoader.PostMiddleware(app);
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
app.UseWebAssemblyDebugging();
|
||||
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
await app.RunAsync();
|
||||
await StartupHandler.RunAsync(args, plugins);
|
||||
@@ -1,10 +0,0 @@
|
||||
using MoonCore.PluginFramework;
|
||||
using Moonlight.Frontend.Startup;
|
||||
|
||||
namespace Moonlight.Frontend.Host;
|
||||
|
||||
[PluginLoader]
|
||||
public partial class AppStartupLoader : IAppStartup
|
||||
{
|
||||
|
||||
}
|
||||
6
Hosts/Moonlight.Frontend.Host/Frontend.props
Normal file
6
Hosts/Moonlight.Frontend.Host/Frontend.props
Normal file
@@ -0,0 +1,6 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<!-- Put your plugin references here -->
|
||||
<!-- E.g. <PackageReference Include="MoonlightServers.Frontend" Version="2.1.0" /> -->
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -14,11 +14,13 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
|
||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
|
||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2" />
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="Frontend.props"/>
|
||||
</Project>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Moonlight.Frontend.Host;
|
||||
using Moonlight.Frontend;
|
||||
using SimplePlugin.Generated;
|
||||
|
||||
var appLoader = new AppStartupLoader();
|
||||
appLoader.Initialize();
|
||||
var plugins = PluginRegistry
|
||||
.Modules
|
||||
.OfType<MoonlightPlugin>()
|
||||
.ToArray();
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
appLoader.PreBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
appLoader.PostBuild(app);
|
||||
|
||||
await app.RunAsync();
|
||||
await StartupHandler.RunAsync(args, plugins);
|
||||
@@ -1,11 +1,11 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/scrollbar.css";
|
||||
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/default-theme.css";
|
||||
@import "../bin/ShadcnBlazor/scrollbar.css";
|
||||
@import "../bin/ShadcnBlazor/default-theme.css";
|
||||
@import "./theme.css";
|
||||
|
||||
@source "../../../Moonlight.Frontend/bin/ShadcnBlazor/ShadcnBlazor.map";
|
||||
@source "../bin/ShadcnBlazor/ShadcnBlazor.map";
|
||||
|
||||
@source "../../../Moonlight.Api/**/*.razor";
|
||||
@source "../../../Moonlight.Api/**/*.cs";
|
||||
|
||||
@@ -2,5 +2,6 @@ namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class ApiOptions
|
||||
{
|
||||
public int LookupCacheMinutes { get; set; } = 3;
|
||||
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||
}
|
||||
6
Moonlight.Api/Configuration/CacheOptions.cs
Normal file
6
Moonlight.Api/Configuration/CacheOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class CacheOptions
|
||||
{
|
||||
public bool EnableLayer2 { get; set; }
|
||||
}
|
||||
7
Moonlight.Api/Configuration/RedisOptions.cs
Normal file
7
Moonlight.Api/Configuration/RedisOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class RedisOptions
|
||||
{
|
||||
public bool Enable { get; set; }
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class SessionOptions
|
||||
{
|
||||
public int ValidationCacheMinutes { get; set; } = 3;
|
||||
}
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
public class SettingsOptions
|
||||
{
|
||||
public int CacheMinutes { get; set; } = 3;
|
||||
public TimeSpan LookupL1CacheTime { get; set; } = TimeSpan.FromMinutes(1);
|
||||
public TimeSpan LookupL2CacheTime { get; set; } = TimeSpan.FromMinutes(5);
|
||||
}
|
||||
7
Moonlight.Api/Configuration/UserOptions.cs
Normal file
7
Moonlight.Api/Configuration/UserOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class UserOptions
|
||||
{
|
||||
public TimeSpan ValidationCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||
public TimeSpan ValidationCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||
}
|
||||
6
Moonlight.Api/Constants/FrontendSettingConstants.cs
Normal file
6
Moonlight.Api/Constants/FrontendSettingConstants.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Moonlight.Api.Constants;
|
||||
|
||||
public class FrontendSettingConstants
|
||||
{
|
||||
public const string Name = "Moonlight.Frontend.Name";
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Implementations.ApiKeyScheme;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
@@ -18,10 +20,12 @@ namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
public class ApiKeyController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<ApiKey> KeyRepository;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository)
|
||||
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
|
||||
{
|
||||
KeyRepository = keyRepository;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -114,6 +118,8 @@ public class ApiKeyController : Controller
|
||||
ApiKeyMapper.Merge(apiKey, request);
|
||||
await KeyRepository.UpdateAsync(apiKey);
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
|
||||
|
||||
return ApiKeyMapper.ToDto(apiKey);
|
||||
}
|
||||
|
||||
@@ -129,6 +135,9 @@ public class ApiKeyController : Controller
|
||||
return Problem("No API key with this id found", statusCode: 404);
|
||||
|
||||
await KeyRepository.RemoveAsync(apiKey);
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
namespace Moonlight.Api.Http.Controllers.Admin.Themes;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/themes")]
|
||||
@@ -0,0 +1,76 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Api.Models;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
using VYaml.Serialization;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin.Themes;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/themes")]
|
||||
[Authorize(Policy = Permissions.Themes.View)]
|
||||
public class TransferController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<Theme> ThemeRepository;
|
||||
|
||||
public TransferController(DatabaseRepository<Theme> themeRepository)
|
||||
{
|
||||
ThemeRepository = themeRepository;
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/export")]
|
||||
public async Task<ActionResult> ExportAsync([FromRoute] int id)
|
||||
{
|
||||
var theme = await ThemeRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (theme == null)
|
||||
return Problem("No theme with that id found", statusCode: 404);
|
||||
|
||||
var yml = YamlSerializer.Serialize(new ThemeTransferModel()
|
||||
{
|
||||
Name = theme.Name,
|
||||
Author = theme.Author,
|
||||
CssContent = theme.CssContent,
|
||||
Version = theme.Version
|
||||
});
|
||||
|
||||
return File(yml.ToArray(), "text/yaml", $"{theme.Name}.yml");
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
public async Task<ActionResult<ThemeDto>> ImportAsync()
|
||||
{
|
||||
var themeToImport = await YamlSerializer.DeserializeAsync<ThemeTransferModel>(Request.Body);
|
||||
|
||||
var existingTheme = await ThemeRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Name == themeToImport.Name && x.Author == themeToImport.Author);
|
||||
|
||||
if (existingTheme == null)
|
||||
{
|
||||
var finalTheme = await ThemeRepository.AddAsync(new Theme()
|
||||
{
|
||||
Name = themeToImport.Name,
|
||||
Author = themeToImport.Author,
|
||||
CssContent = themeToImport.CssContent,
|
||||
Version = themeToImport.Version
|
||||
});
|
||||
|
||||
return ThemeMapper.ToDto(finalTheme);
|
||||
}
|
||||
|
||||
existingTheme.CssContent = themeToImport.CssContent;
|
||||
existingTheme.Version = themeToImport.Version;
|
||||
|
||||
await ThemeRepository.UpdateAsync(existingTheme);
|
||||
|
||||
return ThemeMapper.ToDto(existingTheme);
|
||||
}
|
||||
}
|
||||
@@ -9,21 +9,9 @@ namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
[JsonSerializable(typeof(RebuildEventDto))]
|
||||
[JsonSerializable(typeof(RequestRebuildDto))]
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
private static JsonSerializerOptions? InternalTunedOptions;
|
||||
|
||||
public static JsonSerializerOptions TunedOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (InternalTunedOptions != null)
|
||||
return InternalTunedOptions;
|
||||
|
||||
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
|
||||
|
||||
return InternalTunedOptions;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Database;
|
||||
@@ -14,20 +14,20 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
||||
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
||||
{
|
||||
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
||||
private readonly IMemoryCache MemoryCache;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
private const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
|
||||
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
|
||||
|
||||
public ApiKeySchemeHandler(
|
||||
IOptionsMonitor<ApiKeySchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
DatabaseRepository<ApiKey> apiKeyRepository,
|
||||
IMemoryCache memoryCache
|
||||
HybridCache hybridCache
|
||||
) : base(options, logger, encoder)
|
||||
{
|
||||
ApiKeyRepository = apiKeyRepository;
|
||||
MemoryCache = memoryCache;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
@@ -41,25 +41,26 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
||||
return AuthenticateResult.Fail("Invalid api key specified");
|
||||
|
||||
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
|
||||
|
||||
if (!MemoryCache.TryGetValue<ApiKeySession>(cacheKey, out var apiKey))
|
||||
{
|
||||
apiKey = await ApiKeyRepository
|
||||
.Query()
|
||||
.Where(x => x.Key == authHeaderValue)
|
||||
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (apiKey == null)
|
||||
return AuthenticateResult.Fail("Invalid api key specified");
|
||||
var apiKey = await HybridCache.GetOrCreateAsync<ApiKeySession?>(
|
||||
cacheKey,
|
||||
async ct =>
|
||||
{
|
||||
return await ApiKeyRepository
|
||||
.Query()
|
||||
.Where(x => x.Key == authHeaderValue)
|
||||
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
|
||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||
},
|
||||
new HybridCacheEntryOptions()
|
||||
{
|
||||
LocalCacheExpiration = Options.LookupL1CacheTime,
|
||||
Expiration = Options.LookupL2CacheTime
|
||||
}
|
||||
);
|
||||
|
||||
MemoryCache.Set(cacheKey, apiKey, Options.LookupCacheTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (apiKey == null)
|
||||
return AuthenticateResult.Fail("Invalid api key specified");
|
||||
}
|
||||
if (apiKey == null)
|
||||
return AuthenticateResult.Fail("Invalid api key specified");
|
||||
|
||||
if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
|
||||
return AuthenticateResult.Fail("Api key expired");
|
||||
|
||||
@@ -4,5 +4,6 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
||||
|
||||
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
public TimeSpan LookupCacheTime { get; set; }
|
||||
public TimeSpan LookupL1CacheTime { get; set; }
|
||||
public TimeSpan LookupL2CacheTime { get; set; }
|
||||
}
|
||||
12
Moonlight.Api/Models/ThemeTransferModel.cs
Normal file
12
Moonlight.Api/Models/ThemeTransferModel.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using VYaml.Annotations;
|
||||
|
||||
namespace Moonlight.Api.Models;
|
||||
|
||||
[YamlObject]
|
||||
public partial class ThemeTransferModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string CssContent { get; set; }
|
||||
}
|
||||
@@ -25,9 +25,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||
<PackageReference Include="VYaml" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
28
Moonlight.Api/MoonlightPlugin.cs
Normal file
28
Moonlight.Api/MoonlightPlugin.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using SimplePlugin.Abstractions;
|
||||
|
||||
namespace Moonlight.Api;
|
||||
|
||||
public abstract class MoonlightPlugin : IPluginModule
|
||||
{
|
||||
protected MoonlightPlugin[] Plugins { get; private set; }
|
||||
|
||||
public void Initialize(MoonlightPlugin[] plugins)
|
||||
{
|
||||
Plugins = plugins;
|
||||
}
|
||||
|
||||
public virtual void PreBuild(WebApplicationBuilder builder)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void PostBuild(WebApplication application)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void PostMiddleware(WebApplication application)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
@@ -42,7 +41,7 @@ public class ContainerHelperService
|
||||
request.Content = JsonContent.Create(
|
||||
new RequestRebuildDto(noBuildCache),
|
||||
null,
|
||||
SerializationContext.TunedOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(
|
||||
@@ -77,7 +76,7 @@ public class ContainerHelperService
|
||||
continue;
|
||||
|
||||
var data = line.Trim("data: ");
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.TunedOptions);
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.Default.Options);
|
||||
|
||||
yield return deserializedData;
|
||||
|
||||
@@ -100,14 +99,14 @@ public class ContainerHelperService
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"api/configuration/version",
|
||||
new SetVersionDto(version),
|
||||
SerializationContext.TunedOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
|
||||
var problemDetails =
|
||||
await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.TunedOptions);
|
||||
await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.Default.Options);
|
||||
|
||||
if (problemDetails == null)
|
||||
throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}");
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Database;
|
||||
@@ -12,18 +12,18 @@ public class SettingsService
|
||||
{
|
||||
private readonly DatabaseRepository<SettingsOption> Repository;
|
||||
private readonly IOptions<SettingsOptions> Options;
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
|
||||
|
||||
public SettingsService(
|
||||
DatabaseRepository<SettingsOption> repository,
|
||||
IOptions<SettingsOptions> options,
|
||||
IMemoryCache cache
|
||||
)
|
||||
HybridCache hybridCache
|
||||
)
|
||||
{
|
||||
Repository = repository;
|
||||
Cache = cache;
|
||||
HybridCache = hybridCache;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
@@ -31,24 +31,26 @@ public class SettingsService
|
||||
{
|
||||
var cacheKey = string.Format(CacheKey, key);
|
||||
|
||||
if (Cache.TryGetValue<string>(cacheKey, out var value))
|
||||
return JsonSerializer.Deserialize<T>(value!);
|
||||
|
||||
value = await Repository
|
||||
.Query()
|
||||
.Where(x => x.Key == key)
|
||||
.Select(o => o.ValueJson)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if(string.IsNullOrEmpty(value))
|
||||
return default;
|
||||
|
||||
Cache.Set(
|
||||
var value = await HybridCache.GetOrCreateAsync<string?>(
|
||||
cacheKey,
|
||||
value,
|
||||
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
||||
async ct =>
|
||||
{
|
||||
return await Repository
|
||||
.Query()
|
||||
.Where(x => x.Key == key)
|
||||
.Select(o => o.ValueJson)
|
||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||
},
|
||||
new HybridCacheEntryOptions()
|
||||
{
|
||||
LocalCacheExpiration = Options.Value.LookupL1CacheTime,
|
||||
Expiration = Options.Value.LookupL2CacheTime
|
||||
}
|
||||
);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return default;
|
||||
|
||||
return JsonSerializer.Deserialize<T>(value);
|
||||
}
|
||||
|
||||
@@ -77,7 +79,7 @@ public class SettingsService
|
||||
|
||||
await Repository.AddAsync(option);
|
||||
}
|
||||
|
||||
Cache.Remove(cacheKey);
|
||||
|
||||
await HybridCache.RemoveAsync(cacheKey);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
@@ -14,10 +14,10 @@ namespace Moonlight.Api.Services;
|
||||
public class UserAuthService
|
||||
{
|
||||
private readonly DatabaseRepository<User> UserRepository;
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly ILogger<UserAuthService> Logger;
|
||||
private readonly IOptions<SessionOptions> Options;
|
||||
private readonly IOptions<UserOptions> Options;
|
||||
private readonly IEnumerable<IUserAuthHook> Hooks;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
private const string UserIdClaim = "UserId";
|
||||
private const string IssuedAtClaim = "IssuedAt";
|
||||
@@ -27,15 +27,16 @@ public class UserAuthService
|
||||
public UserAuthService(
|
||||
DatabaseRepository<User> userRepository,
|
||||
ILogger<UserAuthService> logger,
|
||||
IMemoryCache cache, IOptions<SessionOptions> options,
|
||||
IEnumerable<IUserAuthHook> hooks
|
||||
IOptions<UserOptions> options,
|
||||
IEnumerable<IUserAuthHook> hooks,
|
||||
HybridCache hybridCache
|
||||
)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
Logger = logger;
|
||||
Cache = cache;
|
||||
Options = options;
|
||||
Hooks = hooks;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||
@@ -80,8 +81,8 @@ public class UserAuthService
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
{
|
||||
// Run every hook and if any returns false we return false as well
|
||||
if(!await hook.SyncAsync(principal, user))
|
||||
// Run every hook, and if any returns false, we return false as well
|
||||
if (!await hook.SyncAsync(principal, user))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -101,32 +102,29 @@ public class UserAuthService
|
||||
|
||||
var cacheKey = string.Format(CacheKeyPattern, userId);
|
||||
|
||||
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
|
||||
{
|
||||
user = await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => new UserSession(
|
||||
u.InvalidateTimestamp,
|
||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||
)
|
||||
.FirstOrDefaultAsync();
|
||||
var user = await HybridCache.GetOrCreateAsync<UserSession?>(
|
||||
cacheKey,
|
||||
async ct =>
|
||||
{
|
||||
return await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => new UserSession(
|
||||
u.InvalidateTimestamp,
|
||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||
)
|
||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||
},
|
||||
new HybridCacheEntryOptions()
|
||||
{
|
||||
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
|
||||
Expiration = Options.Value.ValidationCacheL2Expiry
|
||||
}
|
||||
);
|
||||
|
||||
if (user == null)
|
||||
return false;
|
||||
|
||||
Cache.Set(
|
||||
cacheKey,
|
||||
user,
|
||||
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (user == null)
|
||||
return false;
|
||||
}
|
||||
if (user == null)
|
||||
return false;
|
||||
|
||||
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
||||
|
||||
@@ -146,11 +144,11 @@ public class UserAuthService
|
||||
principal.Identities.First().AddClaims(
|
||||
user.Permissions.Select(x => new Claim(Permissions.ClaimType, x))
|
||||
);
|
||||
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
{
|
||||
// Run every hook and if any returns false we return false as well
|
||||
if(!await hook.ValidateAsync(principal, userId))
|
||||
// Run every hook, and if any returns false we return false as well
|
||||
if (!await hook.ValidateAsync(principal, userId))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Interfaces;
|
||||
@@ -10,13 +10,17 @@ public class UserDeletionService
|
||||
{
|
||||
private readonly DatabaseRepository<User> Repository;
|
||||
private readonly IEnumerable<IUserDeletionHook> Hooks;
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
public UserDeletionService(DatabaseRepository<User> repository, IEnumerable<IUserDeletionHook> hooks, IMemoryCache cache)
|
||||
public UserDeletionService(
|
||||
DatabaseRepository<User> repository,
|
||||
IEnumerable<IUserDeletionHook> hooks,
|
||||
HybridCache hybridCache
|
||||
)
|
||||
{
|
||||
Repository = repository;
|
||||
Hooks = hooks;
|
||||
Cache = cache;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
|
||||
@@ -54,7 +58,8 @@ public class UserDeletionService
|
||||
await hook.ExecuteAsync(user);
|
||||
|
||||
await Repository.RemoveAsync(user);
|
||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Interfaces;
|
||||
@@ -10,17 +10,17 @@ public class UserLogoutService
|
||||
{
|
||||
private readonly DatabaseRepository<User> Repository;
|
||||
private readonly IEnumerable<IUserLogoutHook> Hooks;
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
public UserLogoutService(
|
||||
DatabaseRepository<User> repository,
|
||||
IEnumerable<IUserLogoutHook> hooks,
|
||||
IMemoryCache cache
|
||||
HybridCache hybridCache
|
||||
)
|
||||
{
|
||||
Repository = repository;
|
||||
Hooks = hooks;
|
||||
Cache = cache;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(int userId)
|
||||
@@ -28,16 +28,16 @@ public class UserLogoutService
|
||||
var user = await Repository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == userId);
|
||||
|
||||
if(user == null)
|
||||
|
||||
if (user == null)
|
||||
throw new AggregateException($"User with id {userId} not found");
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
await hook.ExecuteAsync(user);
|
||||
|
||||
|
||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
||||
await Repository.UpdateAsync(user);
|
||||
|
||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public interface IAppStartup
|
||||
{
|
||||
public void PreBuild(WebApplicationBuilder builder);
|
||||
public void PostBuild(WebApplication application);
|
||||
public void PostMiddleware(WebApplication application);
|
||||
}
|
||||
@@ -17,20 +17,26 @@ public partial class Startup
|
||||
{
|
||||
private static void AddAuth(WebApplicationBuilder builder)
|
||||
{
|
||||
// OIDC
|
||||
var oidcOptions = new OidcOptions();
|
||||
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
|
||||
|
||||
// API Key
|
||||
var apiKeyOptions = new ApiOptions();
|
||||
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
|
||||
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
|
||||
|
||||
// Session
|
||||
builder.Services.AddOptions<UserOptions>().BindConfiguration("Moonlight:User");
|
||||
|
||||
builder.Services.AddScoped<UserAuthService>();
|
||||
|
||||
// Authentication
|
||||
builder.Services.AddAuthentication("Main")
|
||||
.AddPolicyScheme("Main", null, options =>
|
||||
{
|
||||
options.ForwardDefaultSelector += context => context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
|
||||
})
|
||||
.AddPolicyScheme("Main", null,
|
||||
options =>
|
||||
{
|
||||
options.ForwardDefaultSelector += context =>
|
||||
context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
|
||||
})
|
||||
.AddCookie("Session", null, options =>
|
||||
{
|
||||
options.Events.OnSigningIn += async context =>
|
||||
@@ -83,7 +89,7 @@ public partial class Startup
|
||||
var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
|
||||
|
||||
options.Scope.Clear();
|
||||
|
||||
|
||||
foreach (var scope in scopes)
|
||||
options.Scope.Add(scope);
|
||||
|
||||
@@ -97,18 +103,26 @@ public partial class Startup
|
||||
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
})
|
||||
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null, options =>
|
||||
{
|
||||
options.LookupCacheTime = TimeSpan.FromMinutes(apiKeyOptions.LookupCacheMinutes);
|
||||
});
|
||||
|
||||
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
|
||||
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null,
|
||||
options =>
|
||||
{
|
||||
options.LookupL1CacheTime = apiKeyOptions.LookupCacheL1Expiry;
|
||||
options.LookupL2CacheTime = apiKeyOptions.LookupCacheL2Expiry;
|
||||
});
|
||||
|
||||
// Authorization
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Reduce log noise
|
||||
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
|
||||
|
||||
// Custom permission handling using named policies
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
||||
|
||||
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
|
||||
builder.Services.AddScoped<SettingsService>();
|
||||
builder.Services.AddScoped<UserDeletionService>();
|
||||
builder.Services.AddScoped<UserLogoutService>();
|
||||
builder.Services.AddScoped<UserAuthService>();
|
||||
}
|
||||
|
||||
private static void UseAuth(WebApplication application)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
@@ -9,52 +11,73 @@ using Moonlight.Api.Helpers;
|
||||
using Moonlight.Api.Implementations;
|
||||
using Moonlight.Api.Interfaces;
|
||||
using Moonlight.Api.Services;
|
||||
using SessionOptions = Moonlight.Api.Configuration.SessionOptions;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public partial class Startup
|
||||
{
|
||||
private static void AddBase(WebApplicationBuilder builder)
|
||||
private void AddBase(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
});
|
||||
// Create the base directory
|
||||
Directory.CreateDirectory("storage");
|
||||
|
||||
// Hook up source-generated serialization and add controllers
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddApplicationPart(typeof(Startup).Assembly)
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
});
|
||||
|
||||
// Configure logging
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
||||
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
||||
|
||||
// Application service
|
||||
builder.Services.AddSingleton<ApplicationService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
||||
|
||||
// Diagnose
|
||||
builder.Services.AddSingleton<DiagnoseService>();
|
||||
|
||||
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddOptions<SessionOptions>().BindConfiguration("Moonlight:Session");
|
||||
|
||||
// Frontend
|
||||
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
||||
builder.Services.AddScoped<FrontendService>();
|
||||
|
||||
// HTTP Client
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
|
||||
// Version fetching configuration
|
||||
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
|
||||
builder.Services.AddSingleton<VersionService>();
|
||||
|
||||
|
||||
// Container Helper Options
|
||||
builder.Configuration.GetSection("Moonlight:ContainerHelper").Bind(builder.Configuration);
|
||||
|
||||
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
|
||||
builder.Services.AddSingleton<ContainerHelperService>();
|
||||
|
||||
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
|
||||
{
|
||||
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
|
||||
client.BaseAddress = new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
|
||||
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
|
||||
client.BaseAddress =
|
||||
new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
|
||||
});
|
||||
|
||||
|
||||
// User management services
|
||||
builder.Services.AddScoped<UserDeletionService>();
|
||||
builder.Services.AddScoped<UserLogoutService>();
|
||||
|
||||
// Settings options
|
||||
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
|
||||
builder.Services.AddScoped<SettingsService>();
|
||||
|
||||
// Setup key loading
|
||||
var keysDirectory = new DirectoryInfo(Path.Combine("storage", "keys"));
|
||||
builder.Services.AddDataProtection().PersistKeysToFileSystem(keysDirectory);
|
||||
}
|
||||
|
||||
private static void UseBase(WebApplication application)
|
||||
@@ -67,8 +90,8 @@ public partial class Startup
|
||||
application.MapControllers();
|
||||
|
||||
var options = application.Services.GetRequiredService<IOptions<FrontendOptions>>();
|
||||
|
||||
if(options.Value.Enabled)
|
||||
|
||||
if (options.Value.Enabled)
|
||||
application.MapFallbackToFile("index.html");
|
||||
}
|
||||
}
|
||||
34
Moonlight.Api/Startup/Startup.Cache.cs
Normal file
34
Moonlight.Api/Startup/Startup.Cache.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moonlight.Api.Configuration;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public partial class Startup
|
||||
{
|
||||
private static void AddCache(WebApplicationBuilder builder)
|
||||
{
|
||||
// Load cache options
|
||||
var cacheOptions = new CacheOptions();
|
||||
builder.Configuration.GetSection("Moonlight:Cache").Bind(cacheOptions);
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddHybridCache();
|
||||
|
||||
if (!cacheOptions.EnableLayer2)
|
||||
return;
|
||||
|
||||
var redisOptions = new RedisOptions();
|
||||
builder.Configuration.GetSection("Moonlight:Redis").Bind(redisOptions);
|
||||
|
||||
if(!redisOptions.Enable)
|
||||
return;
|
||||
|
||||
builder.Services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
options.Configuration = redisOptions.ConnectionString;
|
||||
options.InstanceName = "Moonlight:";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Moonlight.Shared.Http;
|
||||
using SimplePlugin.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public partial class Startup : IAppStartup
|
||||
[PluginModule]
|
||||
public partial class Startup : MoonlightPlugin
|
||||
{
|
||||
public void PreBuild(WebApplicationBuilder builder)
|
||||
public override void PreBuild(WebApplicationBuilder builder)
|
||||
{
|
||||
AddBase(builder);
|
||||
AddAuth(builder);
|
||||
AddDatabase(builder);
|
||||
AddCache(builder);
|
||||
}
|
||||
|
||||
public void PostBuild(WebApplication application)
|
||||
public override void PostBuild(WebApplication application)
|
||||
{
|
||||
UseBase(application);
|
||||
UseAuth(application);
|
||||
}
|
||||
|
||||
public void PostMiddleware(WebApplication application)
|
||||
public override void PostMiddleware(WebApplication application)
|
||||
{
|
||||
MapBase(application);
|
||||
}
|
||||
|
||||
42
Moonlight.Api/StartupHandler.cs
Normal file
42
Moonlight.Api/StartupHandler.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Moonlight.Api;
|
||||
|
||||
public static class StartupHandler
|
||||
{
|
||||
public static async Task RunAsync(string[] args, MoonlightPlugin[] plugins)
|
||||
{
|
||||
Console.WriteLine($"Starting with: {string.Join(", ", plugins.Select(x => x.GetType().FullName))}");
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Setting up context
|
||||
foreach (var plugin in plugins)
|
||||
plugin.Initialize(plugins);
|
||||
|
||||
// Stage 1: Pre Build
|
||||
foreach (var startup in plugins)
|
||||
startup.PreBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Stage 2: Post Build
|
||||
foreach (var startup in plugins)
|
||||
startup.PostBuild(app);
|
||||
|
||||
// Stage 3: Post Middleware
|
||||
foreach (var startup in plugins)
|
||||
startup.PostMiddleware(app);
|
||||
|
||||
// Frontend debugging
|
||||
if (app.Environment.IsDevelopment())
|
||||
app.UseWebAssemblyDebugging();
|
||||
|
||||
// Frontend hosting
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
await app.RunAsync();
|
||||
}
|
||||
}
|
||||
26
Moonlight.Frontend/Configuration/LayoutMiddlewareOptions.cs
Normal file
26
Moonlight.Frontend/Configuration/LayoutMiddlewareOptions.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Frontend.Interfaces;
|
||||
|
||||
namespace Moonlight.Frontend.Configuration;
|
||||
|
||||
public class LayoutMiddlewareOptions
|
||||
{
|
||||
public IReadOnlyList<Type> Components => InnerComponents;
|
||||
|
||||
private readonly List<Type> InnerComponents = new();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
33
Moonlight.Frontend/Configuration/LayoutPageOptions.cs
Normal file
33
Moonlight.Frontend/Configuration/LayoutPageOptions.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Moonlight.Frontend.Configuration;
|
||||
|
||||
public class LayoutPageOptions
|
||||
{
|
||||
public IReadOnlyList<LayoutPageComponent> Components => InnerComponents;
|
||||
|
||||
private readonly List<LayoutPageComponent> InnerComponents = new();
|
||||
|
||||
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.Configuration;
|
||||
|
||||
public class NavigationAssemblyOptions
|
||||
{
|
||||
public List<Assembly> Assemblies { get; private set; } = new();
|
||||
}
|
||||
35
Moonlight.Frontend/Configuration/SystemSettingsOptions.cs
Normal file
35
Moonlight.Frontend/Configuration/SystemSettingsOptions.cs
Normal 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
|
||||
);
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Moonlight.Shared.Http;
|
||||
|
||||
namespace Moonlight.Frontend;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public static JsonSerializerOptions SerializerOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (InternalOptions != null)
|
||||
return InternalOptions;
|
||||
|
||||
InternalOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
// Add source generated options from shared project
|
||||
InternalOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
|
||||
return InternalOptions;
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions? InternalOptions;
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
8
Moonlight.Frontend/Interfaces/LayoutMiddlewareBase.cs
Normal file
8
Moonlight.Frontend/Interfaces/LayoutMiddlewareBase.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Moonlight.Frontend.Interfaces;
|
||||
|
||||
public abstract class LayoutMiddlewareBase : ComponentBase
|
||||
{
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
@@ -24,8 +24,9 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||
<PackageReference Include="ShadcnBlazor" Version="1.0.11" />
|
||||
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.11" />
|
||||
<PackageReference Include="ShadcnBlazor" Version="1.0.13" />
|
||||
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.13" />
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -35,5 +36,6 @@
|
||||
<ItemGroup>
|
||||
<None Include="Styles/*" Pack="true" PackagePath="Styles/" />
|
||||
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\Moonlight.Frontend.targets" />
|
||||
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="buildTransitive\Moonlight.Frontend.targets" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -5,7 +5,7 @@
|
||||
</MoonlightCssClassDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="CopyContents" BeforeTargets="Build">
|
||||
<Target Name="Moonlight_CopyContents" BeforeTargets="Build">
|
||||
<ItemGroup>
|
||||
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
17
Moonlight.Frontend/MoonlightPlugin.cs
Normal file
17
Moonlight.Frontend/MoonlightPlugin.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using SimplePlugin.Abstractions;
|
||||
|
||||
namespace Moonlight.Frontend;
|
||||
|
||||
public abstract class MoonlightPlugin : IPluginModule
|
||||
{
|
||||
protected MoonlightPlugin[] Plugins { get; private set; }
|
||||
|
||||
public void Initialize(MoonlightPlugin[] plugins)
|
||||
{
|
||||
Plugins = plugins;
|
||||
}
|
||||
|
||||
public virtual void PreBuild(WebAssemblyHostBuilder builder){}
|
||||
public virtual void PostBuild(WebAssemblyHost application){}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualBasic;
|
||||
using Moonlight.Shared.Http;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||
|
||||
namespace Moonlight.Frontend.Services;
|
||||
@@ -23,7 +25,7 @@ public class RemoteAuthProvider : AuthenticationStateProvider
|
||||
try
|
||||
{
|
||||
var claimResponses = await HttpClient.GetFromJsonAsync<ClaimDto[]>(
|
||||
"api/auth/claims", Constants.SerializerOptions
|
||||
"api/auth/claims", SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
var claims = claimResponses!.Select(claim => new Claim(claim.Type, claim.Value));
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
namespace Moonlight.Frontend.Startup;
|
||||
|
||||
public interface IAppStartup
|
||||
{
|
||||
public void PreBuild(WebAssemblyHostBuilder builder);
|
||||
public void PostBuild(WebAssemblyHost application);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
using LucideBlazor;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moonlight.Frontend.Configuration;
|
||||
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;
|
||||
|
||||
@@ -25,5 +28,19 @@ public partial class Startup
|
||||
builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>();
|
||||
|
||||
builder.Services.AddScoped<FrontendService>();
|
||||
|
||||
builder.Services.Configure<NavigationAssemblyOptions>(options =>
|
||||
{
|
||||
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
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using SimplePlugin.Abstractions;
|
||||
|
||||
namespace Moonlight.Frontend.Startup;
|
||||
|
||||
public partial class Startup : IAppStartup
|
||||
[PluginModule]
|
||||
public partial class Startup : MoonlightPlugin
|
||||
{
|
||||
public void PreBuild(WebAssemblyHostBuilder builder)
|
||||
public override void PreBuild(WebAssemblyHostBuilder builder)
|
||||
{
|
||||
AddBase(builder);
|
||||
AddAuth(builder);
|
||||
}
|
||||
|
||||
public void PostBuild(WebAssemblyHost application)
|
||||
public override void PostBuild(WebAssemblyHost application)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
29
Moonlight.Frontend/StartupHandler.cs
Normal file
29
Moonlight.Frontend/StartupHandler.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
namespace Moonlight.Frontend;
|
||||
|
||||
public static class StartupHandler
|
||||
{
|
||||
public static async Task RunAsync(string[] args, MoonlightPlugin[] plugins)
|
||||
{
|
||||
Console.WriteLine($"Starting with: {string.Join(", ", plugins.Select(x => x.GetType().FullName))}");
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
// Setting up context
|
||||
foreach (var plugin in plugins)
|
||||
plugin.Initialize(plugins);
|
||||
|
||||
// Stage 1: Pre Build
|
||||
foreach (var plugin in plugins)
|
||||
plugin.PreBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Stage 2: Post Build
|
||||
foreach(var plugin in plugins)
|
||||
plugin.PostBuild(app);
|
||||
|
||||
await app.RunAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@@ -77,7 +78,7 @@
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/apiKeys",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Roles
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@@ -76,7 +77,7 @@
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"api/admin/roles",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Users
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@@ -65,7 +66,7 @@
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/users",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@@ -74,7 +75,7 @@
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/apiKeys/{Key.Id}",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -124,7 +124,7 @@ else
|
||||
await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
|
||||
{
|
||||
Version = Version
|
||||
}, SerializationContext.TunedOptions);
|
||||
}, SerializationContext.Default.Options);
|
||||
|
||||
// Starting rebuild task
|
||||
CurrentStep = 2;
|
||||
@@ -136,7 +136,7 @@ else
|
||||
request.Content = JsonContent.Create(
|
||||
new RequestRebuildDto(NoBuildCache),
|
||||
null,
|
||||
SerializationContext.TunedOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
var response = await HttpClient.SendAsync(
|
||||
@@ -160,7 +160,7 @@ else
|
||||
continue;
|
||||
|
||||
var data = line.Trim("data: ");
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, Constants.SerializerOptions);
|
||||
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.Default.Options);
|
||||
|
||||
switch (deserializedData.Type)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Roles
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@@ -75,7 +76,7 @@
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"api/admin/roles/{Role.Id}",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
@page "/admin"
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@@ -155,7 +156,7 @@
|
||||
if(!firstRender)
|
||||
return;
|
||||
|
||||
InfoResponse = await HttpClient.GetFromJsonAsync<SystemInfoDto>("api/admin/system/info", Constants.SerializerOptions);
|
||||
InfoResponse = await HttpClient.GetFromJsonAsync<SystemInfoDto>("api/admin/system/info", SerializationContext.Default.Options);
|
||||
IsInfoLoading = false;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
||||
@@ -126,7 +127,7 @@
|
||||
|
||||
var response = await HttpClient.GetFromJsonAsync<PagedData<ApiKeyDto>>(
|
||||
$"api/admin/apiKeys{query}&filterOptions={filterOptions}",
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
return new DataGridResponse<ApiKeyDto>(response!.Data, response.TotalLength);
|
||||
|
||||
@@ -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>
|
||||
<TabsContent Value="settings">
|
||||
<Card ClassName="mt-5">
|
||||
<CardFooter ClassName="justify-end">
|
||||
<Button>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent Value="diagnose">
|
||||
<Diagnose />
|
||||
</TabsContent>
|
||||
@if (SettingsResult.Succeeded)
|
||||
{
|
||||
<TabsContent Value="settings">
|
||||
<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)
|
||||
|
||||
52
Moonlight.Frontend/UI/Admin/Views/Sys/Settings.razor
Normal file
52
Moonlight.Frontend/UI/Admin/Views/Sys/Settings.razor
Normal 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;
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Services
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Themes
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@@ -122,7 +123,7 @@
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/themes",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Moonlight.Shared
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Themes
|
||||
@@ -18,6 +19,8 @@
|
||||
@inject IAuthorizationService AuthorizationService
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<InputFile OnChange="OnFileSelectedAsync" id="import-theme" class="hidden" multiple accept=".yml"/>
|
||||
|
||||
<div class="flex flex-row justify-between mt-5">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Themes</h1>
|
||||
@@ -26,7 +29,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button @onclick="CreateAsync" disabled="@(!CreateAccess.Succeeded)">
|
||||
<Button Variant="ButtonVariant.Outline">
|
||||
<Slot>
|
||||
<label for="import-theme" @attributes="context">
|
||||
<HardDriveUploadIcon/>
|
||||
Import
|
||||
</label>
|
||||
</Slot>
|
||||
</Button>
|
||||
<Button @onclick="Create" disabled="@(!CreateAccess.Succeeded)">
|
||||
<PlusIcon/>
|
||||
Create
|
||||
</Button>
|
||||
@@ -39,13 +50,14 @@
|
||||
<TemplateColumn Identifier="@nameof(ThemeDto.Name)" IsFilterable="true" Title="Name">
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
<a class="text-primary flex flex-row items-center" href="#" @onclick="() => EditAsync(context)" @onclick:preventDefault>
|
||||
<a class="text-primary flex flex-row items-center" href="#" @onclick="() => Edit(context)"
|
||||
@onclick:preventDefault>
|
||||
@context.Name
|
||||
|
||||
|
||||
@if (context.IsEnabled)
|
||||
{
|
||||
<span class="ms-2">
|
||||
<CheckIcon ClassName="size-4 text-green-400" />
|
||||
<CheckIcon ClassName="size-4 text-green-400"/>
|
||||
</span>
|
||||
}
|
||||
</a>
|
||||
@@ -70,7 +82,13 @@
|
||||
</Slot>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent SideOffset="2">
|
||||
<DropdownMenuItem OnClick="() => EditAsync(context)" Disabled="@(!EditAccess.Succeeded)">
|
||||
<DropdownMenuItem OnClick="() => Download(context)">
|
||||
Download
|
||||
<DropdownMenuShortcut>
|
||||
<HardDriveDownloadIcon/>
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem OnClick="() => Edit(context)" Disabled="@(!EditAccess.Succeeded)">
|
||||
Edit
|
||||
<DropdownMenuShortcut>
|
||||
<PenIcon/>
|
||||
@@ -96,9 +114,9 @@
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||
|
||||
|
||||
private DataGrid<ThemeDto> Grid;
|
||||
|
||||
|
||||
private AuthorizationResult EditAccess;
|
||||
private AuthorizationResult DeleteAccess;
|
||||
private AuthorizationResult CreateAccess;
|
||||
@@ -106,7 +124,7 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthState;
|
||||
|
||||
|
||||
EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.Edit);
|
||||
DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.Delete);
|
||||
CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.Create);
|
||||
@@ -119,15 +137,17 @@
|
||||
|
||||
var response = await HttpClient.GetFromJsonAsync<PagedData<ThemeDto>>(
|
||||
$"api/admin/themes{query}&filterOptions={filterOptions}",
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
return new DataGridResponse<ThemeDto>(response!.Data, response.TotalLength);
|
||||
}
|
||||
|
||||
private void CreateAsync() => Navigation.NavigateTo("/admin/system/themes/create");
|
||||
private void Create() => Navigation.NavigateTo("/admin/system/themes/create");
|
||||
|
||||
private void EditAsync(ThemeDto theme) => Navigation.NavigateTo($"/admin/system/themes/{theme.Id}");
|
||||
private void Edit(ThemeDto theme) => Navigation.NavigateTo($"/admin/system/themes/{theme.Id}");
|
||||
|
||||
private void Download(ThemeDto theme) => Navigation.NavigateTo($"api/admin/themes/{theme.Id}/export", true);
|
||||
|
||||
private async Task DeleteAsync(ThemeDto theme)
|
||||
{
|
||||
@@ -145,4 +165,31 @@
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async Task OnFileSelectedAsync(InputFileChangeEventArgs eventArgs)
|
||||
{
|
||||
var files = eventArgs.GetMultipleFiles();
|
||||
|
||||
foreach (var browserFile in files)
|
||||
{
|
||||
await using var contentStream = browserFile.OpenReadStream(browserFile.Size);
|
||||
|
||||
var response = await HttpClient.PostAsync(
|
||||
"api/admin/themes/import",
|
||||
new StreamContent(contentStream)
|
||||
);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var importedTheme = await response
|
||||
.Content
|
||||
.ReadFromJsonAsync<ThemeDto>(SerializationContext.Default.Options);
|
||||
|
||||
if (importedTheme == null)
|
||||
continue;
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
await ToastService.SuccessAsync("Theme Import", $"Successfully imported theme {importedTheme.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.Services
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Themes
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Themes
|
||||
@using ShadcnBlazor.Buttons
|
||||
@@ -136,7 +137,7 @@
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/themes/{Theme.Id}",
|
||||
Request,
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@@ -123,7 +124,7 @@
|
||||
|
||||
var response = await HttpClient.GetFromJsonAsync<PagedData<RoleDto>>(
|
||||
$"api/admin/roles{query}&filterOptions={filterOptions}",
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
return new DataGridResponse<RoleDto>(response!.Data, response.TotalLength);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared
|
||||
@using Moonlight.Shared.Http
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Dropdowns
|
||||
@@ -121,7 +122,7 @@
|
||||
|
||||
var response = await HttpClient.GetFromJsonAsync<PagedData<UserDto>>(
|
||||
$"api/admin/users{query}&filterOptions={filterOptions}",
|
||||
Constants.SerializerOptions
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
return new DataGridResponse<UserDto>(response!.Data, response.TotalLength);
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
@using System.Net
|
||||
@using System.Reflection
|
||||
@using LucideBlazor
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.Extensions.Options
|
||||
@using Moonlight.Frontend.Configuration
|
||||
@using Moonlight.Frontend.UI.Shared
|
||||
@using Moonlight.Frontend.UI.Shared.Components
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using Moonlight.Frontend.UI.Shared.Components.Auth
|
||||
@using Moonlight.Frontend.UI.Shared.Partials
|
||||
@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>
|
||||
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(NotFound)">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
|
||||
<NotAuthorized Context="authRouteViewContext">
|
||||
<AccessDenied/>
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
</Found>
|
||||
</Router>
|
||||
<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/>
|
||||
@@ -71,4 +87,14 @@
|
||||
</div>
|
||||
}
|
||||
</ErrorContent>
|
||||
</ErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
|
||||
@code
|
||||
{
|
||||
private Assembly[] Assemblies;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Assemblies = NavigationOptions.Value.Assemblies.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Auth
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Auth
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Spinners
|
||||
@using ShadcnBlazor.Buttons
|
||||
@@ -48,7 +49,7 @@
|
||||
return;
|
||||
|
||||
var schemes = await HttpClient.GetFromJsonAsync<SchemeDto[]>(
|
||||
"api/auth", Constants.SerializerOptions
|
||||
"api/auth", SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (schemes == null)
|
||||
|
||||
34
Moonlight.Frontend/UI/Shared/LayoutMiddleware.razor
Normal file
34
Moonlight.Frontend/UI/Shared/LayoutMiddleware.razor
Normal file
@@ -0,0 +1,34 @@
|
||||
@using Microsoft.Extensions.Options
|
||||
@using Moonlight.Frontend.Configuration
|
||||
@using Moonlight.Frontend.Interfaces
|
||||
|
||||
@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();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,53 @@
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using Microsoft.Extensions.Options
|
||||
@using Moonlight.Frontend.Configuration
|
||||
@using ShadcnBlazor.Extras.Alerts
|
||||
@using ShadcnBlazor.Extras.Dialogs
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Portals
|
||||
@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>
|
||||
|
||||
<ToastLauncher/>
|
||||
<DialogLauncher/>
|
||||
<AlertDialogLauncher/>
|
||||
|
||||
<PortalOutlet />
|
||||
@foreach (var footerComponent in FooterComponents)
|
||||
{
|
||||
<DynamicComponent Type="footerComponent" />
|
||||
}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Settings;
|
||||
|
||||
public class SetWhiteLabelingDto
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Admin.Settings;
|
||||
|
||||
public class WhiteLabelingDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -58,21 +60,13 @@ namespace Moonlight.Shared.Http;
|
||||
//Misc
|
||||
[JsonSerializable(typeof(VersionDto))]
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
|
||||
// Settings - White Labeling
|
||||
[JsonSerializable(typeof(WhiteLabelingDto))]
|
||||
[JsonSerializable(typeof(SetWhiteLabelingDto))]
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
private static JsonSerializerOptions? InternalTunedOptions;
|
||||
|
||||
public static JsonSerializerOptions TunedOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (InternalTunedOptions != null)
|
||||
return InternalTunedOptions;
|
||||
|
||||
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
|
||||
|
||||
return InternalTunedOptions;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user