Compare commits
50 Commits
c8fe11bd2b
...
v2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dff8c8f6d | |||
| 95a848e571 | |||
| 9d557eea4e | |||
| 94c1aac0ac | |||
| 3bddd64d91 | |||
| 5ad7a6db7b | |||
| 9b9272cd6e | |||
| 31cf34ed04 | |||
| a9b0020131 | |||
| e3b432aae6 | |||
| 06f27605ba | |||
| 0bd138df63 | |||
| d7b725f541 | |||
| 0f26aaf803 | |||
| c45e177001 | |||
| 627e9bb161 | |||
| 1fc33ebf03 | |||
| 64e4d7201e | |||
| 816aa01319 | |||
| 5627e78843 | |||
| 795cec149f | |||
| 83fcb4a921 | |||
| 741a60adc6 | |||
| 6f941a220c | |||
| dd44e5bb86 | |||
| 7b38662f8f | |||
| 6d854d82d3 | |||
| ac1c28d20d | |||
| 5efe591f85 | |||
| 4daf986f3e | |||
| cc7f55c988 | |||
| 11a2f9818a | |||
| 6a151394a7 | |||
| 178ac5ac20 | |||
| 91944a5ef6 | |||
| affdadf3aa | |||
| 8d9a7bb8b3 | |||
| 1f631be1c7 | |||
| 5b4959771c | |||
| b8e1bbb28c | |||
| 09b11cc4ad | |||
| 660319afec | |||
| 8181404f0c | |||
| e1207b8d9b | |||
| 97a676ccd7 | |||
| 136620f1e6 | |||
| 9b11360a0e | |||
| deb69e6014 | |||
| 4e96905fb2 | |||
| e2f344ab4e |
@@ -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
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -400,8 +400,10 @@ FodyWeavers.xsd
|
|||||||
# Style builds
|
# Style builds
|
||||||
**/style.min.css
|
**/style.min.css
|
||||||
**/package-lock.json
|
**/package-lock.json
|
||||||
|
**/bun.lock
|
||||||
|
|
||||||
# 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,15 +7,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
|
|
||||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
|
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
<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>
|
||||||
@@ -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>
|
||||||
@@ -14,11 +14,13 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
|
||||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
|
<PackageReference Include="SimplePlugin" Version="1.0.2" />
|
||||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
|
<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();
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ namespace Moonlight.Api.Configuration;
|
|||||||
|
|
||||||
public class ApiOptions
|
public class ApiOptions
|
||||||
{
|
{
|
||||||
public int LookupCacheMinutes { get; set; } = 3;
|
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||||
}
|
}
|
||||||
6
Moonlight.Api/Configuration/CacheOptions.cs
Normal file
6
Moonlight.Api/Configuration/CacheOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class CacheOptions
|
||||||
|
{
|
||||||
|
public bool EnableLayer2 { get; set; }
|
||||||
|
}
|
||||||
7
Moonlight.Api/Configuration/ContainerHelperOptions.cs
Normal file
7
Moonlight.Api/Configuration/ContainerHelperOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class ContainerHelperOptions
|
||||||
|
{
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
public string Url { get; set; } = "http://helper:8080";
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ public class OidcOptions
|
|||||||
{
|
{
|
||||||
public string Authority { get; set; }
|
public string Authority { get; set; }
|
||||||
public bool RequireHttpsMetadata { get; set; } = true;
|
public bool RequireHttpsMetadata { get; set; } = true;
|
||||||
|
public bool DisableHttpsOnlyCookies { get; set; }
|
||||||
public string ResponseType { get; set; } = "code";
|
public string ResponseType { get; set; } = "code";
|
||||||
public string[]? Scopes { get; set; }
|
public string[]? Scopes { get; set; }
|
||||||
public string ClientId { get; set; }
|
public string ClientId { get; set; }
|
||||||
|
|||||||
7
Moonlight.Api/Configuration/RedisOptions.cs
Normal file
7
Moonlight.Api/Configuration/RedisOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class RedisOptions
|
||||||
|
{
|
||||||
|
public bool Enable { get; set; }
|
||||||
|
public string ConnectionString { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
|
||||||
|
|
||||||
public class SessionOptions
|
|
||||||
{
|
|
||||||
public int ValidationCacheMinutes { get; set; } = 3;
|
|
||||||
}
|
|
||||||
7
Moonlight.Api/Configuration/SettingsOptions.cs
Normal file
7
Moonlight.Api/Configuration/SettingsOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class SettingsOptions
|
||||||
|
{
|
||||||
|
public TimeSpan LookupL1CacheTime { get; set; } = TimeSpan.FromMinutes(1);
|
||||||
|
public TimeSpan LookupL2CacheTime { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
|
}
|
||||||
7
Moonlight.Api/Configuration/UserOptions.cs
Normal file
7
Moonlight.Api/Configuration/UserOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class UserOptions
|
||||||
|
{
|
||||||
|
public TimeSpan ValidationCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public TimeSpan ValidationCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||||
|
}
|
||||||
6
Moonlight.Api/Constants/FrontendSettingConstants.cs
Normal file
6
Moonlight.Api/Constants/FrontendSettingConstants.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.Api.Constants;
|
||||||
|
|
||||||
|
public class FrontendSettingConstants
|
||||||
|
{
|
||||||
|
public const string Name = "Moonlight.Frontend.Name";
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ public class ApiKey : IActionTimestamps
|
|||||||
public required string Description { get; set; }
|
public required string Description { get; set; }
|
||||||
|
|
||||||
public string[] Permissions { get; set; } = [];
|
public string[] Permissions { get; set; } = [];
|
||||||
|
public DateTimeOffset ValidUntil { get; set; }
|
||||||
|
|
||||||
[MaxLength(32)]
|
[MaxLength(32)]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace Moonlight.Api.Database.Entities;
|
namespace Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
@@ -10,5 +11,6 @@ public class SettingsOption
|
|||||||
public required string Key { get; set; }
|
public required string Key { get; set; }
|
||||||
|
|
||||||
[MaxLength(4096)]
|
[MaxLength(4096)]
|
||||||
public required string Value { get; set; }
|
[Column(TypeName = "jsonb")]
|
||||||
|
public required string ValueJson { get; set; }
|
||||||
}
|
}
|
||||||
251
Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs
generated
Normal file
251
Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs
generated
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20260129134620_SwitchedToJsonForSettingsOption")]
|
||||||
|
partial class SwitchedToJsonForSettingsOption
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("core")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("character varying(300)");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("Permissions")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ApiKeys", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("character varying(300)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("Permissions")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Roles", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RoleMembers", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("ValueJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("SettingsOptions", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.Theme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Author")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("CssContent")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20000)
|
||||||
|
.HasColumnType("character varying(20000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Themes", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(254)
|
||||||
|
.HasColumnType("character varying(254)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("InvalidateTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
|
||||||
|
.WithMany("Members")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
|
||||||
|
.WithMany("RoleMemberships")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Members");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RoleMemberships");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SwitchedToJsonForSettingsOption : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Value",
|
||||||
|
schema: "core",
|
||||||
|
table: "SettingsOptions");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ValueJson",
|
||||||
|
schema: "core",
|
||||||
|
table: "SettingsOptions",
|
||||||
|
type: "jsonb",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ValueJson",
|
||||||
|
schema: "core",
|
||||||
|
table: "SettingsOptions");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Value",
|
||||||
|
schema: "core",
|
||||||
|
table: "SettingsOptions",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
254
Moonlight.Api/Database/Migrations/20260209114238_AddedValidUntilToApiKeys.Designer.cs
generated
Normal file
254
Moonlight.Api/Database/Migrations/20260209114238_AddedValidUntilToApiKeys.Designer.cs
generated
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20260209114238_AddedValidUntilToApiKeys")]
|
||||||
|
partial class AddedValidUntilToApiKeys
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("core")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("character varying(300)");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("Permissions")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ValidUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ApiKeys", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("character varying(300)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("Permissions")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Roles", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RoleMembers", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("ValueJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("SettingsOptions", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.Theme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Author")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("CssContent")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20000)
|
||||||
|
.HasColumnType("character varying(20000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Themes", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(254)
|
||||||
|
.HasColumnType("character varying(254)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("InvalidateTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users", "core");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
|
||||||
|
.WithMany("Members")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
|
||||||
|
.WithMany("RoleMemberships")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Members");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RoleMemberships");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedValidUntilToApiKeys : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
name: "ValidUntil",
|
||||||
|
schema: "core",
|
||||||
|
table: "ApiKeys",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ValidUntil",
|
||||||
|
schema: "core",
|
||||||
|
table: "ApiKeys");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,9 @@ namespace Moonlight.Api.Database.Migrations
|
|||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ValidUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ApiKeys", "core");
|
b.ToTable("ApiKeys", "core");
|
||||||
@@ -136,10 +139,10 @@ namespace Moonlight.Api.Database.Migrations
|
|||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(256)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("Value")
|
b.Property<string>("ValueJson")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(4096)
|
.HasMaxLength(4096)
|
||||||
.HasColumnType("character varying(4096)");
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Implementations.ApiKeyScheme;
|
||||||
using Moonlight.Api.Mappers;
|
using Moonlight.Api.Mappers;
|
||||||
using Moonlight.Shared;
|
using Moonlight.Shared;
|
||||||
using Moonlight.Shared.Http.Requests;
|
using Moonlight.Shared.Http.Requests;
|
||||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||||
using Moonlight.Shared.Http.Responses;
|
using Moonlight.Shared.Http.Responses;
|
||||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||||
|
|
||||||
@@ -18,10 +20,12 @@ namespace Moonlight.Api.Http.Controllers.Admin;
|
|||||||
public class ApiKeyController : Controller
|
public class ApiKeyController : Controller
|
||||||
{
|
{
|
||||||
private readonly DatabaseRepository<ApiKey> KeyRepository;
|
private readonly DatabaseRepository<ApiKey> KeyRepository;
|
||||||
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository)
|
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
|
||||||
{
|
{
|
||||||
KeyRepository = keyRepository;
|
KeyRepository = keyRepository;
|
||||||
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -114,6 +118,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 +135,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moonlight.Api.Configuration;
|
||||||
|
using Moonlight.Api.Mappers;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/ch")]
|
||||||
|
[Authorize(Policy = Permissions.System.Instance)]
|
||||||
|
public class ContainerHelperController : Controller
|
||||||
|
{
|
||||||
|
private readonly ContainerHelperService ContainerHelperService;
|
||||||
|
private readonly IOptions<ContainerHelperOptions> Options;
|
||||||
|
|
||||||
|
public ContainerHelperController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
|
||||||
|
{
|
||||||
|
ContainerHelperService = containerHelperService;
|
||||||
|
Options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("status")]
|
||||||
|
public async Task<ActionResult<ContainerHelperStatusDto>> GetStatusAsync()
|
||||||
|
{
|
||||||
|
if (!Options.Value.IsEnabled)
|
||||||
|
return new ContainerHelperStatusDto(false, false);
|
||||||
|
|
||||||
|
var status = await ContainerHelperService.CheckConnectionAsync();
|
||||||
|
|
||||||
|
return new ContainerHelperStatusDto(true, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("rebuild")]
|
||||||
|
public Task<IResult> RebuildAsync([FromBody] RequestRebuildDto request)
|
||||||
|
{
|
||||||
|
var result = ContainerHelperService.RebuildAsync(request.NoBuildCache);
|
||||||
|
var mappedResult = result.Select(ContainerHelperMapper.ToDto);
|
||||||
|
|
||||||
|
return Task.FromResult<IResult>(
|
||||||
|
TypedResults.ServerSentEvents(mappedResult)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("version")]
|
||||||
|
public async Task<ActionResult> SetVersionAsync([FromBody] SetVersionDto request)
|
||||||
|
{
|
||||||
|
await ContainerHelperService.SetVersionAsync(request.Version);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
|
|||||||
using Moonlight.Api.Mappers;
|
using Moonlight.Api.Mappers;
|
||||||
using Moonlight.Shared;
|
using Moonlight.Shared;
|
||||||
using Moonlight.Shared.Http.Responses;
|
using Moonlight.Shared.Http.Responses;
|
||||||
using Moonlight.Shared.Http.Responses.Users;
|
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
|
|||||||
using Moonlight.Api.Mappers;
|
using Moonlight.Api.Mappers;
|
||||||
using Moonlight.Shared;
|
using Moonlight.Shared;
|
||||||
using Moonlight.Shared.Http.Requests;
|
using Moonlight.Shared.Http.Requests;
|
||||||
using Moonlight.Shared.Http.Requests.Roles;
|
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||||
using Moonlight.Shared.Http.Responses;
|
using Moonlight.Shared.Http.Responses;
|
||||||
using Moonlight.Shared.Http.Responses.Admin;
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Moonlight.Api.Constants;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.Settings;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Settings;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin.Settings;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Policy = Permissions.System.Settings)]
|
||||||
|
[Route("api/admin/system/settings/whiteLabeling")]
|
||||||
|
public class WhiteLabelingController : Controller
|
||||||
|
{
|
||||||
|
private readonly SettingsService SettingsService;
|
||||||
|
private readonly FrontendService FrontendService;
|
||||||
|
|
||||||
|
public WhiteLabelingController(SettingsService settingsService, FrontendService frontendService)
|
||||||
|
{
|
||||||
|
SettingsService = settingsService;
|
||||||
|
FrontendService = frontendService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<WhiteLabelingDto>> GetAsync()
|
||||||
|
{
|
||||||
|
var dto = new WhiteLabelingDto
|
||||||
|
{
|
||||||
|
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"
|
||||||
|
};
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<WhiteLabelingDto>> PostAsync([FromBody] SetWhiteLabelingDto request)
|
||||||
|
{
|
||||||
|
await SettingsService.SetValueAsync(FrontendSettingConstants.Name, request.Name);
|
||||||
|
await FrontendService.ResetCacheAsync();
|
||||||
|
|
||||||
|
var dto = new WhiteLabelingDto
|
||||||
|
{
|
||||||
|
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"
|
||||||
|
};
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
Moonlight.Api/Http/Controllers/Admin/SetupController.cs
Normal file
129
Moonlight.Api/Http/Controllers/Admin/SetupController.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
using Moonlight.Shared.Http.Requests.Seup;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/setup")]
|
||||||
|
public class SetupController : Controller
|
||||||
|
{
|
||||||
|
private readonly SettingsService SettingsService;
|
||||||
|
private readonly DatabaseRepository<User> UsersRepository;
|
||||||
|
private readonly DatabaseRepository<Role> RolesRepository;
|
||||||
|
|
||||||
|
private const string StateSettingsKey = "Moonlight.Api.Setup.State";
|
||||||
|
|
||||||
|
public SetupController(
|
||||||
|
SettingsService settingsService,
|
||||||
|
DatabaseRepository<User> usersRepository,
|
||||||
|
DatabaseRepository<Role> rolesRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
SettingsService = settingsService;
|
||||||
|
UsersRepository = usersRepository;
|
||||||
|
RolesRepository = rolesRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<ActionResult> GetSetupAsync()
|
||||||
|
{
|
||||||
|
var hasBeenSetup = await SettingsService.GetValueAsync<bool>(StateSettingsKey);
|
||||||
|
|
||||||
|
if (hasBeenSetup)
|
||||||
|
return Problem("This instance is already configured", statusCode: 405);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<ActionResult> ApplySetupAsync([FromBody] ApplySetupDto dto)
|
||||||
|
{
|
||||||
|
var adminRole = await RolesRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Name == "Administrators");
|
||||||
|
|
||||||
|
if (adminRole == null)
|
||||||
|
{
|
||||||
|
adminRole = await RolesRepository.AddAsync(new Role()
|
||||||
|
{
|
||||||
|
Name = "Administrators",
|
||||||
|
Description = "Automatically generated group for full administrator permissions",
|
||||||
|
Permissions = [
|
||||||
|
Permissions.ApiKeys.View,
|
||||||
|
Permissions.ApiKeys.Create,
|
||||||
|
Permissions.ApiKeys.Edit,
|
||||||
|
Permissions.ApiKeys.Delete,
|
||||||
|
|
||||||
|
Permissions.Roles.View,
|
||||||
|
Permissions.Roles.Create,
|
||||||
|
Permissions.Roles.Edit,
|
||||||
|
Permissions.Roles.Delete,
|
||||||
|
Permissions.Roles.Members,
|
||||||
|
|
||||||
|
Permissions.Users.View,
|
||||||
|
Permissions.Users.Create,
|
||||||
|
Permissions.Users.Edit,
|
||||||
|
Permissions.Users.Delete,
|
||||||
|
Permissions.Users.Logout,
|
||||||
|
|
||||||
|
Permissions.Themes.View,
|
||||||
|
Permissions.Themes.Create,
|
||||||
|
Permissions.Themes.Edit,
|
||||||
|
Permissions.Themes.Delete,
|
||||||
|
|
||||||
|
Permissions.System.Info,
|
||||||
|
Permissions.System.Diagnose,
|
||||||
|
Permissions.System.Versions,
|
||||||
|
Permissions.System.Instance,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var user = await UsersRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(u => u.Email == dto.AdminEmail);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
await UsersRepository.AddAsync(new User()
|
||||||
|
{
|
||||||
|
Email = dto.AdminEmail,
|
||||||
|
Username = dto.AdminUsername,
|
||||||
|
RoleMemberships = [
|
||||||
|
new RoleMember()
|
||||||
|
{
|
||||||
|
Role = adminRole,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow
|
||||||
|
}
|
||||||
|
],
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user.RoleMemberships.Add(new RoleMember()
|
||||||
|
{
|
||||||
|
Role = adminRole,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
await UsersRepository.UpdateAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SettingsService.SetValueAsync(StateSettingsKey, true);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,11 @@ using Moonlight.Api.Mappers;
|
|||||||
using Moonlight.Api.Services;
|
using Moonlight.Api.Services;
|
||||||
using Moonlight.Shared;
|
using Moonlight.Shared;
|
||||||
using Moonlight.Shared.Http.Requests;
|
using Moonlight.Shared.Http.Requests;
|
||||||
using Moonlight.Shared.Http.Requests.Themes;
|
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||||
using Moonlight.Shared.Http.Responses;
|
using Moonlight.Shared.Http.Responses;
|
||||||
using Moonlight.Shared.Http.Responses.Themes;
|
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
namespace Moonlight.Api.Http.Controllers.Admin.Themes;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/admin/themes")]
|
[Route("api/admin/themes")]
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Mappers;
|
||||||
|
using Moonlight.Api.Models;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||||
|
using VYaml.Serialization;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin.Themes;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/themes")]
|
||||||
|
[Authorize(Policy = Permissions.Themes.View)]
|
||||||
|
public class TransferController : Controller
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<Theme> ThemeRepository;
|
||||||
|
|
||||||
|
public TransferController(DatabaseRepository<Theme> themeRepository)
|
||||||
|
{
|
||||||
|
ThemeRepository = themeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:int}/export")]
|
||||||
|
public async Task<ActionResult> ExportAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var theme = await ThemeRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (theme == null)
|
||||||
|
return Problem("No theme with that id found", statusCode: 404);
|
||||||
|
|
||||||
|
var yml = YamlSerializer.Serialize(new ThemeTransferModel()
|
||||||
|
{
|
||||||
|
Name = theme.Name,
|
||||||
|
Author = theme.Author,
|
||||||
|
CssContent = theme.CssContent,
|
||||||
|
Version = theme.Version
|
||||||
|
});
|
||||||
|
|
||||||
|
return File(yml.ToArray(), "text/yaml", $"{theme.Name}.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("import")]
|
||||||
|
public async Task<ActionResult<ThemeDto>> ImportAsync()
|
||||||
|
{
|
||||||
|
var themeToImport = await YamlSerializer.DeserializeAsync<ThemeTransferModel>(Request.Body);
|
||||||
|
|
||||||
|
var existingTheme = await ThemeRepository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Name == themeToImport.Name && x.Author == themeToImport.Author);
|
||||||
|
|
||||||
|
if (existingTheme == null)
|
||||||
|
{
|
||||||
|
var finalTheme = await ThemeRepository.AddAsync(new Theme()
|
||||||
|
{
|
||||||
|
Name = themeToImport.Name,
|
||||||
|
Author = themeToImport.Author,
|
||||||
|
CssContent = themeToImport.CssContent,
|
||||||
|
Version = themeToImport.Version
|
||||||
|
});
|
||||||
|
|
||||||
|
return ThemeMapper.ToDto(finalTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
existingTheme.CssContent = themeToImport.CssContent;
|
||||||
|
existingTheme.Version = themeToImport.Version;
|
||||||
|
|
||||||
|
await ThemeRepository.UpdateAsync(existingTheme);
|
||||||
|
|
||||||
|
return ThemeMapper.ToDto(existingTheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Moonlight.Api.Database;
|
|
||||||
using Moonlight.Api.Database.Entities;
|
|
||||||
using Moonlight.Api.Services;
|
|
||||||
using Moonlight.Shared;
|
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/users/{id:int}")]
|
|
||||||
public class UserActionsController : Controller
|
|
||||||
{
|
|
||||||
// Consider building a service for deletion and logout or actions in general
|
|
||||||
|
|
||||||
private readonly DatabaseRepository<User> UsersRepository;
|
|
||||||
private readonly IMemoryCache Cache;
|
|
||||||
|
|
||||||
public UserActionsController(DatabaseRepository<User> usersRepository, IMemoryCache cache)
|
|
||||||
{
|
|
||||||
UsersRepository = usersRepository;
|
|
||||||
Cache = cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("logout")]
|
|
||||||
[Authorize(Policy = Permissions.Users.Logout)]
|
|
||||||
public async Task<ActionResult> LogoutAsync([FromRoute] int id)
|
|
||||||
{
|
|
||||||
var user = await UsersRepository
|
|
||||||
.Query()
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == id);
|
|
||||||
|
|
||||||
if(user == null)
|
|
||||||
return Problem("User not found", statusCode: 404);
|
|
||||||
|
|
||||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
|
||||||
await UsersRepository.UpdateAsync(user);
|
|
||||||
|
|
||||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, id));
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin.Users;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/users")]
|
||||||
|
[Authorize(Policy = Permissions.Users.Delete)]
|
||||||
|
public class UserDeletionController : Controller
|
||||||
|
{
|
||||||
|
private readonly UserDeletionService UserDeletionService;
|
||||||
|
private readonly DatabaseRepository<User> Repository;
|
||||||
|
|
||||||
|
public UserDeletionController(UserDeletionService userDeletionService, DatabaseRepository<User> repository)
|
||||||
|
{
|
||||||
|
UserDeletionService = userDeletionService;
|
||||||
|
Repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var userExists = await Repository
|
||||||
|
.Query()
|
||||||
|
.AnyAsync(user => user.Id == id);
|
||||||
|
|
||||||
|
if (!userExists)
|
||||||
|
return Problem("No user with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
var validationResult = await UserDeletionService.ValidateAsync(id);
|
||||||
|
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
{
|
||||||
|
return ValidationProblem(
|
||||||
|
new ValidationProblemDetails(
|
||||||
|
new Dictionary<string, string[]>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
string.Empty,
|
||||||
|
validationResult.ErrorMessages.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserDeletionService.DeleteAsync(id);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin.Users;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/users/{id:int}/logout")]
|
||||||
|
[Authorize(Policy = Permissions.Users.Logout)]
|
||||||
|
public class UserLogoutController : Controller
|
||||||
|
{
|
||||||
|
private readonly UserLogoutService LogoutService;
|
||||||
|
private readonly DatabaseRepository<User> Repository;
|
||||||
|
|
||||||
|
public UserLogoutController(
|
||||||
|
UserLogoutService logoutService,
|
||||||
|
DatabaseRepository<User> repository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
LogoutService = logoutService;
|
||||||
|
Repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult> LogoutAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var userExists = await Repository
|
||||||
|
.Query()
|
||||||
|
.AnyAsync(user => user.Id == id);
|
||||||
|
|
||||||
|
if (!userExists)
|
||||||
|
return Problem("No user with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
await LogoutService.LogoutAsync(id);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,11 @@ using Moonlight.Api.Database.Entities;
|
|||||||
using Moonlight.Api.Mappers;
|
using Moonlight.Api.Mappers;
|
||||||
using Moonlight.Shared;
|
using Moonlight.Shared;
|
||||||
using Moonlight.Shared.Http.Requests;
|
using Moonlight.Shared.Http.Requests;
|
||||||
using Moonlight.Shared.Http.Requests.Users;
|
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||||
using Moonlight.Shared.Http.Responses;
|
using Moonlight.Shared.Http.Responses;
|
||||||
using Moonlight.Shared.Http.Responses.Users;
|
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
namespace Moonlight.Api.Http.Controllers.Admin.Users;
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@@ -117,19 +117,4 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
return UserMapper.ToDto(user);
|
return UserMapper.ToDto(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
|
||||||
[Authorize(Policy = Permissions.Users.Delete)]
|
|
||||||
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
|
||||||
{
|
|
||||||
var user = await UserRepository
|
|
||||||
.Query()
|
|
||||||
.FirstOrDefaultAsync(user => user.Id == id);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return Problem("No user with this id found", statusCode: 404);
|
|
||||||
|
|
||||||
await UserRepository.RemoveAsync(user);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
using Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers;
|
namespace Moonlight.Api.Http.Controllers;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Moonlight.Api.Mappers;
|
using Moonlight.Api.Mappers;
|
||||||
using Moonlight.Api.Services;
|
using Moonlight.Api.Services;
|
||||||
using Moonlight.Shared.Http.Responses.Frontend;
|
using Moonlight.Shared.Http.Responses.Admin.Frontend;
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers;
|
namespace Moonlight.Api.Http.Controllers;
|
||||||
|
|
||||||
|
|||||||
13
Moonlight.Api/Http/Controllers/PingController.cs
Normal file
13
Moonlight.Api/Http/Controllers/PingController.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/ping")]
|
||||||
|
public class PingController : Controller
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public IActionResult Get() => Ok("Pong");
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||||
|
|
||||||
|
public class ProblemDetails
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public int Status { get; set; }
|
||||||
|
public string? Detail { get; set; }
|
||||||
|
public Dictionary<string, string[]>? Errors { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||||
|
|
||||||
|
public record RequestRebuildDto(bool NoBuildCache);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||||
|
|
||||||
|
public record SetVersionDto(string Version);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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))]
|
||||||
|
|
||||||
|
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||||
|
public partial class SerializationContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ 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.Database;
|
||||||
@@ -14,18 +14,20 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
|||||||
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
||||||
{
|
{
|
||||||
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
||||||
private readonly IMemoryCache MemoryCache;
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
|
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
|
||||||
|
|
||||||
public ApiKeySchemeHandler(
|
public ApiKeySchemeHandler(
|
||||||
IOptionsMonitor<ApiKeySchemeOptions> options,
|
IOptionsMonitor<ApiKeySchemeOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
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()
|
||||||
@@ -38,24 +40,30 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
|||||||
if (authHeaderValue.Length > 32)
|
if (authHeaderValue.Length > 32)
|
||||||
return AuthenticateResult.Fail("Invalid api key specified");
|
return AuthenticateResult.Fail("Invalid api key specified");
|
||||||
|
|
||||||
if (!MemoryCache.TryGetValue<ApiKeySession>(authHeaderValue, out var apiKey))
|
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
|
||||||
|
|
||||||
|
var apiKey = await HybridCache.GetOrCreateAsync<ApiKeySession?>(
|
||||||
|
cacheKey,
|
||||||
|
async ct =>
|
||||||
{
|
{
|
||||||
apiKey = await ApiKeyRepository
|
return await ApiKeyRepository
|
||||||
.Query()
|
.Query()
|
||||||
.Where(x => x.Key == authHeaderValue)
|
.Where(x => x.Key == authHeaderValue)
|
||||||
.Select(x => new ApiKeySession(x.Permissions))
|
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||||
|
},
|
||||||
if (apiKey == null)
|
new HybridCacheEntryOptions()
|
||||||
return AuthenticateResult.Fail("Invalid api key specified");
|
|
||||||
|
|
||||||
MemoryCache.Set(authHeaderValue, apiKey, Options.LookupCacheTime);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
|
LocalCacheExpiration = Options.LookupL1CacheTime,
|
||||||
|
Expiration = Options.LookupL2CacheTime
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (apiKey == null)
|
if (apiKey == null)
|
||||||
return AuthenticateResult.Fail("Invalid api key specified");
|
return AuthenticateResult.Fail("Invalid api key specified");
|
||||||
}
|
|
||||||
|
if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
|
||||||
|
return AuthenticateResult.Fail("Api key expired");
|
||||||
|
|
||||||
return AuthenticateResult.Success(new AuthenticationTicket(
|
return AuthenticateResult.Success(new AuthenticationTicket(
|
||||||
new ClaimsPrincipal(
|
new ClaimsPrincipal(
|
||||||
@@ -67,5 +75,5 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ApiKeySession(string[] Permissions);
|
private record ApiKeySession(string[] Permissions, DateTimeOffset ValidUntil);
|
||||||
}
|
}
|
||||||
@@ -4,5 +4,6 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
|||||||
|
|
||||||
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
||||||
{
|
{
|
||||||
public TimeSpan LookupCacheTime { get; set; }
|
public TimeSpan LookupL1CacheTime { get; set; }
|
||||||
|
public TimeSpan LookupL2CacheTime { get; set; }
|
||||||
}
|
}
|
||||||
13
Moonlight.Api/Interfaces/IUserAuthHook.cs
Normal file
13
Moonlight.Api/Interfaces/IUserAuthHook.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserAuthHook
|
||||||
|
{
|
||||||
|
public Task<bool> SyncAsync(ClaimsPrincipal principal, User trackedUser);
|
||||||
|
|
||||||
|
// Every implementation of this function should execute as fast as possible
|
||||||
|
// as this directly impacts every api call
|
||||||
|
public Task<bool> ValidateAsync(ClaimsPrincipal principal, int userId);
|
||||||
|
}
|
||||||
9
Moonlight.Api/Interfaces/IUserDeletionHook.cs
Normal file
9
Moonlight.Api/Interfaces/IUserDeletionHook.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserDeletionHook
|
||||||
|
{
|
||||||
|
public Task<bool> ValidateAsync(User user, List<string> errors);
|
||||||
|
public Task ExecuteAsync(User user);
|
||||||
|
}
|
||||||
8
Moonlight.Api/Interfaces/IUserLogoutHook.cs
Normal file
8
Moonlight.Api/Interfaces/IUserLogoutHook.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserLogoutHook
|
||||||
|
{
|
||||||
|
public Task ExecuteAsync(User user);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
namespace Moonlight.Api.Mappers;
|
namespace Moonlight.Api.Mappers;
|
||||||
|
|||||||
13
Moonlight.Api/Mappers/ContainerHelperMapper.cs
Normal file
13
Moonlight.Api/Mappers/ContainerHelperMapper.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Moonlight.Shared.Http.Events;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Mappers;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
[SuppressMessage("Mapper", "RMG020: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 RebuildEventDto ToDto(Http.Services.ContainerHelper.Events.RebuildEventDto rebuildEventDto);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Moonlight.Api.Models;
|
using Moonlight.Api.Models;
|
||||||
using Moonlight.Shared.Http.Responses.Frontend;
|
using Moonlight.Shared.Http.Responses.Admin.Frontend;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
namespace Moonlight.Api.Mappers;
|
namespace Moonlight.Api.Mappers;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
using Moonlight.Shared.Http.Requests.Roles;
|
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||||
using Moonlight.Shared.Http.Responses.Admin;
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
using Moonlight.Shared.Http.Requests.Themes;
|
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||||
using Moonlight.Shared.Http.Responses.Themes;
|
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
namespace Moonlight.Api.Mappers;
|
namespace Moonlight.Api.Mappers;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
using Moonlight.Shared.Http.Requests.Users;
|
|
||||||
using Moonlight.Shared.Http.Responses.Users;
|
|
||||||
using Moonlight.Api.Database.Entities;
|
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.Mappers;
|
||||||
|
|
||||||
|
|||||||
12
Moonlight.Api/Models/ThemeTransferModel.cs
Normal file
12
Moonlight.Api/Models/ThemeTransferModel.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using VYaml.Annotations;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Models;
|
||||||
|
|
||||||
|
[YamlObject]
|
||||||
|
public partial class ThemeTransferModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Version { get; set; }
|
||||||
|
public string Author { get; set; }
|
||||||
|
public string CssContent { get; set; }
|
||||||
|
}
|
||||||
@@ -25,9 +25,14 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
|
||||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||||
|
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||||
|
<PackageReference Include="VYaml" Version="1.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -35,4 +40,8 @@
|
|||||||
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
|
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Http\Services\ContainerHelper\Responses\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
28
Moonlight.Api/MoonlightPlugin.cs
Normal file
28
Moonlight.Api/MoonlightPlugin.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using SimplePlugin.Abstractions;
|
||||||
|
|
||||||
|
namespace Moonlight.Api;
|
||||||
|
|
||||||
|
public abstract class MoonlightPlugin : IPluginModule
|
||||||
|
{
|
||||||
|
protected MoonlightPlugin[] Plugins { get; private set; }
|
||||||
|
|
||||||
|
public void Initialize(MoonlightPlugin[] plugins)
|
||||||
|
{
|
||||||
|
Plugins = plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void PreBuild(WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void PostBuild(WebApplication application)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void PostMiddleware(WebApplication application)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
116
Moonlight.Api/Services/ContainerHelperService.cs
Normal file
116
Moonlight.Api/Services/ContainerHelperService.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Moonlight.Api.Http.Services.ContainerHelper;
|
||||||
|
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||||
|
using Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Services;
|
||||||
|
|
||||||
|
public class ContainerHelperService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory HttpClientFactory;
|
||||||
|
|
||||||
|
public ContainerHelperService(IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
HttpClientFactory = httpClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CheckConnectionAsync()
|
||||||
|
{
|
||||||
|
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await client.GetAsync("api/ping");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<RebuildEventDto> RebuildAsync(bool noBuildCache)
|
||||||
|
{
|
||||||
|
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, "api/rebuild");
|
||||||
|
|
||||||
|
request.Content = JsonContent.Create(
|
||||||
|
new RequestRebuildDto(noBuildCache),
|
||||||
|
null,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
var response = await client.SendAsync(
|
||||||
|
request,
|
||||||
|
HttpCompletionOption.ResponseHeadersRead
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
yield return new RebuildEventDto()
|
||||||
|
{
|
||||||
|
Type = RebuildEventType.Failed,
|
||||||
|
Data = responseText
|
||||||
|
};
|
||||||
|
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||||
|
using var streamReader = new StreamReader(responseStream);
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var line = await streamReader.ReadLineAsync();
|
||||||
|
|
||||||
|
if (line == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var data = line.Trim("data: ");
|
||||||
|
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.Default.Options);
|
||||||
|
|
||||||
|
yield return deserializedData;
|
||||||
|
|
||||||
|
// Exit if service will go down for a clean exit
|
||||||
|
if (deserializedData is { Type: RebuildEventType.Step, Data: "ServiceDown" })
|
||||||
|
yield break;
|
||||||
|
} while (true);
|
||||||
|
|
||||||
|
yield return new RebuildEventDto()
|
||||||
|
{
|
||||||
|
Type = RebuildEventType.Succeeded,
|
||||||
|
Data = string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetVersionAsync(string version)
|
||||||
|
{
|
||||||
|
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||||
|
|
||||||
|
var response = await client.PostAsJsonAsync(
|
||||||
|
"api/configuration/version",
|
||||||
|
new SetVersionDto(version),
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var problemDetails =
|
||||||
|
await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.Default.Options);
|
||||||
|
|
||||||
|
if (problemDetails == null)
|
||||||
|
throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}");
|
||||||
|
|
||||||
|
throw new HttpRequestException($"Failed to set version: {problemDetails.Detail ?? problemDetails.Title}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moonlight.Api.Configuration;
|
using Moonlight.Api.Configuration;
|
||||||
|
using Moonlight.Api.Constants;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
using Moonlight.Api.Models;
|
using Moonlight.Api.Models;
|
||||||
@@ -13,14 +14,16 @@ public class FrontendService
|
|||||||
private readonly IMemoryCache Cache;
|
private readonly IMemoryCache Cache;
|
||||||
private readonly DatabaseRepository<Theme> ThemeRepository;
|
private readonly DatabaseRepository<Theme> ThemeRepository;
|
||||||
private readonly IOptions<FrontendOptions> Options;
|
private readonly IOptions<FrontendOptions> Options;
|
||||||
|
private readonly SettingsService SettingsService;
|
||||||
|
|
||||||
private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}";
|
private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}";
|
||||||
|
|
||||||
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options)
|
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options, SettingsService settingsService)
|
||||||
{
|
{
|
||||||
Cache = cache;
|
Cache = cache;
|
||||||
ThemeRepository = themeRepository;
|
ThemeRepository = themeRepository;
|
||||||
Options = options;
|
Options = options;
|
||||||
|
SettingsService = settingsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FrontendConfiguration> GetConfigurationAsync()
|
public async Task<FrontendConfiguration> GetConfigurationAsync()
|
||||||
@@ -35,7 +38,9 @@ public class FrontendService
|
|||||||
.Query()
|
.Query()
|
||||||
.FirstOrDefaultAsync(x => x.IsEnabled);
|
.FirstOrDefaultAsync(x => x.IsEnabled);
|
||||||
|
|
||||||
var config = new FrontendConfiguration("Moonlight", theme?.CssContent);
|
var name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name);
|
||||||
|
|
||||||
|
var config = new FrontendConfiguration(name ?? "Moonlight", theme?.CssContent);
|
||||||
|
|
||||||
Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes));
|
Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes));
|
||||||
|
|
||||||
|
|||||||
85
Moonlight.Api/Services/SettingsService.cs
Normal file
85
Moonlight.Api/Services/SettingsService.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moonlight.Api.Configuration;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Services;
|
||||||
|
|
||||||
|
public class SettingsService
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<SettingsOption> Repository;
|
||||||
|
private readonly IOptions<SettingsOptions> Options;
|
||||||
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
|
private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
|
||||||
|
|
||||||
|
public SettingsService(
|
||||||
|
DatabaseRepository<SettingsOption> repository,
|
||||||
|
IOptions<SettingsOptions> options,
|
||||||
|
HybridCache hybridCache
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Repository = repository;
|
||||||
|
HybridCache = hybridCache;
|
||||||
|
Options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T?> GetValueAsync<T>(string key)
|
||||||
|
{
|
||||||
|
var cacheKey = string.Format(CacheKey, key);
|
||||||
|
|
||||||
|
var value = await HybridCache.GetOrCreateAsync<string?>(
|
||||||
|
cacheKey,
|
||||||
|
async ct =>
|
||||||
|
{
|
||||||
|
return await Repository
|
||||||
|
.Query()
|
||||||
|
.Where(x => x.Key == key)
|
||||||
|
.Select(o => o.ValueJson)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||||
|
},
|
||||||
|
new HybridCacheEntryOptions()
|
||||||
|
{
|
||||||
|
LocalCacheExpiration = Options.Value.LookupL1CacheTime,
|
||||||
|
Expiration = Options.Value.LookupL2CacheTime
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetValueAsync<T>(string key, T value)
|
||||||
|
{
|
||||||
|
var cacheKey = string.Format(CacheKey, key);
|
||||||
|
|
||||||
|
var option = await Repository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Key == key);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value);
|
||||||
|
|
||||||
|
if (option != null)
|
||||||
|
{
|
||||||
|
option.ValueJson = json;
|
||||||
|
await Repository.UpdateAsync(option);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
option = new SettingsOption()
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
ValueJson = json
|
||||||
|
};
|
||||||
|
|
||||||
|
await Repository.AddAsync(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
await HybridCache.RemoveAsync(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
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.Configuration;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Interfaces;
|
||||||
using Moonlight.Shared;
|
using Moonlight.Shared;
|
||||||
|
|
||||||
namespace Moonlight.Api.Services;
|
namespace Moonlight.Api.Services;
|
||||||
@@ -13,9 +14,10 @@ namespace Moonlight.Api.Services;
|
|||||||
public class UserAuthService
|
public class UserAuthService
|
||||||
{
|
{
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
private readonly DatabaseRepository<User> UserRepository;
|
||||||
private readonly IMemoryCache Cache;
|
|
||||||
private readonly ILogger<UserAuthService> Logger;
|
private readonly ILogger<UserAuthService> Logger;
|
||||||
private readonly IOptions<SessionOptions> Options;
|
private readonly IOptions<UserOptions> Options;
|
||||||
|
private readonly IEnumerable<IUserAuthHook> Hooks;
|
||||||
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
private const string UserIdClaim = "UserId";
|
private const string UserIdClaim = "UserId";
|
||||||
private const string IssuedAtClaim = "IssuedAt";
|
private const string IssuedAtClaim = "IssuedAt";
|
||||||
@@ -25,12 +27,16 @@ public class UserAuthService
|
|||||||
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,
|
||||||
|
HybridCache hybridCache
|
||||||
|
)
|
||||||
{
|
{
|
||||||
UserRepository = userRepository;
|
UserRepository = userRepository;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
Cache = cache;
|
|
||||||
Options = options;
|
Options = options;
|
||||||
|
Hooks = hooks;
|
||||||
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||||
@@ -50,7 +56,6 @@ public class UserAuthService
|
|||||||
// We use email as the primary identifier here
|
// We use email as the primary identifier here
|
||||||
var user = await UserRepository
|
var user = await UserRepository
|
||||||
.Query()
|
.Query()
|
||||||
.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(user => user.Email == email);
|
.FirstOrDefaultAsync(user => user.Email == email);
|
||||||
|
|
||||||
if (user == null) // Sync user if not already existing in the database
|
if (user == null) // Sync user if not already existing in the database
|
||||||
@@ -74,6 +79,13 @@ public class UserAuthService
|
|||||||
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
|
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
foreach (var hook in Hooks)
|
||||||
|
{
|
||||||
|
// Run every hook, and if any returns false, we return false as well
|
||||||
|
if (!await hook.SyncAsync(principal, user))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +102,11 @@ 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,
|
||||||
|
async ct =>
|
||||||
{
|
{
|
||||||
user = await UserRepository
|
return await UserRepository
|
||||||
.Query()
|
.Query()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
@@ -100,22 +114,17 @@ public class UserAuthService
|
|||||||
u.InvalidateTimestamp,
|
u.InvalidateTimestamp,
|
||||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||||
)
|
)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||||
|
},
|
||||||
if (user == null)
|
new HybridCacheEntryOptions()
|
||||||
return false;
|
|
||||||
|
|
||||||
Cache.Set(
|
|
||||||
cacheKey,
|
|
||||||
user,
|
|
||||||
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
|
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
|
||||||
|
Expiration = Options.Value.ValidationCacheL2Expiry
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
||||||
|
|
||||||
@@ -131,10 +140,18 @@ public class UserAuthService
|
|||||||
if (issuedAt < user.InvalidateTimestamp)
|
if (issuedAt < user.InvalidateTimestamp)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Load every permission as claim
|
||||||
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)
|
||||||
|
{
|
||||||
|
// Run every hook, and if any returns false we return false as well
|
||||||
|
if (!await hook.ValidateAsync(principal, userId))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
Moonlight.Api/Services/UserDeletionService.cs
Normal file
66
Moonlight.Api/Services/UserDeletionService.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Interfaces;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Services;
|
||||||
|
|
||||||
|
public class UserDeletionService
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<User> Repository;
|
||||||
|
private readonly IEnumerable<IUserDeletionHook> Hooks;
|
||||||
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
|
public UserDeletionService(
|
||||||
|
DatabaseRepository<User> repository,
|
||||||
|
IEnumerable<IUserDeletionHook> hooks,
|
||||||
|
HybridCache hybridCache
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Repository = repository;
|
||||||
|
Hooks = hooks;
|
||||||
|
HybridCache = hybridCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
|
||||||
|
{
|
||||||
|
var user = await Repository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == userId);
|
||||||
|
|
||||||
|
if(user == null)
|
||||||
|
throw new AggregateException($"User with id {userId} not found");
|
||||||
|
|
||||||
|
var errorMessages = new List<string>();
|
||||||
|
|
||||||
|
foreach (var hook in Hooks)
|
||||||
|
{
|
||||||
|
if (await hook.ValidateAsync(user, errorMessages))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return new UserDeletionValidationResult(false, errorMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserDeletionValidationResult(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int userId)
|
||||||
|
{
|
||||||
|
var user = await Repository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == userId);
|
||||||
|
|
||||||
|
if(user == null)
|
||||||
|
throw new AggregateException($"User with id {userId} not found");
|
||||||
|
|
||||||
|
foreach (var hook in Hooks)
|
||||||
|
await hook.ExecuteAsync(user);
|
||||||
|
|
||||||
|
await Repository.RemoveAsync(user);
|
||||||
|
|
||||||
|
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UserDeletionValidationResult(bool IsValid, IEnumerable<string> ErrorMessages);
|
||||||
43
Moonlight.Api/Services/UserLogoutService.cs
Normal file
43
Moonlight.Api/Services/UserLogoutService.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Interfaces;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Services;
|
||||||
|
|
||||||
|
public class UserLogoutService
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<User> Repository;
|
||||||
|
private readonly IEnumerable<IUserLogoutHook> Hooks;
|
||||||
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
|
public UserLogoutService(
|
||||||
|
DatabaseRepository<User> repository,
|
||||||
|
IEnumerable<IUserLogoutHook> hooks,
|
||||||
|
HybridCache hybridCache
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Repository = repository;
|
||||||
|
Hooks = hooks;
|
||||||
|
HybridCache = hybridCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LogoutAsync(int userId)
|
||||||
|
{
|
||||||
|
var user = await Repository
|
||||||
|
.Query()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == userId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
throw new AggregateException($"User with id {userId} not found");
|
||||||
|
|
||||||
|
foreach (var hook in Hooks)
|
||||||
|
await hook.ExecuteAsync(user);
|
||||||
|
|
||||||
|
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
||||||
|
await Repository.UpdateAsync(user);
|
||||||
|
|
||||||
|
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
|
||||||
|
|
||||||
public interface IAppStartup
|
|
||||||
{
|
|
||||||
public void PreBuild(WebApplicationBuilder builder);
|
|
||||||
public void PostBuild(WebApplication application);
|
|
||||||
public void PostMiddleware(WebApplication application);
|
|
||||||
}
|
|
||||||
@@ -17,19 +17,25 @@ public partial class Startup
|
|||||||
{
|
{
|
||||||
private static void AddAuth(WebApplicationBuilder builder)
|
private static void AddAuth(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
|
// OIDC
|
||||||
var oidcOptions = new OidcOptions();
|
var oidcOptions = new OidcOptions();
|
||||||
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
|
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
|
||||||
|
|
||||||
|
// API Key
|
||||||
var apiKeyOptions = new ApiOptions();
|
var apiKeyOptions = new ApiOptions();
|
||||||
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
|
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
|
||||||
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
|
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
|
||||||
|
|
||||||
builder.Services.AddScoped<UserAuthService>();
|
// Session
|
||||||
|
builder.Services.AddOptions<UserOptions>().BindConfiguration("Moonlight:User");
|
||||||
|
|
||||||
|
// Authentication
|
||||||
builder.Services.AddAuthentication("Main")
|
builder.Services.AddAuthentication("Main")
|
||||||
.AddPolicyScheme("Main", null, options =>
|
.AddPolicyScheme("Main", null,
|
||||||
|
options =>
|
||||||
{
|
{
|
||||||
options.ForwardDefaultSelector += context => context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
|
options.ForwardDefaultSelector += context =>
|
||||||
|
context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
|
||||||
})
|
})
|
||||||
.AddCookie("Session", null, options =>
|
.AddCookie("Session", null, options =>
|
||||||
{
|
{
|
||||||
@@ -74,6 +80,12 @@ public partial class Startup
|
|||||||
options.Authority = oidcOptions.Authority;
|
options.Authority = oidcOptions.Authority;
|
||||||
options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata;
|
options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata;
|
||||||
|
|
||||||
|
if (oidcOptions.DisableHttpsOnlyCookies)
|
||||||
|
{
|
||||||
|
options.NonceCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||||
|
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||||
|
}
|
||||||
|
|
||||||
var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
|
var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
|
||||||
|
|
||||||
options.Scope.Clear();
|
options.Scope.Clear();
|
||||||
@@ -91,15 +103,26 @@ public partial class Startup
|
|||||||
|
|
||||||
options.GetClaimsFromUserInfoEndpoint = true;
|
options.GetClaimsFromUserInfoEndpoint = true;
|
||||||
})
|
})
|
||||||
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null, options =>
|
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null,
|
||||||
|
options =>
|
||||||
{
|
{
|
||||||
options.LookupCacheTime = TimeSpan.FromMinutes(apiKeyOptions.LookupCacheMinutes);
|
options.LookupL1CacheTime = apiKeyOptions.LookupCacheL1Expiry;
|
||||||
|
options.LookupL2CacheTime = apiKeyOptions.LookupCacheL2Expiry;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
// Reduce log noise
|
||||||
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
|
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
|
||||||
|
|
||||||
|
// Custom permission handling using named policies
|
||||||
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||||
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<UserDeletionService>();
|
||||||
|
builder.Services.AddScoped<UserLogoutService>();
|
||||||
|
builder.Services.AddScoped<UserAuthService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseAuth(WebApplication application)
|
private static void UseAuth(WebApplication application)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Console;
|
using Microsoft.Extensions.Logging.Console;
|
||||||
@@ -9,40 +11,73 @@ using Moonlight.Api.Helpers;
|
|||||||
using Moonlight.Api.Implementations;
|
using Moonlight.Api.Implementations;
|
||||||
using Moonlight.Api.Interfaces;
|
using Moonlight.Api.Interfaces;
|
||||||
using Moonlight.Api.Services;
|
using Moonlight.Api.Services;
|
||||||
using SessionOptions = Moonlight.Api.Configuration.SessionOptions;
|
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
namespace Moonlight.Api.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public partial class Startup
|
||||||
{
|
{
|
||||||
private static void AddBase(WebApplicationBuilder builder)
|
private void AddBase(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
// Create the base directory
|
||||||
|
Directory.CreateDirectory("storage");
|
||||||
|
|
||||||
|
// Hook up source-generated serialization and add controllers
|
||||||
|
builder.Services
|
||||||
|
.AddControllers()
|
||||||
|
.AddApplicationPart(typeof(Startup).Assembly)
|
||||||
|
.AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Configure logging
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
||||||
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
||||||
|
|
||||||
|
// Application service
|
||||||
builder.Services.AddSingleton<ApplicationService>();
|
builder.Services.AddSingleton<ApplicationService>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
||||||
|
|
||||||
|
// Diagnose
|
||||||
builder.Services.AddSingleton<DiagnoseService>();
|
builder.Services.AddSingleton<DiagnoseService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
||||||
|
|
||||||
builder.Services.AddMemoryCache();
|
// Frontend
|
||||||
builder.Services.AddOptions<SessionOptions>().BindConfiguration("Moonlight:Session");
|
|
||||||
|
|
||||||
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
||||||
builder.Services.AddScoped<FrontendService>();
|
builder.Services.AddScoped<FrontendService>();
|
||||||
|
|
||||||
|
// HTTP Client
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// Version fetching configuration
|
||||||
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
|
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
|
||||||
builder.Services.AddSingleton<VersionService>();
|
builder.Services.AddSingleton<VersionService>();
|
||||||
|
|
||||||
|
// Container Helper Options
|
||||||
|
builder.Configuration.GetSection("Moonlight:ContainerHelper").Bind(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
|
||||||
|
builder.Services.AddSingleton<ContainerHelperService>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
|
||||||
|
{
|
||||||
|
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
|
||||||
|
client.BaseAddress =
|
||||||
|
new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
|
||||||
|
});
|
||||||
|
|
||||||
|
// User management services
|
||||||
|
builder.Services.AddScoped<UserDeletionService>();
|
||||||
|
builder.Services.AddScoped<UserLogoutService>();
|
||||||
|
|
||||||
|
// Settings options
|
||||||
|
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
|
||||||
|
builder.Services.AddScoped<SettingsService>();
|
||||||
|
|
||||||
|
// Setup key loading
|
||||||
|
var keysDirectory = new DirectoryInfo(Path.Combine("storage", "keys"));
|
||||||
|
builder.Services.AddDataProtection().PersistKeysToFileSystem(keysDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseBase(WebApplication application)
|
private static void UseBase(WebApplication application)
|
||||||
|
|||||||
34
Moonlight.Api/Startup/Startup.Cache.cs
Normal file
34
Moonlight.Api/Startup/Startup.Cache.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Startup;
|
||||||
|
|
||||||
|
public partial class Startup
|
||||||
|
{
|
||||||
|
private static void AddCache(WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
// Load cache options
|
||||||
|
var cacheOptions = new CacheOptions();
|
||||||
|
builder.Configuration.GetSection("Moonlight:Cache").Bind(cacheOptions);
|
||||||
|
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddHybridCache();
|
||||||
|
|
||||||
|
if (!cacheOptions.EnableLayer2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var redisOptions = new RedisOptions();
|
||||||
|
builder.Configuration.GetSection("Moonlight:Redis").Bind(redisOptions);
|
||||||
|
|
||||||
|
if(!redisOptions.Enable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
builder.Services.AddStackExchangeRedisCache(options =>
|
||||||
|
{
|
||||||
|
options.Configuration = redisOptions.ConnectionString;
|
||||||
|
options.InstanceName = "Moonlight:";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,29 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using System.Reflection;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Moonlight.Shared.Http;
|
||||||
|
using SimplePlugin.Abstractions;
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
namespace Moonlight.Api.Startup;
|
||||||
|
|
||||||
public partial class Startup : IAppStartup
|
[PluginModule]
|
||||||
|
public partial class Startup : MoonlightPlugin
|
||||||
{
|
{
|
||||||
public void PreBuild(WebApplicationBuilder builder)
|
public override void PreBuild(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
AddBase(builder);
|
AddBase(builder);
|
||||||
AddAuth(builder);
|
AddAuth(builder);
|
||||||
AddDatabase(builder);
|
AddDatabase(builder);
|
||||||
|
AddCache(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PostBuild(WebApplication application)
|
public override void PostBuild(WebApplication application)
|
||||||
{
|
{
|
||||||
UseBase(application);
|
UseBase(application);
|
||||||
UseAuth(application);
|
UseAuth(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PostMiddleware(WebApplication application)
|
public override void PostMiddleware(WebApplication application)
|
||||||
{
|
{
|
||||||
MapBase(application);
|
MapBase(application);
|
||||||
}
|
}
|
||||||
|
|||||||
42
Moonlight.Api/StartupHandler.cs
Normal file
42
Moonlight.Api/StartupHandler.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Moonlight.Api;
|
||||||
|
|
||||||
|
public static class StartupHandler
|
||||||
|
{
|
||||||
|
public static async Task RunAsync(string[] args, MoonlightPlugin[] plugins)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Starting with: {string.Join(", ", plugins.Select(x => x.GetType().FullName))}");
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Setting up context
|
||||||
|
foreach (var plugin in plugins)
|
||||||
|
plugin.Initialize(plugins);
|
||||||
|
|
||||||
|
// Stage 1: Pre Build
|
||||||
|
foreach (var startup in plugins)
|
||||||
|
startup.PreBuild(builder);
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Stage 2: Post Build
|
||||||
|
foreach (var startup in plugins)
|
||||||
|
startup.PostBuild(app);
|
||||||
|
|
||||||
|
// Stage 3: Post Middleware
|
||||||
|
foreach (var startup in plugins)
|
||||||
|
startup.PostMiddleware(app);
|
||||||
|
|
||||||
|
// Frontend debugging
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
app.UseWebAssemblyDebugging();
|
||||||
|
|
||||||
|
// Frontend hosting
|
||||||
|
app.UseBlazorFrameworkFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Moonlight.Frontend/Configuration/LayoutMiddlewareOptions.cs
Normal file
26
Moonlight.Frontend/Configuration/LayoutMiddlewareOptions.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Moonlight.Frontend.Interfaces;
|
||||||
|
|
||||||
|
namespace Moonlight.Frontend.Configuration;
|
||||||
|
|
||||||
|
public class LayoutMiddlewareOptions
|
||||||
|
{
|
||||||
|
public IReadOnlyList<Type> Components => InnerComponents;
|
||||||
|
|
||||||
|
private readonly List<Type> InnerComponents = new();
|
||||||
|
|
||||||
|
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : LayoutMiddlewareBase
|
||||||
|
{
|
||||||
|
InnerComponents.Add(typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(int index) where T : LayoutMiddlewareBase
|
||||||
|
{
|
||||||
|
InnerComponents.Insert(index, typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : LayoutMiddlewareBase
|
||||||
|
{
|
||||||
|
InnerComponents.Remove(typeof(T));
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Moonlight.Frontend/Configuration/LayoutPageOptions.cs
Normal file
33
Moonlight.Frontend/Configuration/LayoutPageOptions.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.Frontend.Configuration;
|
||||||
|
|
||||||
|
public class LayoutPageOptions
|
||||||
|
{
|
||||||
|
public IReadOnlyList<LayoutPageComponent> Components => InnerComponents;
|
||||||
|
|
||||||
|
private readonly List<LayoutPageComponent> InnerComponents = new();
|
||||||
|
|
||||||
|
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(LayoutPageSlot slot, int order)
|
||||||
|
where T : ComponentBase
|
||||||
|
=> Add(typeof(T), slot, order);
|
||||||
|
|
||||||
|
public void Add(Type componentType, LayoutPageSlot slot, int order)
|
||||||
|
=> InnerComponents.Add(new LayoutPageComponent(componentType, order, slot));
|
||||||
|
|
||||||
|
public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
|
||||||
|
where T : ComponentBase
|
||||||
|
=> Remove(typeof(T));
|
||||||
|
|
||||||
|
public void Remove(Type componentType)
|
||||||
|
=> InnerComponents.RemoveAll(x => x.ComponentType == componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record LayoutPageComponent(Type ComponentType, int Order, LayoutPageSlot Slot);
|
||||||
|
|
||||||
|
public enum LayoutPageSlot
|
||||||
|
{
|
||||||
|
Header = 0,
|
||||||
|
Footer = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Moonlight.Frontend.Configuration;
|
||||||
|
|
||||||
|
public class NavigationAssemblyOptions
|
||||||
|
{
|
||||||
|
public List<Assembly> Assemblies { get; private set; } = new();
|
||||||
|
}
|
||||||
35
Moonlight.Frontend/Configuration/SystemSettingsOptions.cs
Normal file
35
Moonlight.Frontend/Configuration/SystemSettingsOptions.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.Frontend.Configuration;
|
||||||
|
|
||||||
|
public class SystemSettingsOptions
|
||||||
|
{
|
||||||
|
public IReadOnlyList<SystemSettingsPage> Components => InnerComponents;
|
||||||
|
|
||||||
|
private readonly List<SystemSettingsPage> InnerComponents = new();
|
||||||
|
|
||||||
|
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TIcon,
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>(string name, string description,
|
||||||
|
int order)
|
||||||
|
where TIcon : ComponentBase where TComponent : ComponentBase
|
||||||
|
=> Add(name, description, order, typeof(TIcon), typeof(TComponent));
|
||||||
|
|
||||||
|
public void Add(string name, string description, int order, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type iconComponent, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type component)
|
||||||
|
=> InnerComponents.Add(new SystemSettingsPage(name, description, order, iconComponent, component));
|
||||||
|
|
||||||
|
public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>()
|
||||||
|
where TComponent : ComponentBase
|
||||||
|
=> Remove(typeof(TComponent));
|
||||||
|
|
||||||
|
public void Remove([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType)
|
||||||
|
=> InnerComponents.RemoveAll(x => x.ComponentType == componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SystemSettingsPage(
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
int Order,
|
||||||
|
Type IconComponentType,
|
||||||
|
Type ComponentType
|
||||||
|
);
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Moonlight.Shared.Http;
|
|
||||||
|
|
||||||
namespace Moonlight.Frontend;
|
|
||||||
|
|
||||||
public static class Constants
|
|
||||||
{
|
|
||||||
public static JsonSerializerOptions SerializerOptions
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (InternalOptions != null)
|
|
||||||
return InternalOptions;
|
|
||||||
|
|
||||||
InternalOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add source generated options from shared project
|
|
||||||
InternalOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
|
||||||
|
|
||||||
return InternalOptions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JsonSerializerOptions? InternalOptions;
|
|
||||||
}
|
|
||||||
30
Moonlight.Frontend/Helpers/ProblemDetailsHelper.cs
Normal file
30
Moonlight.Frontend/Helpers/ProblemDetailsHelper.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
using Moonlight.Shared.Http.Responses;
|
||||||
|
|
||||||
|
namespace Moonlight.Frontend.Helpers;
|
||||||
|
|
||||||
|
public static class ProblemDetailsHelper
|
||||||
|
{
|
||||||
|
public static async Task HandleProblemDetailsAsync(HttpResponseMessage response, object model, ValidationMessageStore validationMessageStore)
|
||||||
|
{
|
||||||
|
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||||
|
|
||||||
|
if (problemDetails == null)
|
||||||
|
response.EnsureSuccessStatusCode(); // Trigger exception when unable to parse
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(!string.IsNullOrEmpty(problemDetails.Detail))
|
||||||
|
validationMessageStore.Add(new FieldIdentifier(model, string.Empty), problemDetails.Detail);
|
||||||
|
|
||||||
|
if (problemDetails.Errors != null)
|
||||||
|
{
|
||||||
|
foreach (var error in problemDetails.Errors)
|
||||||
|
{
|
||||||
|
foreach (var message in error.Value)
|
||||||
|
validationMessageStore.Add(new FieldIdentifier(model, error.Key), message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ public sealed class PermissionProvider : IPermissionProvider
|
|||||||
new Permission(Permissions.System.Info, "Info", "View system info"),
|
new Permission(Permissions.System.Info, "Info", "View system info"),
|
||||||
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
|
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
|
||||||
new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"),
|
new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"),
|
||||||
|
new Permission(Permissions.System.Instance, "Instance", "Update the moonlight instance and add plugins"),
|
||||||
|
new Permission(Permissions.System.Settings, "Settings", "Change settings of the instance"),
|
||||||
]),
|
]),
|
||||||
new PermissionCategory("API Keys", typeof(KeyIcon), [
|
new PermissionCategory("API Keys", typeof(KeyIcon), [
|
||||||
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),
|
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),
|
||||||
|
|||||||
8
Moonlight.Frontend/Interfaces/LayoutMiddlewareBase.cs
Normal file
8
Moonlight.Frontend/Interfaces/LayoutMiddlewareBase.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.Frontend.Interfaces;
|
||||||
|
|
||||||
|
public abstract class LayoutMiddlewareBase : ComponentBase
|
||||||
|
{
|
||||||
|
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
namespace Moonlight.Frontend.Mappers;
|
namespace Moonlight.Frontend.Mappers;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Moonlight.Shared.Http.Requests.Roles;
|
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||||
using Moonlight.Shared.Http.Responses.Admin;
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Moonlight.Shared.Http.Requests.Themes;
|
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||||
using Moonlight.Shared.Http.Responses.Themes;
|
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
namespace Moonlight.Frontend.Mappers;
|
namespace Moonlight.Frontend.Mappers;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
using Moonlight.Shared.Http.Requests.Users;
|
|
||||||
using Moonlight.Shared.Http.Responses.Users;
|
|
||||||
|
|
||||||
namespace Moonlight.Frontend.Mappers;
|
namespace Moonlight.Frontend.Mappers;
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,9 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||||
<PackageReference Include="ShadcnBlazor" Version="1.0.9" />
|
<PackageReference Include="ShadcnBlazor" Version="1.0.13" />
|
||||||
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.9" />
|
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.13" />
|
||||||
|
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -35,5 +36,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Styles/*" Pack="true" PackagePath="Styles/" />
|
<None Include="Styles/*" Pack="true" PackagePath="Styles/" />
|
||||||
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\Moonlight.Frontend.targets" />
|
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\Moonlight.Frontend.targets" />
|
||||||
|
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="buildTransitive\Moonlight.Frontend.targets" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
</MoonlightCssClassDir>
|
</MoonlightCssClassDir>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="CopyContents" BeforeTargets="Build">
|
<Target Name="Moonlight_CopyContents" BeforeTargets="Build">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
|
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
17
Moonlight.Frontend/MoonlightPlugin.cs
Normal file
17
Moonlight.Frontend/MoonlightPlugin.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using SimplePlugin.Abstractions;
|
||||||
|
|
||||||
|
namespace Moonlight.Frontend;
|
||||||
|
|
||||||
|
public abstract class MoonlightPlugin : IPluginModule
|
||||||
|
{
|
||||||
|
protected MoonlightPlugin[] Plugins { get; private set; }
|
||||||
|
|
||||||
|
public void Initialize(MoonlightPlugin[] plugins)
|
||||||
|
{
|
||||||
|
Plugins = plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void PreBuild(WebAssemblyHostBuilder builder){}
|
||||||
|
public virtual void PostBuild(WebAssemblyHost application){}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ using System.Net.Http.Json;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
using Microsoft.VisualBasic;
|
||||||
|
using Moonlight.Shared.Http;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||||
|
|
||||||
namespace Moonlight.Frontend.Services;
|
namespace Moonlight.Frontend.Services;
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ public class RemoteAuthProvider : AuthenticationStateProvider
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var claimResponses = await HttpClient.GetFromJsonAsync<ClaimDto[]>(
|
var claimResponses = await HttpClient.GetFromJsonAsync<ClaimDto[]>(
|
||||||
"api/auth/claims", Constants.SerializerOptions
|
"api/auth/claims", SerializationContext.Default.Options
|
||||||
);
|
);
|
||||||
|
|
||||||
var claims = claimResponses!.Select(claim => new Claim(claim.Type, claim.Value));
|
var claims = claimResponses!.Select(claim => new Claim(claim.Type, claim.Value));
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
|
||||||
|
|
||||||
namespace Moonlight.Frontend.Startup;
|
|
||||||
|
|
||||||
public interface IAppStartup
|
|
||||||
{
|
|
||||||
public void PreBuild(WebAssemblyHostBuilder builder);
|
|
||||||
public void PostBuild(WebAssemblyHost application);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
|
using LucideBlazor;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moonlight.Frontend.Configuration;
|
||||||
using Moonlight.Frontend.Implementations;
|
using Moonlight.Frontend.Implementations;
|
||||||
using Moonlight.Frontend.Interfaces;
|
using Moonlight.Frontend.Interfaces;
|
||||||
using Moonlight.Frontend.Services;
|
using Moonlight.Frontend.Services;
|
||||||
using Moonlight.Frontend.UI;
|
using Moonlight.Frontend.UI;
|
||||||
|
using Moonlight.Frontend.UI.Admin.Settings;
|
||||||
using ShadcnBlazor;
|
using ShadcnBlazor;
|
||||||
using ShadcnBlazor.Extras;
|
using ShadcnBlazor.Extras;
|
||||||
|
|
||||||
@@ -25,5 +28,19 @@ public partial class Startup
|
|||||||
builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>();
|
builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>();
|
||||||
|
|
||||||
builder.Services.AddScoped<FrontendService>();
|
builder.Services.AddScoped<FrontendService>();
|
||||||
|
|
||||||
|
builder.Services.Configure<NavigationAssemblyOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Assemblies.Add(typeof(Startup).Assembly);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.Configure<SystemSettingsOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Add<TextCursorInputIcon, WhiteLabelingSetting>(
|
||||||
|
"White Labeling",
|
||||||
|
"Settings for white labeling your moonlight instance",
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using SimplePlugin.Abstractions;
|
||||||
|
|
||||||
namespace Moonlight.Frontend.Startup;
|
namespace Moonlight.Frontend.Startup;
|
||||||
|
|
||||||
public partial class Startup : IAppStartup
|
[PluginModule]
|
||||||
|
public partial class Startup : MoonlightPlugin
|
||||||
{
|
{
|
||||||
public void PreBuild(WebAssemblyHostBuilder builder)
|
public override void PreBuild(WebAssemblyHostBuilder builder)
|
||||||
{
|
{
|
||||||
AddBase(builder);
|
AddBase(builder);
|
||||||
AddAuth(builder);
|
AddAuth(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PostBuild(WebAssemblyHost application)
|
public override void PostBuild(WebAssemblyHost application)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
29
Moonlight.Frontend/StartupHandler.cs
Normal file
29
Moonlight.Frontend/StartupHandler.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
|
||||||
|
namespace Moonlight.Frontend;
|
||||||
|
|
||||||
|
public static class StartupHandler
|
||||||
|
{
|
||||||
|
public static async Task RunAsync(string[] args, MoonlightPlugin[] plugins)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Starting with: {string.Join(", ", plugins.Select(x => x.GetType().FullName))}");
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
|
// Setting up context
|
||||||
|
foreach (var plugin in plugins)
|
||||||
|
plugin.Initialize(plugins);
|
||||||
|
|
||||||
|
// Stage 1: Pre Build
|
||||||
|
foreach (var plugin in plugins)
|
||||||
|
plugin.PreBuild(builder);
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Stage 2: Post Build
|
||||||
|
foreach(var plugin in plugins)
|
||||||
|
plugin.PostBuild(app);
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
@using Moonlight.Frontend.UI.Admin.Components
|
@using Moonlight.Frontend.Helpers
|
||||||
@using Moonlight.Shared.Http.Requests.ApiKeys
|
@using Moonlight.Frontend.UI.Admin.Components
|
||||||
|
@using Moonlight.Shared.Http
|
||||||
|
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
|
||||||
|
@using Moonlight.Shared.Http.Responses
|
||||||
@using ShadcnBlazor.Dialogs
|
@using ShadcnBlazor.Dialogs
|
||||||
@using ShadcnBlazor.Extras.Common
|
@using ShadcnBlazor.Extras.Forms
|
||||||
@using ShadcnBlazor.Extras.FormHandlers
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
@using ShadcnBlazor.Inputs
|
@using ShadcnBlazor.Inputs
|
||||||
@using ShadcnBlazor.Labels
|
|
||||||
|
|
||||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create new API key</DialogTitle>
|
<DialogTitle>Create new API key</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -15,56 +21,80 @@
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||||
<div class="flex flex-col gap-6">
|
|
||||||
|
<FieldGroup>
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
<FormValidationSummary/>
|
<FormValidationSummary/>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<FieldSet>
|
||||||
<Label for="keyName">Name</Label>
|
<Field>
|
||||||
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
|
<FieldLabel for="keyName">Name</FieldLabel>
|
||||||
</div>
|
<TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/>
|
||||||
|
</Field>
|
||||||
<div class="grid gap-2">
|
<Field>
|
||||||
<Label for="keyDescription">Description</Label>
|
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||||
<textarea
|
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/>
|
||||||
@bind="Request.Description"
|
</Field>
|
||||||
id="keyDescription"
|
<Field>
|
||||||
maxlength="100"
|
<FieldLabel for="keyValidUntil">Valid until</FieldLabel>
|
||||||
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
<DateTimeInputField @bind-Value="Request.ValidUntil" id="keyValidUntil" />
|
||||||
placeholder="What this key is for">
|
</Field>
|
||||||
|
<Field>
|
||||||
</textarea>
|
<FieldLabel>Permissions</FieldLabel>
|
||||||
</div>
|
<FieldContent>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label>Permissions</Label>
|
|
||||||
<PermissionSelector Permissions="Permissions"/>
|
<PermissionSelector Permissions="Permissions"/>
|
||||||
</div>
|
</FieldContent>
|
||||||
</div>
|
</Field>
|
||||||
</FormHandler>
|
</FieldSet>
|
||||||
|
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||||
<DialogFooter ClassName="justify-end gap-x-1">
|
<SubmitButton>Save changes</SubmitButton>
|
||||||
<WButtom OnClick="() => FormHandler.SubmitAsync()">Save changes</WButtom>
|
</Field>
|
||||||
</DialogFooter>
|
</FieldGroup>
|
||||||
|
</EnhancedEditForm>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public Func<CreateApiKeyDto, Task> OnSubmit { get; set; }
|
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||||
|
|
||||||
private CreateApiKeyDto Request;
|
private CreateApiKeyDto Request;
|
||||||
private FormHandler FormHandler;
|
|
||||||
|
|
||||||
private List<string> Permissions = new();
|
private List<string> Permissions = new();
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
Request = new();
|
Request = new()
|
||||||
|
{
|
||||||
|
Permissions = [],
|
||||||
|
ValidUntil = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SubmitAsync()
|
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||||
{
|
{
|
||||||
Request.Permissions = Permissions.ToArray();
|
Request.Permissions = Permissions.ToArray();
|
||||||
await OnSubmit.Invoke(Request);
|
Request.ValidUntil = Request.ValidUntil.ToUniversalTime();
|
||||||
|
|
||||||
|
var response = await HttpClient.PostAsJsonAsync(
|
||||||
|
"/api/admin/apiKeys",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"API Key creation",
|
||||||
|
$"Successfully created API key {Request.Name}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await OnSubmit.Invoke();
|
||||||
|
|
||||||
await CloseAsync();
|
await CloseAsync();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
|
@using Moonlight.Frontend.Helpers
|
||||||
@using Moonlight.Frontend.UI.Admin.Components
|
@using Moonlight.Frontend.UI.Admin.Components
|
||||||
@using Moonlight.Shared.Http.Requests.Roles
|
@using Moonlight.Shared.Http
|
||||||
@using ShadcnBlazor.Buttons
|
@using Moonlight.Shared.Http.Requests.Admin.Roles
|
||||||
@using ShadcnBlazor.Dialogs
|
@using ShadcnBlazor.Dialogs
|
||||||
@using ShadcnBlazor.Extras.Common
|
@using ShadcnBlazor.Extras.Forms
|
||||||
@using ShadcnBlazor.Extras.FormHandlers
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
@using ShadcnBlazor.Inputs
|
@using ShadcnBlazor.Inputs
|
||||||
@using ShadcnBlazor.Labels
|
|
||||||
|
|
||||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Create new role
|
Create new role
|
||||||
@@ -18,49 +22,43 @@
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||||
<div class="flex flex-col gap-6">
|
<FieldGroup>
|
||||||
|
|
||||||
<FormValidationSummary/>
|
<FormValidationSummary/>
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<FieldSet>
|
||||||
<Label for="roleName">Name</Label>
|
<Field>
|
||||||
<InputField
|
<FieldLabel for="roleName">Name</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
@bind-Value="Request.Name"
|
@bind-Value="Request.Name"
|
||||||
id="roleName"
|
id="roleName"
|
||||||
placeholder="My fancy role"/>
|
placeholder="My fancy role"/>
|
||||||
</div>
|
</Field>
|
||||||
|
<Field>
|
||||||
<div class="grid gap-2">
|
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||||
<Label for="roleDescription">Description</Label>
|
<TextareaInputField @bind-Value="Request.Description" id="keyDescription"
|
||||||
<textarea
|
placeholder="Describe what the role should be used for"/>
|
||||||
@bind="Request.Description"
|
</Field>
|
||||||
id="roleDescription"
|
<Field>
|
||||||
maxlength="100"
|
<FieldLabel>Permissions</FieldLabel>
|
||||||
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
<FieldContent>
|
||||||
placeholder="Describe what the role should be used for">
|
|
||||||
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label>Permissions</Label>
|
|
||||||
<PermissionSelector Permissions="Permissions"/>
|
<PermissionSelector Permissions="Permissions"/>
|
||||||
</div>
|
</FieldContent>
|
||||||
</div>
|
</Field>
|
||||||
</FormHandler>
|
</FieldSet>
|
||||||
|
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||||
<DialogFooter ClassName="justify-end gap-x-1">
|
<SubmitButton>Save changes</SubmitButton>
|
||||||
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
|
</Field>
|
||||||
</DialogFooter>
|
</FieldGroup>
|
||||||
|
</EnhancedEditForm>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public Func<CreateRoleDto, Task> OnSubmit { get; set; }
|
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||||
|
|
||||||
private CreateRoleDto Request;
|
private CreateRoleDto Request;
|
||||||
private List<string> Permissions;
|
private List<string> Permissions;
|
||||||
private FormHandler FormHandler;
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
@@ -72,15 +70,27 @@
|
|||||||
Permissions = new();
|
Permissions = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SubmitAsync()
|
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||||
{
|
{
|
||||||
Request.Permissions = Permissions.ToArray();
|
Request.Permissions = Permissions.ToArray();
|
||||||
await FormHandler.SubmitAsync();
|
|
||||||
|
var response = await HttpClient.PostAsJsonAsync(
|
||||||
|
"api/admin/roles",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnSubmitAsync()
|
await ToastService.SuccessAsync("Role creation", $"Role {Request.Name} has been successfully created");
|
||||||
{
|
|
||||||
await OnSubmit.Invoke(Request);
|
await OnSubmit.Invoke();
|
||||||
await CloseAsync();
|
await CloseAsync();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
@using Moonlight.Shared.Http.Requests.Users
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using Moonlight.Shared.Http
|
||||||
|
@using Moonlight.Shared.Http.Requests.Admin.Users
|
||||||
|
@using Moonlight.Shared.Http.Responses
|
||||||
@using ShadcnBlazor.Dialogs
|
@using ShadcnBlazor.Dialogs
|
||||||
@using ShadcnBlazor.Extras.Common
|
@using ShadcnBlazor.Extras.Forms
|
||||||
@using ShadcnBlazor.Extras.FormHandlers
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
@using ShadcnBlazor.Inputs
|
@using ShadcnBlazor.Inputs
|
||||||
@using ShadcnBlazor.Labels
|
|
||||||
|
|
||||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Create new user
|
Create new user
|
||||||
@@ -16,50 +22,67 @@
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||||
<div class="flex flex-col gap-6">
|
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
<FormValidationSummary/>
|
<FormValidationSummary/>
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<FieldSet>
|
||||||
<Label for="username">Username</Label>
|
<Field>
|
||||||
<InputField
|
<FieldLabel for="username">Username</FieldLabel>
|
||||||
|
<TextInputField
|
||||||
@bind-Value="Request.Username"
|
@bind-Value="Request.Username"
|
||||||
id="username"
|
id="username"
|
||||||
placeholder="Name of the user"/>
|
placeholder="Name of the user"/>
|
||||||
</div>
|
</Field>
|
||||||
|
<Field>
|
||||||
<div class="grid gap-2">
|
<FieldLabel for="emailAddress">Email Address</FieldLabel>
|
||||||
<Label for="emailAddress">Email Address</Label>
|
<TextInputField
|
||||||
<InputField
|
|
||||||
@bind-Value="Request.Email"
|
@bind-Value="Request.Email"
|
||||||
id="emailAddress"
|
id="emailAddress"
|
||||||
Type="email"
|
|
||||||
placeholder="email@of.user"/>
|
placeholder="email@of.user"/>
|
||||||
</div>
|
</Field>
|
||||||
</div>
|
</FieldSet>
|
||||||
</FormHandler>
|
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||||
|
<SubmitButton>Save changes</SubmitButton>
|
||||||
<DialogFooter ClassName="justify-end gap-x-1">
|
</Field>
|
||||||
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
|
</FieldGroup>
|
||||||
</DialogFooter>
|
</EnhancedEditForm>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public Func<CreateUserDto, Task> OnSubmit { get; set; }
|
[Parameter] public Func<Task> OnCompleted { get; set; }
|
||||||
|
|
||||||
private CreateUserDto Request;
|
private CreateUserDto Request;
|
||||||
private FormHandler FormHandler;
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
Request = new();
|
Request = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SubmitAsync()
|
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||||
{
|
{
|
||||||
await OnSubmit.Invoke(Request);
|
var response = await HttpClient.PostAsJsonAsync(
|
||||||
|
"/api/admin/users",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"User creation",
|
||||||
|
$"Successfully created user {Request.Username}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await OnCompleted.Invoke();
|
||||||
await CloseAsync();
|
await CloseAsync();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@using LucideBlazor
|
@using LucideBlazor
|
||||||
@using Moonlight.Shared.Http.Responses
|
@using Moonlight.Shared.Http.Responses
|
||||||
@using Moonlight.Shared.Http.Responses.Admin
|
@using Moonlight.Shared.Http.Responses.Admin
|
||||||
@using Moonlight.Shared.Http.Responses.Users
|
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||||
@using ShadcnBlazor.Buttons
|
@using ShadcnBlazor.Buttons
|
||||||
@using ShadcnBlazor.DataGrids
|
@using ShadcnBlazor.DataGrids
|
||||||
@using ShadcnBlazor.Dialogs
|
@using ShadcnBlazor.Dialogs
|
||||||
@@ -32,9 +32,9 @@
|
|||||||
SearchPlaceholder="Search user"
|
SearchPlaceholder="Search user"
|
||||||
ValueSelector="dto => dto.Username"
|
ValueSelector="dto => dto.Username"
|
||||||
Source="LoadUsersAsync"/>
|
Source="LoadUsersAsync"/>
|
||||||
<WButtom OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
|
<WButton OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
|
||||||
<PlusIcon/>
|
<PlusIcon/>
|
||||||
</WButtom>
|
</WButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
@@ -50,9 +50,9 @@
|
|||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div class="flex justify-end me-1.5">
|
<div class="flex justify-end me-1.5">
|
||||||
<WButtom OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
|
<WButton OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
|
||||||
<TrashIcon/>
|
<TrashIcon/>
|
||||||
</WButtom>
|
</WButton>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
@using Moonlight.Frontend.Mappers
|
@using Moonlight.Frontend.Helpers
|
||||||
|
@using Moonlight.Frontend.Mappers
|
||||||
@using Moonlight.Frontend.UI.Admin.Components
|
@using Moonlight.Frontend.UI.Admin.Components
|
||||||
@using Moonlight.Shared.Http.Requests.ApiKeys
|
@using Moonlight.Shared.Http
|
||||||
@using Moonlight.Shared.Http.Responses.ApiKeys
|
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
|
||||||
|
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
||||||
@using ShadcnBlazor.Dialogs
|
@using ShadcnBlazor.Dialogs
|
||||||
@using ShadcnBlazor.Extras.Common
|
@using ShadcnBlazor.Extras.Forms
|
||||||
@using ShadcnBlazor.Extras.FormHandlers
|
@using ShadcnBlazor.Extras.Toasts
|
||||||
|
@using ShadcnBlazor.Fields
|
||||||
@using ShadcnBlazor.Inputs
|
@using ShadcnBlazor.Inputs
|
||||||
@using ShadcnBlazor.Labels
|
|
||||||
|
|
||||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||||
|
|
||||||
|
@inject HttpClient HttpClient
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update API key</DialogTitle>
|
<DialogTitle>Update API key</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -17,45 +22,43 @@
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||||
<div class="flex flex-col gap-6">
|
<FieldGroup>
|
||||||
<FormValidationSummary/>
|
<FormValidationSummary/>
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<FieldSet>
|
||||||
<Label for="keyName">Name</Label>
|
<Field>
|
||||||
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
|
<FieldLabel for="keyName">Name</FieldLabel>
|
||||||
</div>
|
<TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/>
|
||||||
|
</Field>
|
||||||
<div class="grid gap-2">
|
<Field>
|
||||||
<Label for="keyDescription">Description</Label>
|
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||||
<textarea
|
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/>
|
||||||
@bind="Request.Description"
|
</Field>
|
||||||
id="keyDescription"
|
<Field>
|
||||||
maxlength="100"
|
<FieldLabel for="keyValidUntil">Valid until</FieldLabel>
|
||||||
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
<DateTimeInputField @bind-Value="Request.ValidUntil" id="keyValidUntil" />
|
||||||
placeholder="What this key is for">
|
</Field>
|
||||||
|
<Field>
|
||||||
</textarea>
|
<FieldLabel>Permissions</FieldLabel>
|
||||||
</div>
|
<FieldContent>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label>Permissions</Label>
|
|
||||||
<PermissionSelector Permissions="Permissions"/>
|
<PermissionSelector Permissions="Permissions"/>
|
||||||
</div>
|
</FieldContent>
|
||||||
</div>
|
</Field>
|
||||||
</FormHandler>
|
</FieldSet>
|
||||||
|
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||||
<DialogFooter ClassName="justify-end gap-x-1">
|
<SubmitButton>Save changes</SubmitButton>
|
||||||
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
|
</Field>
|
||||||
</DialogFooter>
|
</FieldGroup>
|
||||||
|
</EnhancedEditForm>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public Func<UpdateApiKeyDto, Task> OnSubmit { get; set; }
|
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||||
[Parameter] public ApiKeyDto Key { get; set; }
|
[Parameter] public ApiKeyDto Key { get; set; }
|
||||||
|
|
||||||
private UpdateApiKeyDto Request;
|
private UpdateApiKeyDto Request;
|
||||||
private FormHandler FormHandler;
|
|
||||||
private List<string> Permissions = new();
|
private List<string> Permissions = new();
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
@@ -64,10 +67,31 @@
|
|||||||
Permissions = Key.Permissions.ToList();
|
Permissions = Key.Permissions.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SubmitAsync()
|
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||||
{
|
{
|
||||||
Request.Permissions = Permissions.ToArray();
|
Request.Permissions = Permissions.ToArray();
|
||||||
await OnSubmit.Invoke(Request);
|
Request.ValidUntil = Request.ValidUntil.ToUniversalTime();
|
||||||
|
|
||||||
|
var response = await HttpClient.PatchAsJsonAsync(
|
||||||
|
$"/api/admin/apiKeys/{Key.Id}",
|
||||||
|
Request,
|
||||||
|
SerializationContext.Default.Options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.SuccessAsync(
|
||||||
|
"API Key update",
|
||||||
|
$"Successfully updated API key {Request.Name}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await OnSubmit.Invoke();
|
||||||
await CloseAsync();
|
await CloseAsync();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,38 @@
|
|||||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||||
|
|
||||||
|
@using System.Text.Json
|
||||||
@using LucideBlazor
|
@using LucideBlazor
|
||||||
|
@using Moonlight.Shared.Http
|
||||||
|
@using Moonlight.Shared.Http.Events
|
||||||
|
@using Moonlight.Shared.Http.Requests.Admin.ContainerHelper
|
||||||
|
@using ShadcnBlazor.Buttons
|
||||||
@using ShadcnBlazor.Dialogs
|
@using ShadcnBlazor.Dialogs
|
||||||
@using ShadcnBlazor.Extras.AlertDialogs
|
|
||||||
@using ShadcnBlazor.Progresses
|
@using ShadcnBlazor.Progresses
|
||||||
@using ShadcnBlazor.Spinners
|
@using ShadcnBlazor.Spinners
|
||||||
|
|
||||||
@inject AlertDialogService AlertService
|
@inject HttpClient HttpClient
|
||||||
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Updating...
|
Updating instance to @Version...
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div class="text-base flex flex-col p-2 gap-y-0.5">
|
<div class="grid grid-cols-1 xl:grid-cols-2 w-full gap-5">
|
||||||
|
<div class="text-base flex flex-col p-2 gap-y-1">
|
||||||
@for (var i = 0; i < Steps.Length; i++)
|
@for (var i = 0; i < Steps.Length; i++)
|
||||||
{
|
{
|
||||||
if (CurrentStep == i)
|
if (CurrentStep == i)
|
||||||
{
|
{
|
||||||
<div class="flex flex-row items-center gap-x-2">
|
<div class="flex flex-row items-center gap-x-1">
|
||||||
<Spinner ClassName="size-4" />
|
@if (IsFailed)
|
||||||
|
{
|
||||||
|
<CircleXIcon ClassName="text-red-500 size-5"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<Spinner ClassName="size-5"/>
|
||||||
|
}
|
||||||
<span>
|
<span>
|
||||||
@Steps[i]
|
@Steps[i]
|
||||||
</span>
|
</span>
|
||||||
@@ -30,8 +42,8 @@
|
|||||||
{
|
{
|
||||||
if (i < CurrentStep)
|
if (i < CurrentStep)
|
||||||
{
|
{
|
||||||
<div class="flex flex-row items-center gap-x-2">
|
<div class="flex flex-row items-center gap-x-1 text-muted-foreground">
|
||||||
<CheckIcon ClassName="text-green-500 size-4" />
|
<CircleCheckIcon ClassName="text-green-500 size-5"/>
|
||||||
<span>
|
<span>
|
||||||
@Steps[i]
|
@Steps[i]
|
||||||
</span>
|
</span>
|
||||||
@@ -39,81 +51,194 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="text-muted-foreground flex flex-row items-center gap-x-2">
|
<div class="text-muted-foreground flex flex-row items-center gap-x-1">
|
||||||
<span class="size-4"></span>
|
<span class="size-5"></span>
|
||||||
@Steps[i]
|
@Steps[i]
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bg-black text-white rounded-lg font-mono h-96 flex flex-col-reverse overflow-auto p-3 scrollbar-thin">
|
||||||
|
@for (var i = LogLines.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
@LogLines[i]
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
@if (CurrentStep == Steps.Length || IsFailed)
|
||||||
<Progress Value="@Progress"></Progress>
|
{
|
||||||
|
<DialogFooter ClassName="justify-end">
|
||||||
|
<Button Variant="ButtonVariant.Outline" @onclick="CloseAsync">Close</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<DialogFooter>
|
||||||
|
<Progress ClassName="my-1" Value="@Progress"></Progress>
|
||||||
|
</DialogFooter>
|
||||||
|
}
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private int Progress = 0;
|
[Parameter] public string Version { get; set; }
|
||||||
|
[Parameter] public bool NoBuildCache { get; set; }
|
||||||
|
|
||||||
|
private bool IsFailed;
|
||||||
|
private int Progress;
|
||||||
private int CurrentStep;
|
private int CurrentStep;
|
||||||
|
|
||||||
private string[] Steps =
|
private readonly string[] Steps =
|
||||||
[
|
[
|
||||||
"Preparing",
|
"Checking", // 0
|
||||||
"Updating configuration files",
|
"Updating configuration files", // 1
|
||||||
"Building docker image",
|
"Starting rebuild task", // 2
|
||||||
"Redeploying container instance",
|
"Building docker image", // 3
|
||||||
"Waiting for container instance to start up",
|
"Redeploying container instance", // 4
|
||||||
"Update complete"
|
"Waiting for container instance to start up", // 5
|
||||||
|
"Update complete" // 6
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private readonly List<string?> LogLines = new();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (!firstRender)
|
if (!firstRender)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Checking
|
||||||
CurrentStep = 0;
|
CurrentStep = 0;
|
||||||
Progress = 0;
|
Progress = 0;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
|
|
||||||
|
// Update configuration
|
||||||
CurrentStep = 1;
|
CurrentStep = 1;
|
||||||
Progress = 20;
|
Progress = 20;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
await Task.Delay(6000);
|
await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
|
||||||
|
{
|
||||||
|
Version = Version
|
||||||
|
}, SerializationContext.Default.Options);
|
||||||
|
|
||||||
|
// Starting rebuild task
|
||||||
CurrentStep = 2;
|
CurrentStep = 2;
|
||||||
Progress = 40;
|
Progress = 30;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
await Task.Delay(2000);
|
var request = new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild");
|
||||||
|
|
||||||
CurrentStep = 3;
|
request.Content = JsonContent.Create(
|
||||||
Progress = 60;
|
new RequestRebuildDto(NoBuildCache),
|
||||||
await InvokeAsync(StateHasChanged);
|
null,
|
||||||
|
SerializationContext.Default.Options
|
||||||
await Task.Delay(4000);
|
|
||||||
|
|
||||||
CurrentStep = 4;
|
|
||||||
Progress = 80;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
await Task.Delay(4000);
|
|
||||||
|
|
||||||
CurrentStep = 5;
|
|
||||||
Progress = 100;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
await AlertService.SuccessAsync(
|
|
||||||
"Update completed",
|
|
||||||
"Update successfully completed. Please refresh the page to load new frontend changes"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await CloseAsync();
|
var response = await HttpClient.SendAsync(
|
||||||
|
request,
|
||||||
|
HttpCompletionOption.ResponseHeadersRead
|
||||||
|
);
|
||||||
|
|
||||||
|
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||||
|
using var streamReader = new StreamReader(responseStream);
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var line = await streamReader.ReadLineAsync();
|
||||||
|
|
||||||
|
if (line == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var data = line.Trim("data: ");
|
||||||
|
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.Default.Options);
|
||||||
|
|
||||||
|
switch (deserializedData.Type)
|
||||||
|
{
|
||||||
|
case RebuildEventType.Log:
|
||||||
|
LogLines.Add(deserializedData.Data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RebuildEventType.Step:
|
||||||
|
|
||||||
|
switch (deserializedData.Data)
|
||||||
|
{
|
||||||
|
case "BuildImage":
|
||||||
|
|
||||||
|
// Building docker image
|
||||||
|
|
||||||
|
CurrentStep = 3;
|
||||||
|
Progress = 40;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ServiceDown":
|
||||||
|
|
||||||
|
// Redeploying container instance
|
||||||
|
|
||||||
|
CurrentStep = 4;
|
||||||
|
Progress = 60;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RebuildEventType.Failed:
|
||||||
|
|
||||||
|
IsFailed = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (true);
|
||||||
|
|
||||||
|
// Waiting for container instance to start up
|
||||||
|
|
||||||
|
CurrentStep = 5;
|
||||||
|
Progress = 90;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
// Wait some time for instance to shut down
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
// Ping instance until its reachable again
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await HttpClient.GetStringAsync("api/ping");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update complete
|
||||||
|
CurrentStep = 7;
|
||||||
|
Progress = 100;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user