24 Commits

Author SHA1 Message Date
6d1e6e1690 Cleaned up using in project. Improved prohect structure and refactored page names. Upgraded dependencies 2026-03-13 08:53:04 +01:00
1257e8b950 Refactored project to module structure 2026-03-12 22:50:15 +01:00
93de9c5d00 Upgraded dependencies to latest version
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 1m0s
2026-03-09 10:47:25 +01:00
3dff8c8f6d Fixed IL trimming removing used icons from build output in system settings tab 2026-02-21 22:46:34 +01:00
95a848e571 Merge pull request 'Implemented extendable system settings tab. Started implementing white labeling settings' (#21) from feat/SystemSettings into v2.1
Reviewed-on: #21
2026-02-21 21:22:06 +00:00
9d557eea4e Implemented extendable system settings tab. Started implementing white labeling settings 2026-02-21 22:20:51 +01:00
94c1aac0ac Merge pull request 'Added plugins hooks for layout related options' (#20) from feat/LayoutMiddleware into v2.1
Reviewed-on: #20
2026-02-20 15:28:24 +00:00
3bddd64d91 Added page hooks for main layout 2026-02-20 16:25:01 +01:00
5ad7a6db7b Added hook option for plugins to inject into the main layout before the router 2026-02-20 12:28:22 +01:00
9b9272cd6e Fixed nuget package build failing after changing shadcnblazor class list location 2026-02-20 09:41:35 +01:00
31cf34ed04 Merge pull request 'Improved css build and initialization of frontend plugins' (#19) from feat/PluginImprovements into v2.1
Some checks failed
Dev Publish: Nuget / Publish Dev Packages (push) Failing after 40s
Reviewed-on: #19
2026-02-20 08:38:27 +00:00
a9b0020131 Upgraded to shadcnblazor 1.0.13. Added transitive mapping copying and prefixed target to stop any collisions 2026-02-20 09:35:43 +01:00
e3b432aae6 Removed unused startup interface. Added plugin list to frontend plugin initialization 2026-02-20 09:20:29 +01:00
06f27605ba Merge pull request 'Switched from self created static constant json options to a source generator options' (#18) from feat/ImproveJsonSerialization into v2.1
Reviewed-on: #18
2026-02-19 07:50:03 +00:00
0bd138df63 Switched from self created static constant json options to a source generator options 2026-02-19 08:49:23 +01:00
d7b725f541 Merge pull request 'Improved plugin loading and handling' (#17) from feat/ImprovePluginLoading into v2.1
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 51s
Reviewed-on: #17
2026-02-19 07:36:06 +00:00
0f26aaf803 Added options for navigation assemblies for the router 2026-02-19 08:32:32 +01:00
c45e177001 Improved handling of moonlight plugins during startup, minimized host project code and moved startup handling to core 2026-02-18 15:36:45 +01:00
627e9bb161 Merge pull request 'Switched to SimplePlugin plugin loader' (#16) from feat/SwitchPluginLoader into v2.1
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 50s
Reviewed-on: #16
2026-02-18 12:57:22 +00:00
1fc33ebf03 Switched to SimplePlugin plugin loader 2026-02-18 13:21:15 +01:00
64e4d7201e Removed test code from ApiKeySchemeHandler 2026-02-14 15:31:47 +01:00
816aa01319 Implemented plugin referencing. Added healthcheck and custom base docker image 2026-02-13 08:38:33 +01:00
5627e78843 Merge pull request 'Implemented theme import and export' (#15) from feat/ThemeExportImport into v2.1
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 50s
Reviewed-on: #15
2026-02-12 14:47:50 +00:00
6f941a220c Implemented theme import and export 2026-02-12 11:09:38 +01:00
232 changed files with 2141 additions and 1554 deletions

View File

@@ -34,7 +34,7 @@ jobs:
# Publish frontend # Publish frontend
# We need to build it first so the class list files generate # We need to build it first so the class list files generate
- name: Build project - 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 - name: Build tailwind styles and extract class list
working-directory: Hosts/Moonlight.Frontend.Host/Styles working-directory: Hosts/Moonlight.Frontend.Host/Styles

View File

@@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<!-- Put your plugin references here -->
<!-- E.g. <PackageReference Include="MoonlightServers.Api" Version="2.1.0" /> -->
</ItemGroup>
</Project>

View File

@@ -1,10 +0,0 @@
using MoonCore.PluginFramework;
using Moonlight.Api.Startup;
namespace Moonlight.Api.Host;
[PluginLoader]
public partial class AppStartupLoader : IAppStartup
{
}

View File

@@ -1,5 +1,5 @@
# Base image # Base image
FROM cgr.dev/chainguard/aspnet-runtime:latest AS base FROM git.battlestati.one/moonlight-panel/app_base:moonlight AS base
WORKDIR /app WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
@@ -29,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.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.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.Api.Host/Moonlight.Api.Host.csproj"
RUN dotnet restore "Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj" RUN dotnet restore "Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj"
@@ -69,4 +72,6 @@ WORKDIR /app
COPY --from=publish /app/publish-api . COPY --from=publish /app/publish-api .
COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot 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"] ENTRYPOINT ["dotnet", "Moonlight.Api.Host.dll"]

View File

@@ -7,20 +7,18 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
<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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Moonlight.Api\Moonlight.Api.csproj"/> <ProjectReference Include="..\..\Moonlight.Api\Moonlight.Api.csproj"/>
<ProjectReference Include="..\Moonlight.Frontend.Host\Moonlight.Frontend.Host.csproj" /> <ProjectReference Include="..\Moonlight.Frontend.Host\Moonlight.Frontend.Host.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -29,4 +27,5 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<Import Project="Api.props"/>
</Project> </Project>

View File

@@ -1,22 +1,9 @@
using Moonlight.Api.Host; using Moonlight.Api;
using SimplePlugin.Generated;
var appLoader = new AppStartupLoader(); var plugins = PluginRegistry
appLoader.Initialize(); .Modules
.OfType<MoonlightPlugin>()
.ToArray();
var builder = WebApplication.CreateBuilder(args); await StartupHandler.RunAsync(args, plugins);
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();

View File

@@ -1,10 +0,0 @@
using MoonCore.PluginFramework;
using Moonlight.Frontend.Startup;
namespace Moonlight.Frontend.Host;
[PluginLoader]
public partial class AppStartupLoader : IAppStartup
{
}

View File

@@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<!-- Put your plugin references here -->
<!-- E.g. <PackageReference Include="MoonlightServers.Frontend" Version="2.1.0" /> -->
</ItemGroup>
</Project>

View File

@@ -12,13 +12,17 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/> <PackageReference Include="Microsoft.DotNet.HotReload.WebAssembly.Browser" Version="10.0.201" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/> <PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="10.0.5" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/> <PackageReference Include="Microsoft.NET.Sdk.WebAssembly.Pack" Version="10.0.5" />
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj" /> <ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj"/>
</ItemGroup> </ItemGroup>
<Import Project="Frontend.props"/>
</Project> </Project>

View File

@@ -1,15 +1,9 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Moonlight.Frontend;
using Moonlight.Frontend.Host; using SimplePlugin.Generated;
var appLoader = new AppStartupLoader(); var plugins = PluginRegistry
appLoader.Initialize(); .Modules
.OfType<MoonlightPlugin>()
.ToArray();
var builder = WebAssemblyHostBuilder.CreateDefault(args); await StartupHandler.RunAsync(args, plugins);
appLoader.PreBuild(builder);
var app = builder.Build();
appLoader.PostBuild(app);
await app.RunAsync();

View File

@@ -15,7 +15,7 @@ export default function extractTailwindClasses(opts = {}) {
}, },
OnceExit() { OnceExit() {
const classArray = Array.from(classSet).sort(); const classArray = Array.from(classSet).sort();
fs.mkdirSync('../../../Moonlight.Frontend/Styles', { recursive: true }); fs.mkdirSync('../../../Moonlight.Frontend/Styles', {recursive: true});
fs.writeFileSync('../../../Moonlight.Frontend/Styles/Moonlight.Frontend.map', classArray.join('\n')); fs.writeFileSync('../../../Moonlight.Frontend/Styles/Moonlight.Frontend.map', classArray.join('\n'));
console.log(`Extracted classes ${classArray.length}`); console.log(`Extracted classes ${classArray.length}`);
} }

View File

@@ -1,11 +1,11 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/scrollbar.css"; @import "../bin/ShadcnBlazor/scrollbar.css";
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/default-theme.css"; @import "../bin/ShadcnBlazor/default-theme.css";
@import "./theme.css"; @import "./theme.css";
@source "../../../Moonlight.Frontend/bin/ShadcnBlazor/ShadcnBlazor.map"; @source "../bin/ShadcnBlazor/ShadcnBlazor.map";
@source "../../../Moonlight.Api/**/*.razor"; @source "../../../Moonlight.Api/**/*.razor";
@source "../../../Moonlight.Api/**/*.cs"; @source "../../../Moonlight.Api/**/*.cs";
@@ -25,6 +25,7 @@
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }

View File

@@ -1,22 +1,22 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="dark"> <html class="dark" lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Moonlight</title> <title>Moonlight</title>
<base href="/" /> <base href="/"/>
<link rel="preload" id="webassembly" /> <link id="webassembly" rel="preload"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback" rel="stylesheet"/>
<link rel="stylesheet" href="style.min.css" /> <link href="style.min.css" rel="stylesheet"/>
<script type="importmap"></script> <script type="importmap"></script>
<script> <script>
window.frontendConfig = { window.frontendConfig = {
STYLE_TAG_ID: 'theme-variables', STYLE_TAG_ID: 'theme-variables',
configuration: {}, configuration: {},
applyTheme: function(cssContent) { applyTheme: function (cssContent) {
// Find or create the style tag // Find or create the style tag
let styleTag = document.getElementById(this.STYLE_TAG_ID); let styleTag = document.getElementById(this.STYLE_TAG_ID);
@@ -30,7 +30,7 @@
styleTag.textContent = cssContent; styleTag.textContent = cssContent;
}, },
reloadConfiguration: function (){ reloadConfiguration: function () {
try { try {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/frontend/config', false); xhr.open('GET', '/api/frontend/config', false);
@@ -43,8 +43,8 @@
console.error('Failed to load initial theme:', error); console.error('Failed to load initial theme:', error);
} }
}, },
getConfiguration: function (){ getConfiguration: function () {
return this.configuration; return this.configuration;
}, },
@@ -61,48 +61,48 @@
</head> </head>
<body class="bg-background text-foreground"> <body class="bg-background text-foreground">
<div id="app"> <div id="app">
<div class="h-screen w-full flex items-center justify-center"> <div class="h-screen w-full flex items-center justify-center">
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-3 rounded-lg border-dashed p-6 text-center text-balance md:p-12"> <div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-3 rounded-lg border-dashed p-6 text-center text-balance md:p-12">
<div class="flex max-w-sm flex-col items-center gap-2 text-center"> <div class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground size-10 rounded-lg [&_svg:not([class*='size-'])]:size-6"> <div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground size-10 rounded-lg [&_svg:not([class*='size-'])]:size-6">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" <svg class="lucide lucide-zap-icon lucide-zap size-6" fill="none" height="24" stroke="currentColor" stroke-linecap="round"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24"
class="lucide lucide-zap-icon lucide-zap size-6"> xmlns="http://www.w3.org/2000/svg">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/> <path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg> </svg>
</div>
</div>
<div class="text-lg font-medium tracking-tight">
Loading application
</div>
<div class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="bg-primary/20 w-full relative h-2 overflow-hidden rounded-full">
<div class="bg-primary h-full w-[var(--blazor-load-percentage,0)] flex-1 transition-all">
</div>
</div>
</div> </div>
</div> </div>
<div class="text-lg font-medium tracking-tight">
Loading application
</div>
<div class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="bg-primary/20 w-full relative h-2 overflow-hidden rounded-full">
<div class="bg-primary h-full w-[var(--blazor-load-percentage,0)] flex-1 transition-all">
</div>
</div>
</div>
</div> </div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div> </div>
</div>
<script src="/_content/ShadcnBlazor/interop.js" defer></script> <div id="blazor-error-ui">
<script src="/_content/ShadcnBlazor.Extras/interop.js" defer></script> An unhandled error has occurred.
<script src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js" defer></script> <a class="reload" href=".">Reload</a>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> <span class="dismiss">🗙</span>
</div>
<script defer src="/_content/ShadcnBlazor/interop.js"></script>
<script defer src="/_content/ShadcnBlazor.Extras/interop.js"></script>
<script defer src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js"></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body> </body>
</html> </html>

View File

@@ -1,23 +1,22 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database; using Moonlight.Api.Admin.Sys.Settings;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Services; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests.Seup; using Moonlight.Shared.Admin.Setup;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Setup;
[ApiController] [ApiController]
[Route("api/admin/setup")] [Route("api/admin/setup")]
public class SetupController : Controller public class SetupController : Controller
{ {
private const string StateSettingsKey = "Moonlight.Api.Setup.State";
private readonly DatabaseRepository<Role> RolesRepository;
private readonly SettingsService SettingsService; private readonly SettingsService SettingsService;
private readonly DatabaseRepository<User> UsersRepository; private readonly DatabaseRepository<User> UsersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private const string StateSettingsKey = "Moonlight.Api.Setup.State";
public SetupController( public SetupController(
SettingsService settingsService, SettingsService settingsService,
@@ -51,41 +50,40 @@ public class SetupController : Controller
.FirstOrDefaultAsync(x => x.Name == "Administrators"); .FirstOrDefaultAsync(x => x.Name == "Administrators");
if (adminRole == null) if (adminRole == null)
{ adminRole = await RolesRepository.AddAsync(new Role
adminRole = await RolesRepository.AddAsync(new Role()
{ {
Name = "Administrators", Name = "Administrators",
Description = "Automatically generated group for full administrator permissions", Description = "Automatically generated group for full administrator permissions",
Permissions = [ Permissions =
[
Permissions.ApiKeys.View, Permissions.ApiKeys.View,
Permissions.ApiKeys.Create, Permissions.ApiKeys.Create,
Permissions.ApiKeys.Edit, Permissions.ApiKeys.Edit,
Permissions.ApiKeys.Delete, Permissions.ApiKeys.Delete,
Permissions.Roles.View, Permissions.Roles.View,
Permissions.Roles.Create, Permissions.Roles.Create,
Permissions.Roles.Edit, Permissions.Roles.Edit,
Permissions.Roles.Delete, Permissions.Roles.Delete,
Permissions.Roles.Members, Permissions.Roles.Members,
Permissions.Users.View, Permissions.Users.View,
Permissions.Users.Create, Permissions.Users.Create,
Permissions.Users.Edit, Permissions.Users.Edit,
Permissions.Users.Delete, Permissions.Users.Delete,
Permissions.Users.Logout, Permissions.Users.Logout,
Permissions.Themes.View, Permissions.Themes.View,
Permissions.Themes.Create, Permissions.Themes.Create,
Permissions.Themes.Edit, Permissions.Themes.Edit,
Permissions.Themes.Delete, Permissions.Themes.Delete,
Permissions.System.Info, Permissions.System.Info,
Permissions.System.Diagnose, Permissions.System.Diagnose,
Permissions.System.Versions, Permissions.System.Versions,
Permissions.System.Instance, Permissions.System.Instance
] ]
}); });
}
var user = await UsersRepository var user = await UsersRepository
@@ -94,12 +92,13 @@ public class SetupController : Controller
if (user == null) if (user == null)
{ {
await UsersRepository.AddAsync(new User() await UsersRepository.AddAsync(new User
{ {
Email = dto.AdminEmail, Email = dto.AdminEmail,
Username = dto.AdminUsername, Username = dto.AdminUsername,
RoleMemberships = [ RoleMemberships =
new RoleMember() [
new RoleMember
{ {
Role = adminRole, Role = adminRole,
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow,
@@ -112,16 +111,16 @@ public class SetupController : Controller
} }
else else
{ {
user.RoleMemberships.Add(new RoleMember() user.RoleMemberships.Add(new RoleMember
{ {
Role = adminRole, Role = adminRole,
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow UpdatedAt = DateTimeOffset.UtcNow
}); });
await UsersRepository.UpdateAsync(user); await UsersRepository.UpdateAsync(user);
} }
await SettingsService.SetValueAsync(StateSettingsKey, true); await SettingsService.SetValueAsync(StateSettingsKey, true);
return NoContent(); return NoContent();

View File

@@ -2,25 +2,22 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database; using Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Implementations.ApiKeyScheme; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Admin.Sys.ApiKeys;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Sys.ApiKeys;
[Authorize] [Authorize]
[ApiController] [ApiController]
[Route("api/admin/apiKeys")] [Route("api/admin/apiKeys")]
public class ApiKeyController : Controller public class ApiKeyController : Controller
{ {
private readonly DatabaseRepository<ApiKey> KeyRepository;
private readonly HybridCache HybridCache; private readonly HybridCache HybridCache;
private readonly DatabaseRepository<ApiKey> KeyRepository;
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache) public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
{ {
@@ -48,9 +45,7 @@ public class ApiKeyController : Controller
// Filters // Filters
if (filterOptions != null) if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters) foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch query = filterOption.Key switch
{ {
nameof(ApiKey.Name) => nameof(ApiKey.Name) =>
@@ -61,8 +56,6 @@ public class ApiKeyController : Controller
_ => query _ => query
}; };
}
}
// Pagination // Pagination
var data = await query var data = await query
@@ -96,7 +89,7 @@ public class ApiKeyController : Controller
public async Task<ActionResult<ApiKeyDto>> CreateAsync([FromBody] CreateApiKeyDto request) public async Task<ActionResult<ApiKeyDto>> CreateAsync([FromBody] CreateApiKeyDto request)
{ {
var apiKey = ApiKeyMapper.ToEntity(request); var apiKey = ApiKeyMapper.ToEntity(request);
apiKey.Key = Guid.NewGuid().ToString("N").Substring(0, 32); apiKey.Key = Guid.NewGuid().ToString("N").Substring(0, 32);
var finalKey = await KeyRepository.AddAsync(apiKey); var finalKey = await KeyRepository.AddAsync(apiKey);
@@ -135,9 +128,9 @@ public class ApiKeyController : Controller
return Problem("No API key with this id found", statusCode: 404); return Problem("No API key with this id found", statusCode: 404);
await KeyRepository.RemoveAsync(apiKey); await KeyRepository.RemoveAsync(apiKey);
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key)); await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
return NoContent(); return NoContent();
} }
} }

View File

@@ -1,10 +1,9 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Admin.Sys.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Admin.Sys.ApiKeys;
[Mapper] [Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")] [SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Admin.Sys.ApiKeys;
public class ApiOptions public class ApiOptions
{ {

View File

@@ -5,19 +5,18 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared; using Moonlight.Shared;
namespace Moonlight.Api.Implementations.ApiKeyScheme; namespace Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions> public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
{ {
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
private readonly DatabaseRepository<ApiKey> ApiKeyRepository; private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
private readonly HybridCache HybridCache; private readonly HybridCache HybridCache;
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
public ApiKeySchemeHandler( public ApiKeySchemeHandler(
IOptionsMonitor<ApiKeySchemeOptions> options, IOptionsMonitor<ApiKeySchemeOptions> options,
ILoggerFactory logger, ILoggerFactory logger,
@@ -46,17 +45,13 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
cacheKey, cacheKey,
async ct => async ct =>
{ {
var x = await ApiKeyRepository return await ApiKeyRepository
.Query() .Query()
.Where(x => x.Key == authHeaderValue) .Where(x => x.Key == authHeaderValue)
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil)) .Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
.FirstOrDefaultAsync(cancellationToken: ct); .FirstOrDefaultAsync(ct);
Console.WriteLine($"API: {x?.ValidUntil}");
return x;
}, },
new HybridCacheEntryOptions() new HybridCacheEntryOptions
{ {
LocalCacheExpiration = Options.LookupL1CacheTime, LocalCacheExpiration = Options.LookupL1CacheTime,
Expiration = Options.LookupL2CacheTime Expiration = Options.LookupL2CacheTime

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
namespace Moonlight.Api.Implementations.ApiKeyScheme; namespace Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
public class ApiKeySchemeOptions : AuthenticationSchemeOptions public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{ {

View File

@@ -1,24 +1,61 @@
using System.Diagnostics; using System.Diagnostics;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moonlight.Api.Helpers; using VersionService = Moonlight.Api.Admin.Sys.Versions.VersionService;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Admin.Sys;
public class ApplicationService : IHostedService public class ApplicationService : IHostedService
{ {
private readonly VersionService VersionService;
private readonly ILogger<ApplicationService> Logger; private readonly ILogger<ApplicationService> Logger;
private readonly VersionService VersionService;
public ApplicationService(VersionService versionService, ILogger<ApplicationService> logger)
{
VersionService = versionService;
Logger = logger;
}
public DateTimeOffset StartedAt { get; private set; } public DateTimeOffset StartedAt { get; private set; }
public string VersionName { get; private set; } = "N/A"; public string VersionName { get; private set; } = "N/A";
public bool IsUpToDate { get; set; } = true; public bool IsUpToDate { get; set; } = true;
public string OperatingSystem { get; private set; } = "N/A"; public string OperatingSystem { get; private set; } = "N/A";
public ApplicationService(VersionService versionService, ILogger<ApplicationService> logger) public async Task StartAsync(CancellationToken cancellationToken)
{ {
VersionService = versionService; StartedAt = DateTimeOffset.UtcNow;
Logger = logger;
OperatingSystem = OsHelper.GetName();
try
{
var currentVersion = await VersionService.GetInstanceVersionAsync();
var latestVersion = await VersionService.GetLatestVersionAsync();
VersionName = currentVersion.Identifier;
IsUpToDate = latestVersion == null || currentVersion.Identifier == latestVersion.Identifier;
Logger.LogInformation("Running Moonlight Panel {version} on {operatingSystem}", VersionName,
OperatingSystem);
if (!IsUpToDate)
Logger.LogWarning("Your instance is not up-to-date");
if (currentVersion.IsDevelopment)
Logger.LogWarning("Your instance is running a development version");
if (currentVersion.IsPreRelease)
Logger.LogWarning("Your instance is running a pre-release version");
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled exception occurred while fetching version details");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
} }
public Task<long> GetMemoryUsageAsync() public Task<long> GetMemoryUsageAsync()
@@ -45,41 +82,8 @@ public class ApplicationService : IHostedService
// Calculate CPU usage // Calculate CPU usage
var cpuUsedMs = (endCpuTime - startCpuTime).TotalMilliseconds; var cpuUsedMs = (endCpuTime - startCpuTime).TotalMilliseconds;
var totalMsPassed = (endTime - startTime).TotalMilliseconds; var totalMsPassed = (endTime - startTime).TotalMilliseconds;
var cpuUsagePercent = (cpuUsedMs / (Environment.ProcessorCount * totalMsPassed)) * 100; var cpuUsagePercent = cpuUsedMs / (Environment.ProcessorCount * totalMsPassed) * 100;
return Math.Round(cpuUsagePercent, 2); return Math.Round(cpuUsagePercent, 2);
} }
public async Task StartAsync(CancellationToken cancellationToken)
{
StartedAt = DateTimeOffset.UtcNow;
OperatingSystem = OsHelper.GetName();
try
{
var currentVersion = await VersionService.GetInstanceVersionAsync();
var latestVersion = await VersionService.GetLatestVersionAsync();
VersionName = currentVersion.Identifier;
IsUpToDate = latestVersion == null || currentVersion.Identifier == latestVersion.Identifier;
Logger.LogInformation("Running Moonlight Panel {version} on {operatingSystem}", VersionName, OperatingSystem);
if (!IsUpToDate)
Logger.LogWarning("Your instance is not up-to-date");
if (currentVersion.IsDevelopment)
Logger.LogWarning("Your instance is running a development version");
if (currentVersion.IsPreRelease)
Logger.LogWarning("Your instance is running a pre-release version");
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled exception occurred while fetching version details");
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
} }

View File

@@ -2,14 +2,10 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper; using Moonlight.Shared.Admin.Sys.ContainerHelper;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Sys.ContainerHelper;
[ApiController] [ApiController]
[Route("api/admin/ch")] [Route("api/admin/ch")]
@@ -19,7 +15,8 @@ public class ContainerHelperController : Controller
private readonly ContainerHelperService ContainerHelperService; private readonly ContainerHelperService ContainerHelperService;
private readonly IOptions<ContainerHelperOptions> Options; private readonly IOptions<ContainerHelperOptions> Options;
public ContainerHelperController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options) public ContainerHelperController(ContainerHelperService containerHelperService,
IOptions<ContainerHelperOptions> options)
{ {
ContainerHelperService = containerHelperService; ContainerHelperService = containerHelperService;
Options = options; Options = options;

View File

@@ -1,13 +1,13 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Events; using Moonlight.Shared.Admin.Sys.ContainerHelper;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Admin.Sys.ContainerHelper;
[Mapper] [Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")] [SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")] [SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class ContainerHelperMapper public static partial class ContainerHelperMapper
{ {
public static partial RebuildEventDto ToDto(Http.Services.ContainerHelper.Events.RebuildEventDto rebuildEventDto); public static partial RebuildEventDto ToDto(Models.Events.RebuildEventDto rebuildEventDto);
} }

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Admin.Sys.ContainerHelper;
public class ContainerHelperOptions public class ContainerHelperOptions
{ {

View File

@@ -1,11 +1,10 @@
using System.Net.Http.Headers; using System.Net.Http.Json;
using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using Moonlight.Api.Http.Services.ContainerHelper; using Moonlight.Api.Admin.Sys.ContainerHelper.Models;
using Moonlight.Api.Http.Services.ContainerHelper.Requests; using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Events;
using Moonlight.Api.Http.Services.ContainerHelper.Events; using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Admin.Sys.ContainerHelper;
public class ContainerHelperService public class ContainerHelperService
{ {
@@ -42,7 +41,7 @@ public class ContainerHelperService
request.Content = JsonContent.Create( request.Content = JsonContent.Create(
new RequestRebuildDto(noBuildCache), new RequestRebuildDto(noBuildCache),
null, null,
SerializationContext.TunedOptions SerializationContext.Default.Options
); );
var response = await client.SendAsync( var response = await client.SendAsync(
@@ -54,7 +53,7 @@ public class ContainerHelperService
{ {
var responseText = await response.Content.ReadAsStringAsync(); var responseText = await response.Content.ReadAsStringAsync();
yield return new RebuildEventDto() yield return new RebuildEventDto
{ {
Type = RebuildEventType.Failed, Type = RebuildEventType.Failed,
Data = responseText Data = responseText
@@ -77,7 +76,8 @@ public class ContainerHelperService
continue; continue;
var data = line.Trim("data: "); 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; yield return deserializedData;
@@ -86,7 +86,7 @@ public class ContainerHelperService
yield break; yield break;
} while (true); } while (true);
yield return new RebuildEventDto() yield return new RebuildEventDto
{ {
Type = RebuildEventType.Succeeded, Type = RebuildEventType.Succeeded,
Data = string.Empty Data = string.Empty
@@ -100,14 +100,14 @@ public class ContainerHelperService
var response = await client.PostAsJsonAsync( var response = await client.PostAsJsonAsync(
"api/configuration/version", "api/configuration/version",
new SetVersionDto(version), new SetVersionDto(version),
SerializationContext.TunedOptions SerializationContext.Default.Options
); );
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
return; return;
var problemDetails = var problemDetails =
await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.TunedOptions); await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.Default.Options);
if (problemDetails == null) if (problemDetails == null)
throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}"); throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}");

View File

@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models.Events;
public struct RebuildEventDto
{
[JsonPropertyName("type")] public RebuildEventType Type { get; set; }
[JsonPropertyName("data")] public string Data { get; set; }
}
public enum RebuildEventType
{
Log = 0,
Failed = 1,
Succeeded = 2,
Step = 3
}

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Http.Services.ContainerHelper; namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models;
public class ProblemDetails public class ProblemDetails
{ {

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
public record RequestRebuildDto(bool NoBuildCache);

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
public record SetVersionDto(string Version);

View File

@@ -0,0 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Events;
using Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models;
[JsonSerializable(typeof(SetVersionDto))]
[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(RebuildEventDto))]
[JsonSerializable(typeof(RequestRebuildDto))]
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
public partial class SerializationContext : JsonSerializerContext
{
}

View File

@@ -1,11 +1,9 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Admin.Sys.Diagnose;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Sys.Diagnose;
[ApiController] [ApiController]
[Authorize(Policy = Permissions.System.Diagnose)] [Authorize(Policy = Permissions.System.Diagnose)]

View File

@@ -0,0 +1,17 @@
namespace Moonlight.Api.Admin.Sys.Diagnose;
public record DiagnoseResult(
DiagnoseLevel Level,
string Title,
string[] Tags,
string? Message,
string? StackStrace,
string? SolutionUrl,
string? ReportUrl);
public enum DiagnoseLevel
{
Error = 0,
Warning = 1,
Healthy = 2
}

View File

@@ -1,9 +1,8 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models; using Moonlight.Shared.Admin.Sys.Diagnose;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Admin.Sys.Diagnose;
[Mapper] [Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")] [SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,13 +1,12 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moonlight.Api.Interfaces; using Moonlight.Api.Infrastructure.Hooks;
using Moonlight.Api.Models;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Admin.Sys.Diagnose;
public class DiagnoseService public class DiagnoseService
{ {
private readonly IEnumerable<IDiagnoseProvider> Providers;
private readonly ILogger<DiagnoseService> Logger; private readonly ILogger<DiagnoseService> Logger;
private readonly IEnumerable<IDiagnoseProvider> Providers;
public DiagnoseService(IEnumerable<IDiagnoseProvider> providers, ILogger<DiagnoseService> logger) public DiagnoseService(IEnumerable<IDiagnoseProvider> providers, ILogger<DiagnoseService> logger)
{ {
@@ -20,7 +19,6 @@ public class DiagnoseService
var results = new List<DiagnoseResult>(); var results = new List<DiagnoseResult>();
foreach (var provider in Providers) foreach (var provider in Providers)
{
try try
{ {
results.AddRange( results.AddRange(
@@ -31,7 +29,6 @@ public class DiagnoseService
{ {
Logger.LogError(e, "An unhandled error occured while processing provider"); Logger.LogError(e, "An unhandled error occured while processing provider");
} }
}
return results.ToArray(); return results.ToArray();
} }

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Moonlight.Api.Helpers; namespace Moonlight.Api.Admin.Sys;
public class OsHelper public class OsHelper
{ {
@@ -53,17 +53,12 @@ public class OsHelper
string? version = null; string? version = null;
foreach (var line in lines) foreach (var line in lines)
{
if (line.StartsWith("NAME=")) if (line.StartsWith("NAME="))
name = line.Substring(5).Trim('"'); name = line.Substring(5).Trim('"');
else if (line.StartsWith("VERSION_ID=")) else if (line.StartsWith("VERSION_ID="))
version = line.Substring(11).Trim('"'); version = line.Substring(11).Trim('"');
}
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name)) return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
{
return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
}
} }
//If for some weird reason it still uses lsb release //If for some weird reason it still uses lsb release
@@ -74,17 +69,12 @@ public class OsHelper
string? version = null; string? version = null;
foreach (var line in lines) foreach (var line in lines)
{
if (line.StartsWith("DISTRIB_ID=")) if (line.StartsWith("DISTRIB_ID="))
name = line.Substring(11); name = line.Substring(11);
else if (line.StartsWith("DISTRIB_RELEASE=")) else if (line.StartsWith("DISTRIB_RELEASE="))
version = line.Substring(16); version = line.Substring(16);
}
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name)) return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
{
return string.IsNullOrEmpty(version) ? name : $"{name} {version}";
}
} }
} }
catch catch

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Admin.Sys.Settings;
public class SettingsOptions public class SettingsOptions
{ {

View File

@@ -2,25 +2,23 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Admin.Sys.Settings;
public class SettingsService public class SettingsService
{ {
private readonly DatabaseRepository<SettingsOption> Repository;
private readonly IOptions<SettingsOptions> Options;
private readonly HybridCache HybridCache;
private const string CacheKey = "Moonlight.Api.SettingsService.{0}"; private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
private readonly HybridCache HybridCache;
private readonly IOptions<SettingsOptions> Options;
private readonly DatabaseRepository<SettingsOption> Repository;
public SettingsService( public SettingsService(
DatabaseRepository<SettingsOption> repository, DatabaseRepository<SettingsOption> repository,
IOptions<SettingsOptions> options, IOptions<SettingsOptions> options,
HybridCache hybridCache HybridCache hybridCache
) )
{ {
Repository = repository; Repository = repository;
HybridCache = hybridCache; HybridCache = hybridCache;
@@ -39,9 +37,9 @@ public class SettingsService
.Query() .Query()
.Where(x => x.Key == key) .Where(x => x.Key == key)
.Select(o => o.ValueJson) .Select(o => o.ValueJson)
.FirstOrDefaultAsync(cancellationToken: ct); .FirstOrDefaultAsync(ct);
}, },
new HybridCacheEntryOptions() new HybridCacheEntryOptions
{ {
LocalCacheExpiration = Options.Value.LookupL1CacheTime, LocalCacheExpiration = Options.Value.LookupL1CacheTime,
Expiration = Options.Value.LookupL2CacheTime Expiration = Options.Value.LookupL2CacheTime
@@ -57,13 +55,13 @@ public class SettingsService
public async Task SetValueAsync<T>(string key, T value) public async Task SetValueAsync<T>(string key, T value)
{ {
var cacheKey = string.Format(CacheKey, key); var cacheKey = string.Format(CacheKey, key);
var option = await Repository var option = await Repository
.Query() .Query()
.FirstOrDefaultAsync(x => x.Key == key); .FirstOrDefaultAsync(x => x.Key == key);
var json = JsonSerializer.Serialize(value); var json = JsonSerializer.Serialize(value);
if (option != null) if (option != null)
{ {
option.ValueJson = json; option.ValueJson = json;
@@ -71,12 +69,12 @@ public class SettingsService
} }
else else
{ {
option = new SettingsOption() option = new SettingsOption
{ {
Key = key, Key = key,
ValueJson = json ValueJson = json
}; };
await Repository.AddAsync(option); await Repository.AddAsync(option);
} }

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Shared.Frontend;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.Settings;
namespace Moonlight.Api.Admin.Sys.Settings;
[ApiController]
[Authorize(Policy = Permissions.System.Settings)]
[Route("api/admin/system/settings/whiteLabeling")]
public class WhiteLabelingController : Controller
{
private readonly FrontendService FrontendService;
private readonly SettingsService SettingsService;
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

@@ -1,10 +1,9 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Admin.Sys;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Sys;
[ApiController] [ApiController]
[Route("api/admin/system")] [Route("api/admin/system")]

View File

@@ -1,10 +1,9 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Themes; using Moonlight.Shared.Admin.Sys.Themes;
using Moonlight.Shared.Http.Responses.Admin.Themes;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Admin.Sys.Themes;
[Mapper] [Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")] [SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -0,0 +1,12 @@
using VYaml.Annotations;
namespace Moonlight.Api.Admin.Sys.Themes;
[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; }
}

View File

@@ -1,24 +1,21 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Mappers; using Moonlight.Api.Shared.Frontend;
using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Admin.Sys.Themes;
using Moonlight.Shared.Http.Requests.Admin.Themes; using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Themes;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Sys.Themes;
[ApiController] [ApiController]
[Route("api/admin/themes")] [Route("api/admin/themes")]
public class ThemesController : Controller public class ThemesController : Controller
{ {
private readonly DatabaseRepository<Theme> ThemeRepository;
private readonly FrontendService FrontendService; private readonly FrontendService FrontendService;
private readonly DatabaseRepository<Theme> ThemeRepository;
public ThemesController(DatabaseRepository<Theme> themeRepository, FrontendService frontendService) public ThemesController(DatabaseRepository<Theme> themeRepository, FrontendService frontendService)
{ {
@@ -48,9 +45,7 @@ public class ThemesController : Controller
// Filters // Filters
if (filterOptions != null) if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters) foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch query = filterOption.Key switch
{ {
nameof(Theme.Name) => nameof(Theme.Name) =>
@@ -64,8 +59,6 @@ public class ThemesController : Controller
_ => query _ => query
}; };
}
}
// Pagination // Pagination
var data = await query var data = await query
@@ -116,7 +109,7 @@ public class ThemesController : Controller
if (theme == null) if (theme == null)
return Problem("No theme with this id found", statusCode: 404); return Problem("No theme with this id found", statusCode: 404);
ThemeMapper.Merge(theme, request); ThemeMapper.Merge(theme, request);
await ThemeRepository.UpdateAsync(theme); await ThemeRepository.UpdateAsync(theme);
@@ -137,9 +130,9 @@ public class ThemesController : Controller
return Problem("No theme with this id found", statusCode: 404); return Problem("No theme with this id found", statusCode: 404);
await ThemeRepository.RemoveAsync(theme); await ThemeRepository.RemoveAsync(theme);
await FrontendService.ResetCacheAsync(); await FrontendService.ResetCacheAsync();
return NoContent(); return NoContent();
} }
} }

View File

@@ -0,0 +1,74 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared;
using Moonlight.Shared.Admin.Sys.Themes;
using VYaml.Serialization;
namespace Moonlight.Api.Admin.Sys.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);
}
}

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Admin.Sys.Versions;
public class FrontendOptions public class FrontendOptions
{ {

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Models; namespace Moonlight.Api.Admin.Sys.Versions;
// Notes: // Notes:
// Identifier - This needs to be the branch to clone to build this version if // Identifier - This needs to be the branch to clone to build this version if

View File

@@ -1,9 +1,8 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models; using Moonlight.Shared.Admin.Sys.Versions;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Admin.Sys.Versions;
[Mapper] [Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")] [SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Admin.Sys.Versions;
public class VersionOptions public class VersionOptions
{ {

View File

@@ -1,22 +1,16 @@
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Models;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Admin.Sys.Versions;
public partial class VersionService public partial class VersionService
{ {
private readonly IOptions<VersionOptions> Options;
private readonly IHttpClientFactory HttpClientFactory;
private const string VersionPath = "/app/version"; private const string VersionPath = "/app/version";
private const string GiteaServer = "https://git.battlestati.one"; private const string GiteaServer = "https://git.battlestati.one";
private const string GiteaRepository = "Moonlight-Panel/Moonlight"; private const string GiteaRepository = "Moonlight-Panel/Moonlight";
private readonly IHttpClientFactory HttpClientFactory;
[GeneratedRegex(@"^v(?!1(\.|$))\d+\.[A-Za-z0-9]+(\.[A-Za-z0-9]+)*$")] private readonly IOptions<VersionOptions> Options;
private static partial Regex RegexFilter();
public VersionService( public VersionService(
IOptions<VersionOptions> options, IOptions<VersionOptions> options,
@@ -27,11 +21,14 @@ public partial class VersionService
HttpClientFactory = httpClientFactory; HttpClientFactory = httpClientFactory;
} }
[GeneratedRegex(@"^v(?!1(\.|$))\d+\.[A-Za-z0-9]+(\.[A-Za-z0-9]+)*$")]
private static partial Regex RegexFilter();
public async Task<MoonlightVersion[]> GetVersionsAsync() public async Task<MoonlightVersion[]> GetVersionsAsync()
{ {
if (Options.Value.OfflineMode) if (Options.Value.OfflineMode)
return []; return [];
var versions = new List<MoonlightVersion>(); var versions = new List<MoonlightVersion>();
var httpClient = HttpClientFactory.CreateClient(); var httpClient = HttpClientFactory.CreateClient();
@@ -42,7 +39,6 @@ public partial class VersionService
var tagsJson = await JsonNode.ParseAsync(tagsJsonStream); var tagsJson = await JsonNode.ParseAsync(tagsJsonStream);
if (tagsJson != null) if (tagsJson != null)
{
foreach (var node in tagsJson.AsArray()) foreach (var node in tagsJson.AsArray())
{ {
if (node == null) if (node == null)
@@ -50,8 +46,8 @@ public partial class VersionService
var name = node["name"]?.GetValue<string>() ?? "N/A"; var name = node["name"]?.GetValue<string>() ?? "N/A";
var createdAt = node["createdAt"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.MinValue; var createdAt = node["createdAt"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.MinValue;
if(!RegexFilter().IsMatch(name)) if (!RegexFilter().IsMatch(name))
continue; continue;
versions.Add(new MoonlightVersion( versions.Add(new MoonlightVersion(
@@ -61,8 +57,7 @@ public partial class VersionService
createdAt createdAt
)); ));
} }
}
// Branches // Branches
const string branchesPath = $"{GiteaServer}/api/v1/repos/{GiteaRepository}/branches"; const string branchesPath = $"{GiteaServer}/api/v1/repos/{GiteaRepository}/branches";
@@ -70,7 +65,6 @@ public partial class VersionService
var branchesJson = await JsonNode.ParseAsync(branchesJsonStream); var branchesJson = await JsonNode.ParseAsync(branchesJsonStream);
if (branchesJson != null) if (branchesJson != null)
{
foreach (var node in branchesJson.AsArray()) foreach (var node in branchesJson.AsArray())
{ {
if (node == null) if (node == null)
@@ -83,8 +77,8 @@ public partial class VersionService
continue; continue;
var createdAt = commit["timestamp"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.MinValue; var createdAt = commit["timestamp"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.MinValue;
if(!RegexFilter().IsMatch(name)) if (!RegexFilter().IsMatch(name))
continue; continue;
versions.Add(new MoonlightVersion( versions.Add(new MoonlightVersion(
@@ -94,7 +88,6 @@ public partial class VersionService
createdAt createdAt
)); ));
} }
}
return versions.ToArray(); return versions.ToArray();
} }
@@ -106,7 +99,9 @@ public partial class VersionService
string versionIdentifier; string versionIdentifier;
if (!string.IsNullOrWhiteSpace(Options.Value.CurrentOverride)) if (!string.IsNullOrWhiteSpace(Options.Value.CurrentOverride))
{
versionIdentifier = Options.Value.CurrentOverride; versionIdentifier = Options.Value.CurrentOverride;
}
else else
{ {
if (File.Exists(VersionPath)) if (File.Exists(VersionPath))

View File

@@ -1,11 +1,9 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Admin.Sys.Versions;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Sys.Versions;
[ApiController] [ApiController]
[Route("api/admin/versions")] [Route("api/admin/versions")]
@@ -37,8 +35,8 @@ public class VersionsController : Controller
public async Task<ActionResult<VersionDto>> GetLatestAsync() public async Task<ActionResult<VersionDto>> GetLatestAsync()
{ {
var version = await VersionService.GetLatestVersionAsync(); var version = await VersionService.GetLatestVersionAsync();
if(version == null) if (version == null)
return Problem("Unable to retrieve latest version", statusCode: 404); return Problem("Unable to retrieve latest version", statusCode: 404);
return VersionMapper.ToDto(version); return VersionMapper.ToDto(version);

View File

@@ -1,10 +1,9 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Roles; using Moonlight.Shared.Admin.Users.Roles;
using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Admin.Users.Roles;
[Mapper] [Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")] [SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
@@ -13,6 +12,7 @@ public static partial class RoleMapper
{ {
[MapProperty([nameof(Role.Members), nameof(Role.Members.Count)], nameof(RoleDto.MemberCount))] [MapProperty([nameof(Role.Members), nameof(Role.Members.Count)], nameof(RoleDto.MemberCount))]
public static partial RoleDto ToDto(Role role); public static partial RoleDto ToDto(Role role);
public static partial Role ToEntity(CreateRoleDto request); public static partial Role ToEntity(CreateRoleDto request);
public static partial void Merge([MappingTarget] Role role, UpdateRoleDto request); public static partial void Merge([MappingTarget] Role role, UpdateRoleDto request);

View File

@@ -1,23 +1,23 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database; using Moonlight.Api.Admin.Users.Users;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Mappers; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Admin.Users.Users;
using Moonlight.Shared.Http.Responses.Admin.Users; using Moonlight.Shared.Shared;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Users.Roles;
[ApiController] [ApiController]
[Authorize(Policy = Permissions.Roles.Members)] [Authorize(Policy = Permissions.Roles.Members)]
[Route("api/admin/roles/{roleId:int}/members")] [Route("api/admin/roles/{roleId:int}/members")]
public class RoleMembersController : Controller public class RoleMembersController : Controller
{ {
private readonly DatabaseRepository<User> UsersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private readonly DatabaseRepository<RoleMember> RoleMembersRepository; private readonly DatabaseRepository<RoleMember> RoleMembersRepository;
private readonly DatabaseRepository<Role> RolesRepository;
private readonly DatabaseRepository<User> UsersRepository;
public RoleMembersController( public RoleMembersController(
DatabaseRepository<User> usersRepository, DatabaseRepository<User> usersRepository,
@@ -53,19 +53,16 @@ public class RoleMembersController : Controller
// Filtering // Filtering
if (!string.IsNullOrWhiteSpace(searchTerm)) if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(x => query = query.Where(x =>
EF.Functions.ILike(x.Username, $"%{searchTerm}%") || EF.Functions.ILike(x.Username, $"%{searchTerm}%") ||
EF.Functions.ILike(x.Email, $"%{searchTerm}%") EF.Functions.ILike(x.Email, $"%{searchTerm}%")
); );
}
// Pagination // Pagination
var items = query var items = UserMapper.ProjectToDto(query
.OrderBy(x => x.Id) .OrderBy(x => x.Id)
.Skip(startIndex) .Skip(startIndex)
.Take(length) .Take(length))
.ProjectToDto()
.ToArray(); .ToArray();
var totalCount = await query.CountAsync(); var totalCount = await query.CountAsync();
@@ -95,19 +92,16 @@ public class RoleMembersController : Controller
// Filtering // Filtering
if (!string.IsNullOrWhiteSpace(searchTerm)) if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(x => query = query.Where(x =>
EF.Functions.ILike(x.Username, $"%{searchTerm}%") || EF.Functions.ILike(x.Username, $"%{searchTerm}%") ||
EF.Functions.ILike(x.Email, $"%{searchTerm}%") EF.Functions.ILike(x.Email, $"%{searchTerm}%")
); );
}
// Pagination // Pagination
var items = query var items = UserMapper.ProjectToDto(query
.OrderBy(x => x.Id) .OrderBy(x => x.Id)
.Skip(startIndex) .Skip(startIndex)
.Take(length) .Take(length))
.ProjectToDto()
.ToArray(); .ToArray();
var totalCount = await query.CountAsync(); var totalCount = await query.CountAsync();

View File

@@ -1,16 +1,13 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Admin.Users.Roles;
using Moonlight.Shared.Http.Requests.Admin.Roles; using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Admin.Users.Roles;
[ApiController] [ApiController]
[Route("api/admin/roles")] [Route("api/admin/roles")]
@@ -39,15 +36,13 @@ public class RolesController : Controller
return Problem("Invalid length specified"); return Problem("Invalid length specified");
// Query building // Query building
var query = RoleRepository var query = RoleRepository
.Query(); .Query();
// Filters // Filters
if (filterOptions != null) if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters) foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch query = filterOption.Key switch
{ {
nameof(Role.Name) => nameof(Role.Name) =>
@@ -55,8 +50,6 @@ public class RolesController : Controller
_ => query _ => query
}; };
}
}
// Pagination // Pagination
var data = await query var data = await query
@@ -106,7 +99,7 @@ public class RolesController : Controller
if (role == null) if (role == null)
return Problem("No role with this id found", statusCode: 404); return Problem("No role with this id found", statusCode: 404);
RoleMapper.Merge(role, request); RoleMapper.Merge(role, request);
await RoleRepository.UpdateAsync(role); await RoleRepository.UpdateAsync(role);

View File

@@ -3,26 +3,25 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Hooks;
using Moonlight.Api.Interfaces;
using Moonlight.Shared; using Moonlight.Shared;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Admin.Users.Users;
public class UserAuthService public class UserAuthService
{ {
private readonly DatabaseRepository<User> UserRepository; public const string CacheKeyPattern = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{{0}}";
private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<UserOptions> Options;
private readonly IEnumerable<IUserAuthHook> Hooks;
private readonly HybridCache HybridCache;
private const string UserIdClaim = "UserId"; private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt"; private const string IssuedAtClaim = "IssuedAt";
public const string CacheKeyPattern = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{{0}}"; private readonly IEnumerable<IUserAuthHook> Hooks;
private readonly HybridCache HybridCache;
private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<UserOptions> Options;
private readonly DatabaseRepository<User> UserRepository;
public UserAuthService( public UserAuthService(
DatabaseRepository<User> userRepository, DatabaseRepository<User> userRepository,
@@ -60,7 +59,7 @@ public class UserAuthService
if (user == null) // Sync user if not already existing in the database if (user == null) // Sync user if not already existing in the database
{ {
user = await UserRepository.AddAsync(new User() user = await UserRepository.AddAsync(new User
{ {
Username = username, Username = username,
Email = email, Email = email,
@@ -80,11 +79,9 @@ public class UserAuthService
]); ]);
foreach (var hook in Hooks) foreach (var hook in Hooks)
{
// Run every hook, and if any returns false, we return false as well // Run every hook, and if any returns false, we return false as well
if (!await hook.SyncAsync(principal, user)) if (!await hook.SyncAsync(principal, user))
return false; return false;
}
return true; return true;
} }
@@ -114,9 +111,9 @@ public class UserAuthService
u.InvalidateTimestamp, u.InvalidateTimestamp,
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray()) u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
) )
.FirstOrDefaultAsync(cancellationToken: ct); .FirstOrDefaultAsync(ct);
}, },
new HybridCacheEntryOptions() new HybridCacheEntryOptions
{ {
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry, LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
Expiration = Options.Value.ValidationCacheL2Expiry Expiration = Options.Value.ValidationCacheL2Expiry
@@ -146,11 +143,9 @@ public class UserAuthService
); );
foreach (var hook in Hooks) foreach (var hook in Hooks)
{
// Run every hook, and if any returns false we return false as well // Run every hook, and if any returns false we return false as well
if (!await hook.ValidateAsync(principal, userId)) if (!await hook.ValidateAsync(principal, userId))
return false; return false;
}
return true; return true;
} }

View File

@@ -1,21 +1,19 @@
using System.Collections.Frozen;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
namespace Moonlight.Api.Http.Controllers.Admin.Users; namespace Moonlight.Api.Admin.Users.Users;
[ApiController] [ApiController]
[Route("api/admin/users")] [Route("api/admin/users")]
[Authorize(Policy = Permissions.Users.Delete)] [Authorize(Policy = Permissions.Users.Delete)]
public class UserDeletionController : Controller public class UserDeletionController : Controller
{ {
private readonly UserDeletionService UserDeletionService;
private readonly DatabaseRepository<User> Repository; private readonly DatabaseRepository<User> Repository;
private readonly UserDeletionService UserDeletionService;
public UserDeletionController(UserDeletionService userDeletionService, DatabaseRepository<User> repository) public UserDeletionController(UserDeletionService userDeletionService, DatabaseRepository<User> repository)
{ {
@@ -39,7 +37,7 @@ public class UserDeletionController : Controller
{ {
return ValidationProblem( return ValidationProblem(
new ValidationProblemDetails( new ValidationProblemDetails(
new Dictionary<string, string[]>() new Dictionary<string, string[]>
{ {
{ {
string.Empty, string.Empty,
@@ -49,7 +47,7 @@ public class UserDeletionController : Controller
) )
); );
} }
await UserDeletionService.DeleteAsync(id); await UserDeletionService.DeleteAsync(id);
return NoContent(); return NoContent();
} }

View File

@@ -1,22 +1,22 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Interfaces; using Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Admin.Users.Users;
public class UserDeletionService public class UserDeletionService
{ {
private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserDeletionHook> Hooks; private readonly IEnumerable<IUserDeletionHook> Hooks;
private readonly HybridCache HybridCache; private readonly HybridCache HybridCache;
private readonly DatabaseRepository<User> Repository;
public UserDeletionService( public UserDeletionService(
DatabaseRepository<User> repository, DatabaseRepository<User> repository,
IEnumerable<IUserDeletionHook> hooks, IEnumerable<IUserDeletionHook> hooks,
HybridCache hybridCache HybridCache hybridCache
) )
{ {
Repository = repository; Repository = repository;
Hooks = hooks; Hooks = hooks;
@@ -28,20 +28,20 @@ public class UserDeletionService
var user = await Repository var user = await Repository
.Query() .Query()
.FirstOrDefaultAsync(x => x.Id == userId); .FirstOrDefaultAsync(x => x.Id == userId);
if(user == null) if (user == null)
throw new AggregateException($"User with id {userId} not found"); throw new AggregateException($"User with id {userId} not found");
var errorMessages = new List<string>(); var errorMessages = new List<string>();
foreach (var hook in Hooks) foreach (var hook in Hooks)
{ {
if (await hook.ValidateAsync(user, errorMessages)) if (await hook.ValidateAsync(user, errorMessages))
continue; continue;
return new UserDeletionValidationResult(false, errorMessages); return new UserDeletionValidationResult(false, errorMessages);
} }
return new UserDeletionValidationResult(true, []); return new UserDeletionValidationResult(true, []);
} }
@@ -50,13 +50,13 @@ public class UserDeletionService
var user = await Repository var user = await Repository
.Query() .Query()
.FirstOrDefaultAsync(x => x.Id == userId); .FirstOrDefaultAsync(x => x.Id == userId);
if(user == null) if (user == null)
throw new AggregateException($"User with id {userId} not found"); throw new AggregateException($"User with id {userId} not found");
foreach (var hook in Hooks) foreach (var hook in Hooks)
await hook.ExecuteAsync(user); await hook.ExecuteAsync(user);
await Repository.RemoveAsync(user); await Repository.RemoveAsync(user);
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id)); await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));

View File

@@ -1,12 +1,11 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
namespace Moonlight.Api.Http.Controllers.Admin.Users; namespace Moonlight.Api.Admin.Users.Users;
[ApiController] [ApiController]
[Route("api/admin/users/{id:int}/logout")] [Route("api/admin/users/{id:int}/logout")]

View File

@@ -1,16 +1,16 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Interfaces; using Moonlight.Api.Infrastructure.Hooks;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Admin.Users.Users;
public class UserLogoutService public class UserLogoutService
{ {
private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserLogoutHook> Hooks; private readonly IEnumerable<IUserLogoutHook> Hooks;
private readonly HybridCache HybridCache; private readonly HybridCache HybridCache;
private readonly DatabaseRepository<User> Repository;
public UserLogoutService( public UserLogoutService(
DatabaseRepository<User> repository, DatabaseRepository<User> repository,

View File

@@ -1,10 +1,9 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Shared.Admin.Users.Users;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Admin.Users.Users;
[Mapper] [Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")] [SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Admin.Users.Users;
public class UserOptions public class UserOptions
{ {

View File

@@ -1,16 +1,13 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database; using Moonlight.Api.Infrastructure.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Admin.Users.Users;
using Moonlight.Shared.Http.Requests.Admin.Users; using Moonlight.Shared.Shared;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Http.Controllers.Admin.Users; namespace Moonlight.Api.Admin.Users.Users;
[Authorize] [Authorize]
[ApiController] [ApiController]
@@ -40,7 +37,7 @@ public class UsersController : Controller
return Problem("Invalid length specified"); return Problem("Invalid length specified");
// Query building // Query building
var query = UserRepository var query = UserRepository
.Query(); .Query();
@@ -51,10 +48,10 @@ public class UsersController : Controller
{ {
query = filterOption.Key switch query = filterOption.Key switch
{ {
nameof(Database.Entities.User.Email) => nameof(Infrastructure.Database.Entities.User.Email) =>
query.Where(user => EF.Functions.ILike(user.Email, $"%{filterOption.Value}%")), query.Where(user => EF.Functions.ILike(user.Email, $"%{filterOption.Value}%")),
nameof(Database.Entities.User.Username) => nameof(Infrastructure.Database.Entities.User.Username) =>
query.Where(user => EF.Functions.ILike(user.Username, $"%{filterOption.Value}%")), query.Where(user => EF.Functions.ILike(user.Username, $"%{filterOption.Value}%")),
_ => query _ => query

View File

@@ -1,21 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Api.Database.Entities;
public class Theme
{
public int Id { get; set; }
[MaxLength(30)]
public required string Name { get; set; }
[MaxLength(30)]
public required string Version { get; set; }
[MaxLength(30)]
public required string Author { get; set; }
public bool IsEnabled { get; set; }
[MaxLength(20_000)]
public required string CssContent { get; set; }
}

View File

@@ -1,20 +0,0 @@
using System.Text.Json.Serialization;
namespace Moonlight.Api.Http.Services.ContainerHelper.Events;
public struct RebuildEventDto
{
[JsonPropertyName("type")]
public RebuildEventType Type { get; set; }
[JsonPropertyName("data")]
public string Data { get; set; }
}
public enum RebuildEventType
{
Log = 0,
Failed = 1,
Succeeded = 2,
Step = 3
}

View File

@@ -1,3 +0,0 @@
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
public record RequestRebuildDto(bool NoBuildCache);

View File

@@ -1,3 +0,0 @@
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
public record SetVersionDto(string Version);

View File

@@ -1,29 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Moonlight.Api.Http.Services.ContainerHelper.Events;
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
namespace Moonlight.Api.Http.Services.ContainerHelper;
[JsonSerializable(typeof(SetVersionDto))]
[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(RebuildEventDto))]
[JsonSerializable(typeof(RequestRebuildDto))]
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;
}
}
}

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Infrastructure.Configuration;
public class CacheOptions public class CacheOptions
{ {

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Infrastructure.Configuration;
public class OidcOptions public class OidcOptions
{ {

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Infrastructure.Configuration;
public class RedisOptions public class RedisOptions
{ {

View File

@@ -1,19 +1,11 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration; using Moonlight.Api.Infrastructure.Database.Entities;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Database; namespace Moonlight.Api.Infrastructure.Database;
public class DataContext : DbContext public class DataContext : DbContext
{ {
public DbSet<User> Users { get; set; }
public DbSet<SettingsOption> SettingsOptions { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<RoleMember> RoleMembers { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Theme> Themes { get; set; }
private readonly IOptions<DatabaseOptions> Options; private readonly IOptions<DatabaseOptions> Options;
public DataContext(IOptions<DatabaseOptions> options) public DataContext(IOptions<DatabaseOptions> options)
@@ -21,24 +13,36 @@ public class DataContext : DbContext
Options = options; Options = options;
} }
public DbSet<User> Users { get; set; }
public DbSet<SettingsOption> SettingsOptions { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<RoleMember> RoleMembers { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Theme> Themes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
if (optionsBuilder.IsConfigured) if (optionsBuilder.IsConfigured)
return; return;
optionsBuilder.UseNpgsql( optionsBuilder.UseNpgsql(
$"Host={Options.Value.Host};" + $"Host={Options.Value.Host};" +
$"Port={Options.Value.Port};" + $"Port={Options.Value.Port};" +
$"Username={Options.Value.Username};" + $"Username={Options.Value.Username};" +
$"Password={Options.Value.Password};" + $"Password={Options.Value.Password};" +
$"Database={Options.Value.Database}" $"Database={Options.Value.Database}",
builder =>
{
builder.MigrationsAssembly(typeof(DataContext).Assembly);
builder.MigrationsHistoryTable("MigrationsHistory", "core");
}
); );
} }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.HasDefaultSchema("core"); modelBuilder.HasDefaultSchema("core");
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
} }
} }

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Configuration; namespace Moonlight.Api.Infrastructure.Database;
public class DatabaseOptions public class DatabaseOptions
{ {

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.Api.Database.Interfaces; using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database; namespace Moonlight.Api.Infrastructure.Database;
public class DatabaseRepository<T> where T : class public class DatabaseRepository<T> where T : class
{ {
@@ -14,7 +14,10 @@ public class DatabaseRepository<T> where T : class
Set = DataContext.Set<T>(); Set = DataContext.Set<T>();
} }
public IQueryable<T> Query() => Set; public IQueryable<T> Query()
{
return Set;
}
public async Task<T> AddAsync(T entity) public async Task<T> AddAsync(T entity)
{ {
@@ -23,7 +26,7 @@ public class DatabaseRepository<T> where T : class
actionTimestamps.CreatedAt = DateTimeOffset.UtcNow; actionTimestamps.CreatedAt = DateTimeOffset.UtcNow;
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow; actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
} }
var final = Set.Add(entity); var final = Set.Add(entity);
await DataContext.SaveChangesAsync(); await DataContext.SaveChangesAsync();
return final.Entity; return final.Entity;
@@ -33,7 +36,7 @@ public class DatabaseRepository<T> where T : class
{ {
if (entity is IActionTimestamps actionTimestamps) if (entity is IActionTimestamps actionTimestamps)
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow; actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
Set.Update(entity); Set.Update(entity);
await DataContext.SaveChangesAsync(); await DataContext.SaveChangesAsync();
} }

View File

@@ -2,9 +2,8 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moonlight.Api.Database;
namespace Moonlight.Api.Services; namespace Moonlight.Api.Infrastructure.Database;
public class DbMigrationService : IHostedLifecycleService public class DbMigrationService : IHostedLifecycleService
{ {
@@ -29,7 +28,7 @@ public class DbMigrationService : IHostedLifecycleService
if (migrationNames.Length == 0) if (migrationNames.Length == 0)
{ {
Logger.LogDebug("No pending migrations found"); Logger.LogTrace("No pending migrations found");
return; return;
} }
@@ -41,9 +40,28 @@ public class DbMigrationService : IHostedLifecycleService
Logger.LogInformation("Migration complete"); Logger.LogInformation("Migration complete");
} }
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartAsync(CancellationToken cancellationToken)
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; {
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; return Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StartedAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StoppedAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StoppingAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
} }

View File

@@ -1,24 +1,21 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces; using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Entities; namespace Moonlight.Api.Infrastructure.Database.Entities;
public class ApiKey : IActionTimestamps public class ApiKey : IActionTimestamps
{ {
public int Id { get; set; } public int Id { get; set; }
[MaxLength(30)] [MaxLength(30)] public required string Name { get; set; }
public required string Name { get; set; }
[MaxLength(300)] public required string Description { get; set; }
[MaxLength(300)]
public required string Description { get; set; }
public string[] Permissions { get; set; } = []; public string[] Permissions { get; set; } = [];
public DateTimeOffset ValidUntil { get; set; } public DateTimeOffset ValidUntil { get; set; }
[MaxLength(32)] [MaxLength(32)] public string Key { get; set; }
public string Key { get; set; }
// Action timestamps // Action timestamps
public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; }

View File

@@ -1,23 +1,21 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces; using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Entities; namespace Moonlight.Api.Infrastructure.Database.Entities;
public class Role : IActionTimestamps public class Role : IActionTimestamps
{ {
public int Id { get; set; } public int Id { get; set; }
[MaxLength(30)] [MaxLength(30)] public required string Name { get; set; }
public required string Name { get; set; }
[MaxLength(300)] public required string Description { get; set; }
[MaxLength(300)]
public required string Description { get; set; }
public string[] Permissions { get; set; } = []; public string[] Permissions { get; set; } = [];
// Relations // Relations
public List<RoleMember> Members { get; set; } = []; public List<RoleMember> Members { get; set; } = [];
// Action timestamps // Action timestamps
public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; }

View File

@@ -1,6 +1,6 @@
using Moonlight.Api.Database.Interfaces; using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Entities; namespace Moonlight.Api.Infrastructure.Database.Entities;
public class RoleMember : IActionTimestamps public class RoleMember : IActionTimestamps
{ {
@@ -8,7 +8,7 @@ public class RoleMember : IActionTimestamps
public Role Role { get; set; } public Role Role { get; set; }
public User User { get; set; } public User User { get; set; }
// Action timestamps // Action timestamps
public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; }

View File

@@ -1,15 +1,14 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace Moonlight.Api.Database.Entities; namespace Moonlight.Api.Infrastructure.Database.Entities;
public class SettingsOption public class SettingsOption
{ {
public int Id { get; set; } public int Id { get; set; }
[MaxLength(256)] [MaxLength(256)] public required string Key { get; set; }
public required string Key { get; set; }
[MaxLength(4096)] [MaxLength(4096)]
[Column(TypeName = "jsonb")] [Column(TypeName = "jsonb")]
public required string ValueJson { get; set; } public required string ValueJson { get; set; }

View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Api.Infrastructure.Database.Entities;
public class Theme
{
public int Id { get; set; }
[MaxLength(30)] public required string Name { get; set; }
[MaxLength(30)] public required string Version { get; set; }
[MaxLength(30)] public required string Author { get; set; }
public bool IsEnabled { get; set; }
[MaxLength(20_000)] public required string CssContent { get; set; }
}

View File

@@ -1,25 +1,23 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Moonlight.Api.Database.Interfaces; using Moonlight.Api.Infrastructure.Database.Interfaces;
namespace Moonlight.Api.Database.Entities; namespace Moonlight.Api.Infrastructure.Database.Entities;
public class User : IActionTimestamps public class User : IActionTimestamps
{ {
public int Id { get; set; } public int Id { get; set; }
// Base information // Base information
[MaxLength(50)] [MaxLength(50)] public required string Username { get; set; }
public required string Username { get; set; }
[MaxLength(254)] public required string Email { get; set; }
[MaxLength(254)]
public required string Email { get; set; }
// Authentication // Authentication
public DateTimeOffset InvalidateTimestamp { get; set; } public DateTimeOffset InvalidateTimestamp { get; set; }
// Relations // Relations
public List<RoleMember> RoleMemberships { get; set; } = []; public List<RoleMember> RoleMemberships { get; set; } = [];
// Action timestamps // Action timestamps
public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; }

View File

@@ -1,4 +1,4 @@
namespace Moonlight.Api.Database.Interfaces; namespace Moonlight.Api.Infrastructure.Database.Interfaces;
internal interface IActionTimestamps internal interface IActionTimestamps
{ {

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable

View File

@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Infrastructure.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable

View File

@@ -2,7 +2,7 @@
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Logging.Console;
namespace Moonlight.Api.Helpers; namespace Moonlight.Api.Infrastructure.Helpers;
public class AppConsoleFormatter : ConsoleFormatter public class AppConsoleFormatter : ConsoleFormatter
{ {
@@ -58,7 +58,9 @@ public class AppConsoleFormatter : ConsoleFormatter
textWriter.WriteLine(logEntry.Exception.ToString()); textWriter.WriteLine(logEntry.Exception.ToString());
} }
else else
{
textWriter.WriteLine(); textWriter.WriteLine();
}
} }
private static (string text, string color) GetLevelInfo(LogLevel logLevel) private static (string text, string color) GetLevelInfo(LogLevel logLevel)

View File

@@ -1,6 +1,6 @@
using Moonlight.Api.Models; using Moonlight.Api.Admin.Sys.Diagnose;
namespace Moonlight.Api.Interfaces; namespace Moonlight.Api.Infrastructure.Hooks;
public interface IDiagnoseProvider public interface IDiagnoseProvider
{ {

View File

@@ -1,12 +1,12 @@
using System.Security.Claims; using System.Security.Claims;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Interfaces; namespace Moonlight.Api.Infrastructure.Hooks;
public interface IUserAuthHook public interface IUserAuthHook
{ {
public Task<bool> SyncAsync(ClaimsPrincipal principal, User trackedUser); public Task<bool> SyncAsync(ClaimsPrincipal principal, User trackedUser);
// Every implementation of this function should execute as fast as possible // Every implementation of this function should execute as fast as possible
// as this directly impacts every api call // as this directly impacts every api call
public Task<bool> ValidateAsync(ClaimsPrincipal principal, int userId); public Task<bool> ValidateAsync(ClaimsPrincipal principal, int userId);

View File

@@ -1,6 +1,6 @@
using Moonlight.Api.Database.Entities; using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Interfaces; namespace Moonlight.Api.Infrastructure.Hooks;
public interface IUserDeletionHook public interface IUserDeletionHook
{ {

View File

@@ -0,0 +1,8 @@
using Moonlight.Api.Infrastructure.Database.Entities;
namespace Moonlight.Api.Infrastructure.Hooks;
public interface IUserLogoutHook
{
public Task ExecuteAsync(User user);
}

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Moonlight.Shared; using Moonlight.Shared;
namespace Moonlight.Api.Implementations; namespace Moonlight.Api.Infrastructure.Implementations;
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement> public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{ {

View File

@@ -2,22 +2,22 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Shared; using Moonlight.Shared;
namespace Moonlight.Api.Implementations; namespace Moonlight.Api.Infrastructure.Implementations;
public class PermissionPolicyProvider : IAuthorizationPolicyProvider public class PermissionPolicyProvider : IAuthorizationPolicyProvider
{ {
private readonly DefaultAuthorizationPolicyProvider FallbackProvider; private readonly DefaultAuthorizationPolicyProvider FallbackProvider;
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options) public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
{ {
FallbackProvider = new DefaultAuthorizationPolicyProvider(options); FallbackProvider = new DefaultAuthorizationPolicyProvider(options);
} }
public async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName) public async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{ {
if (!policyName.StartsWith(Permissions.Prefix, StringComparison.OrdinalIgnoreCase)) if (!policyName.StartsWith(Permissions.Prefix, StringComparison.OrdinalIgnoreCase))
return await FallbackProvider.GetPolicyAsync(policyName); return await FallbackProvider.GetPolicyAsync(policyName);
var policy = new AuthorizationPolicyBuilder(); var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName)); policy.AddRequirements(new PermissionRequirement(policyName));
@@ -25,18 +25,22 @@ public class PermissionPolicyProvider : IAuthorizationPolicyProvider
} }
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> FallbackProvider.GetDefaultPolicyAsync(); {
return FallbackProvider.GetDefaultPolicyAsync();
}
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
=> FallbackProvider.GetFallbackPolicyAsync(); {
return FallbackProvider.GetFallbackPolicyAsync();
}
} }
public class PermissionRequirement : IAuthorizationRequirement public class PermissionRequirement : IAuthorizationRequirement
{ {
public string Identifier { get; }
public PermissionRequirement(string identifier) public PermissionRequirement(string identifier)
{ {
Identifier = identifier; Identifier = identifier;
} }
public string Identifier { get; }
} }

Some files were not shown because too many files have changed in this diff Show More