Compare commits
27 Commits
dd44e5bb86
...
feat/Restr
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d1e6e1690 | |||
| 1257e8b950 | |||
| 93de9c5d00 | |||
| 3dff8c8f6d | |||
| 95a848e571 | |||
| 9d557eea4e | |||
| 94c1aac0ac | |||
| 3bddd64d91 | |||
| 5ad7a6db7b | |||
| 9b9272cd6e | |||
| 31cf34ed04 | |||
| a9b0020131 | |||
| e3b432aae6 | |||
| 06f27605ba | |||
| 0bd138df63 | |||
| d7b725f541 | |||
| 0f26aaf803 | |||
| c45e177001 | |||
| 627e9bb161 | |||
| 1fc33ebf03 | |||
| 64e4d7201e | |||
| 816aa01319 | |||
| 5627e78843 | |||
| 795cec149f | |||
| 83fcb4a921 | |||
| 741a60adc6 | |||
| 6f941a220c |
@@ -34,7 +34,7 @@ jobs:
|
|||||||
# Publish frontend
|
# 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
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -405,4 +405,5 @@ FodyWeavers.xsd
|
|||||||
# Secrets
|
# Secrets
|
||||||
**/.env
|
**/.env
|
||||||
**/appsettings.json
|
**/appsettings.json
|
||||||
**/appsettings.Development.json
|
**/appsettings.Development.json
|
||||||
|
**/storage
|
||||||
6
Hosts/Moonlight.Api.Host/Api.props
Normal file
6
Hosts/Moonlight.Api.Host/Api.props
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Project>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Put your plugin references here -->
|
||||||
|
<!-- E.g. <PackageReference Include="MoonlightServers.Api" Version="2.1.0" /> -->
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using MoonCore.PluginFramework;
|
|
||||||
using Moonlight.Api.Startup;
|
|
||||||
|
|
||||||
namespace Moonlight.Api.Host;
|
|
||||||
|
|
||||||
[PluginLoader]
|
|
||||||
public partial class AppStartupLoader : IAppStartup
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
# Base image
|
# 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
|
||||||
|
|
||||||
# Build dependencies
|
# Install required packages
|
||||||
RUN apt-get update; apt-get install nodejs npm -y; apt-get clean
|
RUN apt-get update; apt-get install unzip -y; apt-get clean
|
||||||
|
RUN curl -fsSL https://bun.sh/install | bash
|
||||||
|
ENV PATH="/root/.bun/bin:${PATH}"
|
||||||
|
|
||||||
# Build options
|
# Build options
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
@@ -15,7 +17,7 @@ WORKDIR /src/Hosts/Moonlight.Frontend.Host/Styles
|
|||||||
|
|
||||||
COPY ["Hosts/Moonlight.Frontend.Host/Styles/package.json", "package.json"]
|
COPY ["Hosts/Moonlight.Frontend.Host/Styles/package.json", "package.json"]
|
||||||
|
|
||||||
RUN npm install
|
RUN bun install
|
||||||
|
|
||||||
# Restore nuget packages
|
# Restore nuget packages
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@@ -27,6 +29,9 @@ COPY ["Moonlight.Shared/Moonlight.Shared.csproj", "Moonlight.Shared/"]
|
|||||||
COPY ["Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj", "Hosts/Moonlight.Frontend.Host/"]
|
COPY ["Hosts/Moonlight.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"
|
||||||
|
|
||||||
@@ -39,7 +44,7 @@ WORKDIR "/src/Hosts/Moonlight.Frontend.Host"
|
|||||||
RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
|
RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
|
||||||
|
|
||||||
WORKDIR "/src/Hosts/Moonlight.Frontend.Host/Styles"
|
WORKDIR "/src/Hosts/Moonlight.Frontend.Host/Styles"
|
||||||
RUN npm run build
|
RUN bun run build
|
||||||
|
|
||||||
# Build projects
|
# Build projects
|
||||||
WORKDIR "/src/Hosts/Moonlight.Api.Host"
|
WORKDIR "/src/Hosts/Moonlight.Api.Host"
|
||||||
@@ -67,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"]
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using MoonCore.PluginFramework;
|
|
||||||
using Moonlight.Frontend.Startup;
|
|
||||||
|
|
||||||
namespace Moonlight.Frontend.Host;
|
|
||||||
|
|
||||||
[PluginLoader]
|
|
||||||
public partial class AppStartupLoader : IAppStartup
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
6
Hosts/Moonlight.Frontend.Host/Frontend.props
Normal file
6
Hosts/Moonlight.Frontend.Host/Frontend.props
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Project>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Put your plugin references here -->
|
||||||
|
<!-- E.g. <PackageReference Include="MoonlightServers.Frontend" Version="2.1.0" /> -->
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -1,27 +1,28 @@
|
|||||||
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 Microsoft.Extensions.Caching.Hybrid;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
|
||||||
using Moonlight.Api.Mappers;
|
using Moonlight.Api.Infrastructure.Database;
|
||||||
|
using Moonlight.Api.Infrastructure.Database.Entities;
|
||||||
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 HybridCache HybridCache;
|
||||||
private readonly DatabaseRepository<ApiKey> KeyRepository;
|
private readonly DatabaseRepository<ApiKey> KeyRepository;
|
||||||
|
|
||||||
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository)
|
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
|
||||||
{
|
{
|
||||||
KeyRepository = keyRepository;
|
KeyRepository = keyRepository;
|
||||||
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -44,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) =>
|
||||||
@@ -57,8 +56,6 @@ public class ApiKeyController : Controller
|
|||||||
|
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
var data = await query
|
var data = await query
|
||||||
@@ -92,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);
|
||||||
@@ -114,6 +111,8 @@ public class ApiKeyController : Controller
|
|||||||
ApiKeyMapper.Merge(apiKey, request);
|
ApiKeyMapper.Merge(apiKey, request);
|
||||||
await KeyRepository.UpdateAsync(apiKey);
|
await KeyRepository.UpdateAsync(apiKey);
|
||||||
|
|
||||||
|
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
|
||||||
|
|
||||||
return ApiKeyMapper.ToDto(apiKey);
|
return ApiKeyMapper.ToDto(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +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));
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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")]
|
||||||
7
Moonlight.Api/Admin/Sys/ApiKeys/ApiOptions.cs
Normal file
7
Moonlight.Api/Admin/Sys/ApiKeys/ApiOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Admin.Sys.ApiKeys;
|
||||||
|
|
||||||
|
public class ApiOptions
|
||||||
|
{
|
||||||
|
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||||
|
}
|
||||||
@@ -2,32 +2,31 @@ using System.Security.Claims;
|
|||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
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 IMemoryCache MemoryCache;
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
private const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
|
|
||||||
|
|
||||||
public ApiKeySchemeHandler(
|
public ApiKeySchemeHandler(
|
||||||
IOptionsMonitor<ApiKeySchemeOptions> options,
|
IOptionsMonitor<ApiKeySchemeOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
DatabaseRepository<ApiKey> apiKeyRepository,
|
DatabaseRepository<ApiKey> apiKeyRepository,
|
||||||
IMemoryCache memoryCache
|
HybridCache hybridCache
|
||||||
) : base(options, logger, encoder)
|
) : base(options, logger, encoder)
|
||||||
{
|
{
|
||||||
ApiKeyRepository = apiKeyRepository;
|
ApiKeyRepository = apiKeyRepository;
|
||||||
MemoryCache = memoryCache;
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
@@ -41,25 +40,26 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
|||||||
return AuthenticateResult.Fail("Invalid api key specified");
|
return AuthenticateResult.Fail("Invalid api key specified");
|
||||||
|
|
||||||
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
|
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
|
||||||
|
|
||||||
if (!MemoryCache.TryGetValue<ApiKeySession>(cacheKey, out var apiKey))
|
|
||||||
{
|
|
||||||
apiKey = await ApiKeyRepository
|
|
||||||
.Query()
|
|
||||||
.Where(x => x.Key == authHeaderValue)
|
|
||||||
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (apiKey == null)
|
var apiKey = await HybridCache.GetOrCreateAsync<ApiKeySession?>(
|
||||||
return AuthenticateResult.Fail("Invalid api key specified");
|
cacheKey,
|
||||||
|
async ct =>
|
||||||
|
{
|
||||||
|
return await ApiKeyRepository
|
||||||
|
.Query()
|
||||||
|
.Where(x => x.Key == authHeaderValue)
|
||||||
|
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
},
|
||||||
|
new HybridCacheEntryOptions
|
||||||
|
{
|
||||||
|
LocalCacheExpiration = Options.LookupL1CacheTime,
|
||||||
|
Expiration = Options.LookupL2CacheTime
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
MemoryCache.Set(cacheKey, apiKey, Options.LookupCacheTime);
|
if (apiKey == null)
|
||||||
}
|
return AuthenticateResult.Fail("Invalid api key specified");
|
||||||
else
|
|
||||||
{
|
|
||||||
if (apiKey == null)
|
|
||||||
return AuthenticateResult.Fail("Invalid api key specified");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
|
if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
|
||||||
return AuthenticateResult.Fail("Api key expired");
|
return AuthenticateResult.Fail("Api key expired");
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Admin.Sys.ApiKeys.Scheme;
|
||||||
|
|
||||||
|
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
||||||
|
{
|
||||||
|
public TimeSpan LookupL1CacheTime { get; set; }
|
||||||
|
public TimeSpan LookupL2CacheTime { get; set; }
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
namespace Moonlight.Api.Admin.Sys.ContainerHelper;
|
||||||
|
|
||||||
public class ContainerHelperOptions
|
public class ContainerHelperOptions
|
||||||
{
|
{
|
||||||
@@ -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}");
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models;
|
||||||
|
|
||||||
public class ProblemDetails
|
public class ProblemDetails
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
|
||||||
|
|
||||||
|
public record RequestRebuildDto(bool NoBuildCache);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Moonlight.Api.Admin.Sys.ContainerHelper.Models.Requests;
|
||||||
|
|
||||||
|
public record SetVersionDto(string Version);
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -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)]
|
||||||
17
Moonlight.Api/Admin/Sys/Diagnose/DiagnoseResult.cs
Normal file
17
Moonlight.Api/Admin/Sys/Diagnose/DiagnoseResult.cs
Normal 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
|
||||||
|
}
|
||||||
@@ -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")]
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
7
Moonlight.Api/Admin/Sys/Settings/SettingsOptions.cs
Normal file
7
Moonlight.Api/Admin/Sys/Settings/SettingsOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Admin.Sys.Settings;
|
||||||
|
|
||||||
|
public class SettingsOptions
|
||||||
|
{
|
||||||
|
public TimeSpan LookupL1CacheTime { get; set; } = TimeSpan.FromMinutes(1);
|
||||||
|
public TimeSpan LookupL2CacheTime { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
|
}
|
||||||
@@ -1,29 +1,27 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
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 IMemoryCache Cache;
|
|
||||||
|
|
||||||
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,
|
||||||
IMemoryCache cache
|
HybridCache hybridCache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Repository = repository;
|
Repository = repository;
|
||||||
Cache = cache;
|
HybridCache = hybridCache;
|
||||||
Options = options;
|
Options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,37 +29,39 @@ public class SettingsService
|
|||||||
{
|
{
|
||||||
var cacheKey = string.Format(CacheKey, key);
|
var cacheKey = string.Format(CacheKey, key);
|
||||||
|
|
||||||
if (Cache.TryGetValue<string>(cacheKey, out var value))
|
var value = await HybridCache.GetOrCreateAsync<string?>(
|
||||||
return JsonSerializer.Deserialize<T>(value!);
|
|
||||||
|
|
||||||
value = await Repository
|
|
||||||
.Query()
|
|
||||||
.Where(x => x.Key == key)
|
|
||||||
.Select(o => o.ValueJson)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(value))
|
|
||||||
return default;
|
|
||||||
|
|
||||||
Cache.Set(
|
|
||||||
cacheKey,
|
cacheKey,
|
||||||
value,
|
async ct =>
|
||||||
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
{
|
||||||
|
return await Repository
|
||||||
|
.Query()
|
||||||
|
.Where(x => x.Key == key)
|
||||||
|
.Select(o => o.ValueJson)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
},
|
||||||
|
new HybridCacheEntryOptions
|
||||||
|
{
|
||||||
|
LocalCacheExpiration = Options.Value.LookupL1CacheTime,
|
||||||
|
Expiration = Options.Value.LookupL2CacheTime
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return default;
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<T>(value);
|
return JsonSerializer.Deserialize<T>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -69,15 +69,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache.Remove(cacheKey);
|
await HybridCache.RemoveAsync(cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
47
Moonlight.Api/Admin/Sys/Settings/WhiteLabelingController.cs
Normal file
47
Moonlight.Api/Admin/Sys/Settings/WhiteLabelingController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")]
|
||||||
@@ -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")]
|
||||||
12
Moonlight.Api/Admin/Sys/Themes/ThemeTransferModel.cs
Normal file
12
Moonlight.Api/Admin/Sys/Themes/ThemeTransferModel.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
74
Moonlight.Api/Admin/Sys/Themes/TransferController.cs
Normal file
74
Moonlight.Api/Admin/Sys/Themes/TransferController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
namespace Moonlight.Api.Admin.Sys.Versions;
|
||||||
|
|
||||||
public class FrontendOptions
|
public class FrontendOptions
|
||||||
{
|
{
|
||||||
@@ -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
|
||||||
@@ -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")]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
namespace Moonlight.Api.Admin.Sys.Versions;
|
||||||
|
|
||||||
public class VersionOptions
|
public class VersionOptions
|
||||||
{
|
{
|
||||||
@@ -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))
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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();
|
||||||
@@ -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);
|
||||||
@@ -1,41 +1,41 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
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 IMemoryCache Cache;
|
|
||||||
private readonly ILogger<UserAuthService> Logger;
|
|
||||||
private readonly IOptions<SessionOptions> Options;
|
|
||||||
private readonly IEnumerable<IUserAuthHook> Hooks;
|
|
||||||
|
|
||||||
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,
|
||||||
ILogger<UserAuthService> logger,
|
ILogger<UserAuthService> logger,
|
||||||
IMemoryCache cache, IOptions<SessionOptions> options,
|
IOptions<UserOptions> options,
|
||||||
IEnumerable<IUserAuthHook> hooks
|
IEnumerable<IUserAuthHook> hooks,
|
||||||
|
HybridCache hybridCache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
UserRepository = userRepository;
|
UserRepository = userRepository;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
Cache = cache;
|
|
||||||
Options = options;
|
Options = options;
|
||||||
Hooks = hooks;
|
Hooks = hooks;
|
||||||
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||||
@@ -59,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,
|
||||||
@@ -79,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;
|
||||||
}
|
}
|
||||||
@@ -101,32 +99,29 @@ public class UserAuthService
|
|||||||
|
|
||||||
var cacheKey = string.Format(CacheKeyPattern, userId);
|
var cacheKey = string.Format(CacheKeyPattern, userId);
|
||||||
|
|
||||||
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
|
var user = await HybridCache.GetOrCreateAsync<UserSession?>(
|
||||||
{
|
cacheKey,
|
||||||
user = await UserRepository
|
async ct =>
|
||||||
.Query()
|
{
|
||||||
.AsNoTracking()
|
return await UserRepository
|
||||||
.Where(u => u.Id == userId)
|
.Query()
|
||||||
.Select(u => new UserSession(
|
.AsNoTracking()
|
||||||
u.InvalidateTimestamp,
|
.Where(u => u.Id == userId)
|
||||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
.Select(u => new UserSession(
|
||||||
)
|
u.InvalidateTimestamp,
|
||||||
.FirstOrDefaultAsync();
|
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||||
|
)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
},
|
||||||
|
new HybridCacheEntryOptions
|
||||||
|
{
|
||||||
|
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
|
||||||
|
Expiration = Options.Value.ValidationCacheL2Expiry
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Cache.Set(
|
|
||||||
cacheKey,
|
|
||||||
user,
|
|
||||||
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (user == null)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
||||||
|
|
||||||
@@ -146,13 +141,11 @@ public class UserAuthService
|
|||||||
principal.Identities.First().AddClaims(
|
principal.Identities.First().AddClaims(
|
||||||
user.Permissions.Select(x => new Claim(Permissions.ClaimType, x))
|
user.Permissions.Select(x => new Claim(Permissions.ClaimType, x))
|
||||||
);
|
);
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
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 IMemoryCache Cache;
|
private readonly HybridCache HybridCache;
|
||||||
|
private readonly DatabaseRepository<User> Repository;
|
||||||
|
|
||||||
public UserDeletionService(DatabaseRepository<User> repository, IEnumerable<IUserDeletionHook> hooks, IMemoryCache cache)
|
public UserDeletionService(
|
||||||
|
DatabaseRepository<User> repository,
|
||||||
|
IEnumerable<IUserDeletionHook> hooks,
|
||||||
|
HybridCache hybridCache
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Repository = repository;
|
Repository = repository;
|
||||||
Hooks = hooks;
|
Hooks = hooks;
|
||||||
Cache = cache;
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
|
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
|
||||||
@@ -24,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, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,15 +50,16 @@ 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);
|
||||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
|
||||||
|
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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")]
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
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 IMemoryCache Cache;
|
private readonly HybridCache HybridCache;
|
||||||
|
private readonly DatabaseRepository<User> Repository;
|
||||||
|
|
||||||
public UserLogoutService(
|
public UserLogoutService(
|
||||||
DatabaseRepository<User> repository,
|
DatabaseRepository<User> repository,
|
||||||
IEnumerable<IUserLogoutHook> hooks,
|
IEnumerable<IUserLogoutHook> hooks,
|
||||||
IMemoryCache cache
|
HybridCache hybridCache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Repository = repository;
|
Repository = repository;
|
||||||
Hooks = hooks;
|
Hooks = hooks;
|
||||||
Cache = cache;
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogoutAsync(int userId)
|
public async Task LogoutAsync(int userId)
|
||||||
@@ -28,16 +28,16 @@ public class UserLogoutService
|
|||||||
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);
|
||||||
|
|
||||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
||||||
await Repository.UpdateAsync(user);
|
await Repository.UpdateAsync(user);
|
||||||
|
|
||||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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")]
|
||||||
7
Moonlight.Api/Admin/Users/Users/UserOptions.cs
Normal file
7
Moonlight.Api/Admin/Users/Users/UserOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Admin.Users.Users;
|
||||||
|
|
||||||
|
public class UserOptions
|
||||||
|
{
|
||||||
|
public TimeSpan ValidationCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public TimeSpan ValidationCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
|
||||||
|
|
||||||
public class ApiOptions
|
|
||||||
{
|
|
||||||
public int LookupCacheMinutes { get; set; } = 3;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
|
||||||
|
|
||||||
public class SessionOptions
|
|
||||||
{
|
|
||||||
public int ValidationCacheMinutes { get; set; } = 3;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
|
||||||
|
|
||||||
public class SettingsOptions
|
|
||||||
{
|
|
||||||
public int CacheMinutes { get; set; } = 3;
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
|
||||||
|
|
||||||
public record RequestRebuildDto(bool NoBuildCache);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
|
||||||
|
|
||||||
public record SetVersionDto(string Version);
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
|
|
||||||
namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
|
||||||
|
|
||||||
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
|
||||||
{
|
|
||||||
public TimeSpan LookupCacheTime { get; set; }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.Api.Infrastructure.Configuration;
|
||||||
|
|
||||||
|
public class CacheOptions
|
||||||
|
{
|
||||||
|
public bool EnableLayer2 { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
namespace Moonlight.Api.Infrastructure.Configuration;
|
||||||
|
|
||||||
public class OidcOptions
|
public class OidcOptions
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Infrastructure.Configuration;
|
||||||
|
|
||||||
|
public class RedisOptions
|
||||||
|
{
|
||||||
|
public bool Enable { get; set; }
|
||||||
|
public string ConnectionString { get; set; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
namespace Moonlight.Api.Infrastructure.Database;
|
||||||
|
|
||||||
public class DatabaseOptions
|
public class DatabaseOptions
|
||||||
{
|
{
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
@@ -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; }
|
||||||
@@ -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; }
|
||||||
@@ -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; }
|
||||||
18
Moonlight.Api/Infrastructure/Database/Entities/Theme.cs
Normal file
18
Moonlight.Api/Infrastructure/Database/Entities/Theme.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Moonlight.Api.Database.Interfaces;
|
namespace Moonlight.Api.Infrastructure.Database.Interfaces;
|
||||||
|
|
||||||
internal interface IActionTimestamps
|
internal interface IActionTimestamps
|
||||||
{
|
{
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user