Compare commits
2 Commits
v2.1
...
826714962e
| Author | SHA1 | Date | |
|---|---|---|---|
| 826714962e | |||
| 58c882603c |
@@ -34,7 +34,7 @@ jobs:
|
||||
# Publish frontend
|
||||
# We need to build it first so the class list files generate
|
||||
- name: Build project
|
||||
run: dotnet build Hosts/Moonlight.Frontend.Host --configuration Debug
|
||||
run: dotnet build Moonlight.Frontend --configuration Debug
|
||||
|
||||
- name: Build tailwind styles and extract class list
|
||||
working-directory: Hosts/Moonlight.Frontend.Host/Styles
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -400,10 +400,8 @@ FodyWeavers.xsd
|
||||
# Style builds
|
||||
**/style.min.css
|
||||
**/package-lock.json
|
||||
**/bun.lock
|
||||
|
||||
# Secrets
|
||||
**/.env
|
||||
**/appsettings.json
|
||||
**/appsettings.Development.json
|
||||
**/storage
|
||||
**/appsettings.Development.json
|
||||
@@ -1,6 +0,0 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<!-- Put your plugin references here -->
|
||||
<!-- E.g. <PackageReference Include="MoonlightServers.Api" Version="2.1.0" /> -->
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
10
Hosts/Moonlight.Api.Host/AppStartupLoader.cs
Normal file
10
Hosts/Moonlight.Api.Host/AppStartupLoader.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using MoonCore.PluginFramework;
|
||||
using Moonlight.Api.Startup;
|
||||
|
||||
namespace Moonlight.Api.Host;
|
||||
|
||||
[PluginLoader]
|
||||
public partial class AppStartupLoader : IAppStartup
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
# Base image
|
||||
FROM git.battlestati.one/moonlight-panel/app_base:moonlight AS base
|
||||
FROM cgr.dev/chainguard/aspnet-runtime:latest AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
|
||||
# Install required packages
|
||||
RUN apt-get update; apt-get install unzip -y; apt-get clean
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
ENV PATH="/root/.bun/bin:${PATH}"
|
||||
# Build dependencies
|
||||
RUN apt-get update; apt-get install nodejs npm -y; apt-get clean
|
||||
|
||||
# Build options
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
@@ -17,7 +15,7 @@ WORKDIR /src/Hosts/Moonlight.Frontend.Host/Styles
|
||||
|
||||
COPY ["Hosts/Moonlight.Frontend.Host/Styles/package.json", "package.json"]
|
||||
|
||||
RUN bun install
|
||||
RUN npm install
|
||||
|
||||
# Restore nuget packages
|
||||
WORKDIR /src
|
||||
@@ -29,9 +27,6 @@ COPY ["Moonlight.Shared/Moonlight.Shared.csproj", "Moonlight.Shared/"]
|
||||
COPY ["Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj", "Hosts/Moonlight.Frontend.Host/"]
|
||||
COPY ["Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj", "Hosts/Moonlight.Api.Host/"]
|
||||
|
||||
COPY ["Hosts/Moonlight.Frontend.Host/Frontend.props", "Hosts/Moonlight.Frontend.Host/"]
|
||||
COPY ["Hosts/Moonlight.Api.Host/Api.props", "Hosts/Moonlight.Api.Host/"]
|
||||
|
||||
RUN dotnet restore "Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj"
|
||||
RUN dotnet restore "Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj"
|
||||
|
||||
@@ -44,7 +39,7 @@ WORKDIR "/src/Hosts/Moonlight.Frontend.Host"
|
||||
RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
|
||||
|
||||
WORKDIR "/src/Hosts/Moonlight.Frontend.Host/Styles"
|
||||
RUN bun run build
|
||||
RUN npm run build
|
||||
|
||||
# Build projects
|
||||
WORKDIR "/src/Hosts/Moonlight.Api.Host"
|
||||
@@ -72,6 +67,4 @@ WORKDIR /app
|
||||
COPY --from=publish /app/publish-api .
|
||||
COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD ["/usr/bin/curl", "-sf", "-o", "/dev/null", "http://localhost:8080/"]
|
||||
|
||||
ENTRYPOINT ["dotnet", "Moonlight.Api.Host.dll"]
|
||||
@@ -7,13 +7,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
|
||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2" />
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -27,5 +29,4 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="Api.props"/>
|
||||
</Project>
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
using Moonlight.Api;
|
||||
using SimplePlugin.Generated;
|
||||
using Moonlight.Api.Host;
|
||||
|
||||
var plugins = PluginRegistry
|
||||
.Modules
|
||||
.OfType<MoonlightPlugin>()
|
||||
.ToArray();
|
||||
var appLoader = new AppStartupLoader();
|
||||
appLoader.Initialize();
|
||||
|
||||
await StartupHandler.RunAsync(args, plugins);
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
appLoader.PreBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
appLoader.PostBuild(app);
|
||||
|
||||
appLoader.PostMiddleware(app);
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
app.UseWebAssemblyDebugging();
|
||||
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
await app.RunAsync();
|
||||
10
Hosts/Moonlight.Frontend.Host/AppStartupLoader.cs
Normal file
10
Hosts/Moonlight.Frontend.Host/AppStartupLoader.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using MoonCore.PluginFramework;
|
||||
using Moonlight.Frontend.Startup;
|
||||
|
||||
namespace Moonlight.Frontend.Host;
|
||||
|
||||
[PluginLoader]
|
||||
public partial class AppStartupLoader : IAppStartup
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<!-- Put your plugin references here -->
|
||||
<!-- E.g. <PackageReference Include="MoonlightServers.Frontend" Version="2.1.0" /> -->
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -14,13 +14,11 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2" />
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
|
||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="Frontend.props"/>
|
||||
</Project>
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
using Moonlight.Frontend;
|
||||
using SimplePlugin.Generated;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Moonlight.Frontend.Host;
|
||||
|
||||
var plugins = PluginRegistry
|
||||
.Modules
|
||||
.OfType<MoonlightPlugin>()
|
||||
.ToArray();
|
||||
var appLoader = new AppStartupLoader();
|
||||
appLoader.Initialize();
|
||||
|
||||
await StartupHandler.RunAsync(args, plugins);
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
appLoader.PreBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
appLoader.PostBuild(app);
|
||||
|
||||
await app.RunAsync();
|
||||
@@ -1,11 +1,11 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@import "../bin/ShadcnBlazor/scrollbar.css";
|
||||
@import "../bin/ShadcnBlazor/default-theme.css";
|
||||
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/scrollbar.css";
|
||||
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/default-theme.css";
|
||||
@import "./theme.css";
|
||||
|
||||
@source "../bin/ShadcnBlazor/ShadcnBlazor.map";
|
||||
@source "../../../Moonlight.Frontend/bin/ShadcnBlazor/ShadcnBlazor.map";
|
||||
|
||||
@source "../../../Moonlight.Api/**/*.razor";
|
||||
@source "../../../Moonlight.Api/**/*.cs";
|
||||
|
||||
@@ -2,6 +2,5 @@ namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class ApiOptions
|
||||
{
|
||||
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||
public int LookupCacheMinutes { get; set; } = 3;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class CacheOptions
|
||||
{
|
||||
public bool EnableLayer2 { get; set; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class ContainerHelperOptions
|
||||
{
|
||||
public bool IsEnabled { get; set; }
|
||||
public string Url { get; set; } = "http://helper:8080";
|
||||
}
|
||||
@@ -4,7 +4,6 @@ public class OidcOptions
|
||||
{
|
||||
public string Authority { get; set; }
|
||||
public bool RequireHttpsMetadata { get; set; } = true;
|
||||
public bool DisableHttpsOnlyCookies { get; set; }
|
||||
public string ResponseType { get; set; } = "code";
|
||||
public string[]? Scopes { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class RedisOptions
|
||||
{
|
||||
public bool Enable { get; set; }
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
6
Moonlight.Api/Configuration/SessionOptions.cs
Normal file
6
Moonlight.Api/Configuration/SessionOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class SessionOptions
|
||||
{
|
||||
public int ValidationCacheMinutes { get; set; } = 3;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class SettingsOptions
|
||||
{
|
||||
public TimeSpan LookupL1CacheTime { get; set; } = TimeSpan.FromMinutes(1);
|
||||
public TimeSpan LookupL2CacheTime { get; set; } = TimeSpan.FromMinutes(5);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Moonlight.Api.Configuration;
|
||||
|
||||
public class UserOptions
|
||||
{
|
||||
public TimeSpan ValidationCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||
public TimeSpan ValidationCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Moonlight.Api.Constants;
|
||||
|
||||
public class FrontendSettingConstants
|
||||
{
|
||||
public const string Name = "Moonlight.Frontend.Name";
|
||||
}
|
||||
@@ -14,7 +14,6 @@ public class ApiKey : IActionTimestamps
|
||||
public required string Description { get; set; }
|
||||
|
||||
public string[] Permissions { get; set; } = [];
|
||||
public DateTimeOffset ValidUntil { get; set; }
|
||||
|
||||
[MaxLength(32)]
|
||||
public string Key { get; set; }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Moonlight.Api.Database.Entities;
|
||||
|
||||
@@ -11,6 +10,5 @@ public class SettingsOption
|
||||
public required string Key { get; set; }
|
||||
|
||||
[MaxLength(4096)]
|
||||
[Column(TypeName = "jsonb")]
|
||||
public required string ValueJson { get; set; }
|
||||
public required string Value { get; set; }
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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: "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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,9 +56,6 @@ namespace Moonlight.Api.Database.Migrations
|
||||
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");
|
||||
@@ -139,10 +136,10 @@ namespace Moonlight.Api.Database.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ValueJson")
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("jsonb");
|
||||
.HasColumnType("character varying(4096)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Implementations.ApiKeyScheme;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
@@ -20,12 +18,10 @@ namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
public class ApiKeyController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<ApiKey> KeyRepository;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
|
||||
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository)
|
||||
{
|
||||
KeyRepository = keyRepository;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -118,8 +114,6 @@ public class ApiKeyController : Controller
|
||||
ApiKeyMapper.Merge(apiKey, request);
|
||||
await KeyRepository.UpdateAsync(apiKey);
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
|
||||
|
||||
return ApiKeyMapper.ToDto(apiKey);
|
||||
}
|
||||
|
||||
@@ -135,9 +129,6 @@ public class ApiKeyController : Controller
|
||||
return Problem("No API key with this id found", statusCode: 404);
|
||||
|
||||
await KeyRepository.RemoveAsync(apiKey);
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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.Shared;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ using Moonlight.Api.Mappers;
|
||||
using Moonlight.Api.Services;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Requests.Themes;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Themes;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin.Themes;
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/themes")]
|
||||
@@ -6,9 +6,9 @@ using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Shared;
|
||||
using Moonlight.Shared.Http.Requests;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Responses;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin.Users;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Api.Services;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Frontend;
|
||||
using Moonlight.Shared.Http.Responses.Frontend;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers;
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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");
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||
|
||||
public struct RebuildEventDto
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public RebuildEventType Type { get; set; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public enum RebuildEventType
|
||||
{
|
||||
Log = 0,
|
||||
Failed = 1,
|
||||
Succeeded = 2,
|
||||
Step = 3
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
|
||||
public record RequestRebuildDto(bool NoBuildCache);
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
|
||||
public record SetVersionDto(string Version);
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||
|
||||
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||
|
||||
[JsonSerializable(typeof(SetVersionDto))]
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
[JsonSerializable(typeof(RebuildEventDto))]
|
||||
[JsonSerializable(typeof(RequestRebuildDto))]
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Database;
|
||||
@@ -14,20 +14,18 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
||||
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
||||
{
|
||||
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
|
||||
private readonly IMemoryCache MemoryCache;
|
||||
|
||||
public ApiKeySchemeHandler(
|
||||
IOptionsMonitor<ApiKeySchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
DatabaseRepository<ApiKey> apiKeyRepository,
|
||||
HybridCache hybridCache
|
||||
IMemoryCache memoryCache
|
||||
) : base(options, logger, encoder)
|
||||
{
|
||||
ApiKeyRepository = apiKeyRepository;
|
||||
HybridCache = hybridCache;
|
||||
MemoryCache = memoryCache;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
@@ -40,30 +38,24 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
||||
if (authHeaderValue.Length > 32)
|
||||
return AuthenticateResult.Fail("Invalid api key specified");
|
||||
|
||||
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
|
||||
if (!MemoryCache.TryGetValue<ApiKeySession>(authHeaderValue, out var apiKey))
|
||||
{
|
||||
apiKey = await ApiKeyRepository
|
||||
.Query()
|
||||
.Where(x => x.Key == authHeaderValue)
|
||||
.Select(x => new ApiKeySession(x.Permissions))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var apiKey = await HybridCache.GetOrCreateAsync<ApiKeySession?>(
|
||||
cacheKey,
|
||||
async ct =>
|
||||
{
|
||||
return await ApiKeyRepository
|
||||
.Query()
|
||||
.Where(x => x.Key == authHeaderValue)
|
||||
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
|
||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||
},
|
||||
new HybridCacheEntryOptions()
|
||||
{
|
||||
LocalCacheExpiration = Options.LookupL1CacheTime,
|
||||
Expiration = Options.LookupL2CacheTime
|
||||
}
|
||||
);
|
||||
if (apiKey == null)
|
||||
return AuthenticateResult.Fail("Invalid api key specified");
|
||||
|
||||
if (apiKey == null)
|
||||
return AuthenticateResult.Fail("Invalid api key specified");
|
||||
|
||||
if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
|
||||
return AuthenticateResult.Fail("Api key expired");
|
||||
MemoryCache.Set(authHeaderValue, apiKey, Options.LookupCacheTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (apiKey == null)
|
||||
return AuthenticateResult.Fail("Invalid api key specified");
|
||||
}
|
||||
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(
|
||||
new ClaimsPrincipal(
|
||||
@@ -75,5 +67,5 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
||||
));
|
||||
}
|
||||
|
||||
private record ApiKeySession(string[] Permissions, DateTimeOffset ValidUntil);
|
||||
private record ApiKeySession(string[] Permissions);
|
||||
}
|
||||
@@ -4,6 +4,5 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
||||
|
||||
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
public TimeSpan LookupL1CacheTime { get; set; }
|
||||
public TimeSpan LookupL2CacheTime { get; set; }
|
||||
public TimeSpan LookupCacheTime { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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 Moonlight.Api.Models;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Frontend;
|
||||
using Moonlight.Shared.Http.Responses.Frontend;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Requests.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Themes;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
|
||||
namespace Moonlight.Api.Mappers;
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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,14 +25,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||
<PackageReference Include="VYaml" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -40,8 +35,4 @@
|
||||
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Http\Services\ContainerHelper\Responses\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using SimplePlugin.Abstractions;
|
||||
|
||||
namespace Moonlight.Api;
|
||||
|
||||
public abstract class MoonlightPlugin : IPluginModule
|
||||
{
|
||||
protected MoonlightPlugin[] Plugins { get; private set; }
|
||||
|
||||
public void Initialize(MoonlightPlugin[] plugins)
|
||||
{
|
||||
Plugins = plugins;
|
||||
}
|
||||
|
||||
public virtual void PreBuild(WebApplicationBuilder builder)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void PostBuild(WebApplication application)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void PostMiddleware(WebApplication application)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
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,7 +2,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Constants;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Models;
|
||||
@@ -14,16 +13,14 @@ public class FrontendService
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly DatabaseRepository<Theme> ThemeRepository;
|
||||
private readonly IOptions<FrontendOptions> Options;
|
||||
private readonly SettingsService SettingsService;
|
||||
|
||||
private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}";
|
||||
|
||||
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options, SettingsService settingsService)
|
||||
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options)
|
||||
{
|
||||
Cache = cache;
|
||||
ThemeRepository = themeRepository;
|
||||
Options = options;
|
||||
SettingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task<FrontendConfiguration> GetConfigurationAsync()
|
||||
@@ -38,9 +35,7 @@ public class FrontendService
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.IsEnabled);
|
||||
|
||||
var name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name);
|
||||
|
||||
var config = new FrontendConfiguration(name ?? "Moonlight", theme?.CssContent);
|
||||
var config = new FrontendConfiguration("Moonlight", theme?.CssContent);
|
||||
|
||||
Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes));
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
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,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
@@ -14,10 +14,10 @@ namespace Moonlight.Api.Services;
|
||||
public class UserAuthService
|
||||
{
|
||||
private readonly DatabaseRepository<User> UserRepository;
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly ILogger<UserAuthService> Logger;
|
||||
private readonly IOptions<UserOptions> Options;
|
||||
private readonly IOptions<SessionOptions> Options;
|
||||
private readonly IEnumerable<IUserAuthHook> Hooks;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
private const string UserIdClaim = "UserId";
|
||||
private const string IssuedAtClaim = "IssuedAt";
|
||||
@@ -27,16 +27,15 @@ public class UserAuthService
|
||||
public UserAuthService(
|
||||
DatabaseRepository<User> userRepository,
|
||||
ILogger<UserAuthService> logger,
|
||||
IOptions<UserOptions> options,
|
||||
IEnumerable<IUserAuthHook> hooks,
|
||||
HybridCache hybridCache
|
||||
IMemoryCache cache, IOptions<SessionOptions> options,
|
||||
IEnumerable<IUserAuthHook> hooks
|
||||
)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
Logger = logger;
|
||||
Cache = cache;
|
||||
Options = options;
|
||||
Hooks = hooks;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||
@@ -81,8 +80,8 @@ public class UserAuthService
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
{
|
||||
// Run every hook, and if any returns false, we return false as well
|
||||
if (!await hook.SyncAsync(principal, user))
|
||||
// Run every hook and if any returns false we return false as well
|
||||
if(!await hook.SyncAsync(principal, user))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -102,29 +101,32 @@ public class UserAuthService
|
||||
|
||||
var cacheKey = string.Format(CacheKeyPattern, userId);
|
||||
|
||||
var user = await HybridCache.GetOrCreateAsync<UserSession?>(
|
||||
cacheKey,
|
||||
async ct =>
|
||||
{
|
||||
return await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => new UserSession(
|
||||
u.InvalidateTimestamp,
|
||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||
)
|
||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||
},
|
||||
new HybridCacheEntryOptions()
|
||||
{
|
||||
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
|
||||
Expiration = Options.Value.ValidationCacheL2Expiry
|
||||
}
|
||||
);
|
||||
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
|
||||
{
|
||||
user = await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => new UserSession(
|
||||
u.InvalidateTimestamp,
|
||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||
)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (user == null)
|
||||
return false;
|
||||
if (user == null)
|
||||
return false;
|
||||
|
||||
Cache.Set(
|
||||
cacheKey,
|
||||
user,
|
||||
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (user == null)
|
||||
return false;
|
||||
}
|
||||
|
||||
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
||||
|
||||
@@ -144,11 +146,11 @@ public class UserAuthService
|
||||
principal.Identities.First().AddClaims(
|
||||
user.Permissions.Select(x => new Claim(Permissions.ClaimType, x))
|
||||
);
|
||||
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
{
|
||||
// Run every hook, and if any returns false we return false as well
|
||||
if (!await hook.ValidateAsync(principal, userId))
|
||||
// Run every hook and if any returns false we return false as well
|
||||
if(!await hook.ValidateAsync(principal, userId))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Interfaces;
|
||||
@@ -10,17 +10,13 @@ public class UserDeletionService
|
||||
{
|
||||
private readonly DatabaseRepository<User> Repository;
|
||||
private readonly IEnumerable<IUserDeletionHook> Hooks;
|
||||
private readonly HybridCache HybridCache;
|
||||
private readonly IMemoryCache Cache;
|
||||
|
||||
public UserDeletionService(
|
||||
DatabaseRepository<User> repository,
|
||||
IEnumerable<IUserDeletionHook> hooks,
|
||||
HybridCache hybridCache
|
||||
)
|
||||
public UserDeletionService(DatabaseRepository<User> repository, IEnumerable<IUserDeletionHook> hooks, IMemoryCache cache)
|
||||
{
|
||||
Repository = repository;
|
||||
Hooks = hooks;
|
||||
HybridCache = hybridCache;
|
||||
Cache = cache;
|
||||
}
|
||||
|
||||
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
|
||||
@@ -58,8 +54,7 @@ public class UserDeletionService
|
||||
await hook.ExecuteAsync(user);
|
||||
|
||||
await Repository.RemoveAsync(user);
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Interfaces;
|
||||
@@ -10,17 +10,17 @@ public class UserLogoutService
|
||||
{
|
||||
private readonly DatabaseRepository<User> Repository;
|
||||
private readonly IEnumerable<IUserLogoutHook> Hooks;
|
||||
private readonly HybridCache HybridCache;
|
||||
private readonly IMemoryCache Cache;
|
||||
|
||||
public UserLogoutService(
|
||||
DatabaseRepository<User> repository,
|
||||
IEnumerable<IUserLogoutHook> hooks,
|
||||
HybridCache hybridCache
|
||||
IMemoryCache cache
|
||||
)
|
||||
{
|
||||
Repository = repository;
|
||||
Hooks = hooks;
|
||||
HybridCache = hybridCache;
|
||||
Cache = cache;
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(int userId)
|
||||
@@ -28,16 +28,16 @@ public class UserLogoutService
|
||||
var user = await Repository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == userId);
|
||||
|
||||
if (user == null)
|
||||
|
||||
if(user == null)
|
||||
throw new AggregateException($"User with id {userId} not found");
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
await hook.ExecuteAsync(user);
|
||||
|
||||
|
||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
||||
await Repository.UpdateAsync(user);
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||
|
||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
||||
}
|
||||
}
|
||||
10
Moonlight.Api/Startup/IAppStartup.cs
Normal file
10
Moonlight.Api/Startup/IAppStartup.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
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,26 +17,20 @@ public partial class Startup
|
||||
{
|
||||
private static void AddAuth(WebApplicationBuilder builder)
|
||||
{
|
||||
// OIDC
|
||||
var oidcOptions = new OidcOptions();
|
||||
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
|
||||
|
||||
// API Key
|
||||
var apiKeyOptions = new ApiOptions();
|
||||
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
|
||||
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
|
||||
|
||||
// Session
|
||||
builder.Services.AddOptions<UserOptions>().BindConfiguration("Moonlight:User");
|
||||
|
||||
// Authentication
|
||||
builder.Services.AddScoped<UserAuthService>();
|
||||
|
||||
builder.Services.AddAuthentication("Main")
|
||||
.AddPolicyScheme("Main", null,
|
||||
options =>
|
||||
{
|
||||
options.ForwardDefaultSelector += context =>
|
||||
context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
|
||||
})
|
||||
.AddPolicyScheme("Main", null, options =>
|
||||
{
|
||||
options.ForwardDefaultSelector += context => context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
|
||||
})
|
||||
.AddCookie("Session", null, options =>
|
||||
{
|
||||
options.Events.OnSigningIn += async context =>
|
||||
@@ -80,16 +74,10 @@ public partial class Startup
|
||||
options.Authority = oidcOptions.Authority;
|
||||
options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata;
|
||||
|
||||
if (oidcOptions.DisableHttpsOnlyCookies)
|
||||
{
|
||||
options.NonceCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
}
|
||||
|
||||
var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
|
||||
|
||||
options.Scope.Clear();
|
||||
|
||||
|
||||
foreach (var scope in scopes)
|
||||
options.Scope.Add(scope);
|
||||
|
||||
@@ -103,26 +91,15 @@ public partial class Startup
|
||||
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
})
|
||||
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null,
|
||||
options =>
|
||||
{
|
||||
options.LookupL1CacheTime = apiKeyOptions.LookupCacheL1Expiry;
|
||||
options.LookupL2CacheTime = apiKeyOptions.LookupCacheL2Expiry;
|
||||
});
|
||||
|
||||
// Authorization
|
||||
builder.Services.AddAuthorization();
|
||||
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null, options =>
|
||||
{
|
||||
options.LookupCacheTime = TimeSpan.FromMinutes(apiKeyOptions.LookupCacheMinutes);
|
||||
});
|
||||
|
||||
// Reduce log noise
|
||||
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
|
||||
|
||||
// Custom permission handling using named policies
|
||||
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
||||
|
||||
builder.Services.AddScoped<UserDeletionService>();
|
||||
builder.Services.AddScoped<UserLogoutService>();
|
||||
builder.Services.AddScoped<UserAuthService>();
|
||||
}
|
||||
|
||||
private static void UseAuth(WebApplication application)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
@@ -11,73 +9,43 @@ using Moonlight.Api.Helpers;
|
||||
using Moonlight.Api.Implementations;
|
||||
using Moonlight.Api.Interfaces;
|
||||
using Moonlight.Api.Services;
|
||||
using SessionOptions = Moonlight.Api.Configuration.SessionOptions;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
public partial class Startup
|
||||
{
|
||||
private void AddBase(WebApplicationBuilder builder)
|
||||
private static void AddBase(WebApplicationBuilder builder)
|
||||
{
|
||||
// Create the base directory
|
||||
Directory.CreateDirectory("storage");
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
});
|
||||
|
||||
// Hook up source-generated serialization and add controllers
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddApplicationPart(typeof(Startup).Assembly)
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
});
|
||||
|
||||
// Configure logging
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
||||
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
||||
|
||||
// Application service
|
||||
builder.Services.AddSingleton<ApplicationService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
||||
|
||||
// Diagnose
|
||||
builder.Services.AddSingleton<DiagnoseService>();
|
||||
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
||||
|
||||
// Frontend
|
||||
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddOptions<SessionOptions>().BindConfiguration("Moonlight:Session");
|
||||
|
||||
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
||||
builder.Services.AddScoped<FrontendService>();
|
||||
|
||||
// HTTP Client
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// Version fetching configuration
|
||||
|
||||
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
|
||||
builder.Services.AddSingleton<VersionService>();
|
||||
|
||||
// Container Helper Options
|
||||
builder.Configuration.GetSection("Moonlight:ContainerHelper").Bind(builder.Configuration);
|
||||
|
||||
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
|
||||
builder.Services.AddSingleton<ContainerHelperService>();
|
||||
|
||||
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
|
||||
{
|
||||
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
|
||||
client.BaseAddress =
|
||||
new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
|
||||
});
|
||||
|
||||
// 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)
|
||||
@@ -90,8 +58,8 @@ public partial class Startup
|
||||
application.MapControllers();
|
||||
|
||||
var options = application.Services.GetRequiredService<IOptions<FrontendOptions>>();
|
||||
|
||||
if (options.Value.Enabled)
|
||||
|
||||
if(options.Value.Enabled)
|
||||
application.MapFallbackToFile("index.html");
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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,29 +1,23 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Moonlight.Shared.Http;
|
||||
using SimplePlugin.Abstractions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
|
||||
[PluginModule]
|
||||
public partial class Startup : MoonlightPlugin
|
||||
public partial class Startup : IAppStartup
|
||||
{
|
||||
public override void PreBuild(WebApplicationBuilder builder)
|
||||
public void PreBuild(WebApplicationBuilder builder)
|
||||
{
|
||||
AddBase(builder);
|
||||
AddAuth(builder);
|
||||
AddDatabase(builder);
|
||||
AddCache(builder);
|
||||
}
|
||||
|
||||
public override void PostBuild(WebApplication application)
|
||||
public void PostBuild(WebApplication application)
|
||||
{
|
||||
UseBase(application);
|
||||
UseAuth(application);
|
||||
}
|
||||
|
||||
public override void PostMiddleware(WebApplication application)
|
||||
public void PostMiddleware(WebApplication application)
|
||||
{
|
||||
MapBase(application);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Moonlight.Frontend.Configuration;
|
||||
|
||||
public class NavigationAssemblyOptions
|
||||
{
|
||||
public List<Assembly> Assemblies { get; private set; } = new();
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
);
|
||||
28
Moonlight.Frontend/Constants.cs
Normal file
28
Moonlight.Frontend/Constants.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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,8 +28,6 @@ public sealed class PermissionProvider : IPermissionProvider
|
||||
new Permission(Permissions.System.Info, "Info", "View system info"),
|
||||
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
|
||||
new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"),
|
||||
new Permission(Permissions.System.Instance, "Instance", "Update the moonlight instance and add plugins"),
|
||||
new Permission(Permissions.System.Settings, "Settings", "Change settings of the instance"),
|
||||
]),
|
||||
new PermissionCategory("API Keys", typeof(KeyIcon), [
|
||||
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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 Moonlight.Shared.Http.Requests.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
|
||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||
using Moonlight.Shared.Http.Responses.ApiKeys;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Frontend.Mappers;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Roles;
|
||||
using Moonlight.Shared.Http.Requests.Roles;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Themes;
|
||||
using Moonlight.Shared.Http.Requests.Themes;
|
||||
using Moonlight.Shared.Http.Responses.Themes;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace Moonlight.Frontend.Mappers;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Users;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
using Moonlight.Shared.Http.Requests.Users;
|
||||
using Moonlight.Shared.Http.Responses.Users;
|
||||
|
||||
namespace Moonlight.Frontend.Mappers;
|
||||
|
||||
|
||||
@@ -24,9 +24,8 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||
<PackageReference Include="ShadcnBlazor" Version="1.0.13" />
|
||||
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.13" />
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
|
||||
<PackageReference Include="ShadcnBlazor" Version="1.0.9" />
|
||||
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -36,6 +35,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Styles/*" Pack="true" PackagePath="Styles/" />
|
||||
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\Moonlight.Frontend.targets" />
|
||||
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="buildTransitive\Moonlight.Frontend.targets" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -5,7 +5,7 @@
|
||||
</MoonlightCssClassDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="Moonlight_CopyContents" BeforeTargets="Build">
|
||||
<Target Name="CopyContents" BeforeTargets="Build">
|
||||
<ItemGroup>
|
||||
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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,9 +3,7 @@ using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualBasic;
|
||||
using Moonlight.Shared.Http;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Auth;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
|
||||
namespace Moonlight.Frontend.Services;
|
||||
|
||||
@@ -25,7 +23,7 @@ public class RemoteAuthProvider : AuthenticationStateProvider
|
||||
try
|
||||
{
|
||||
var claimResponses = await HttpClient.GetFromJsonAsync<ClaimDto[]>(
|
||||
"api/auth/claims", SerializationContext.Default.Options
|
||||
"api/auth/claims", Constants.SerializerOptions
|
||||
);
|
||||
|
||||
var claims = claimResponses!.Select(claim => new Claim(claim.Type, claim.Value));
|
||||
|
||||
9
Moonlight.Frontend/Startup/IAppStartup.cs
Normal file
9
Moonlight.Frontend/Startup/IAppStartup.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
namespace Moonlight.Frontend.Startup;
|
||||
|
||||
public interface IAppStartup
|
||||
{
|
||||
public void PreBuild(WebAssemblyHostBuilder builder);
|
||||
public void PostBuild(WebAssemblyHost application);
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
using LucideBlazor;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moonlight.Frontend.Configuration;
|
||||
using Moonlight.Frontend.Implementations;
|
||||
using Moonlight.Frontend.Interfaces;
|
||||
using Moonlight.Frontend.Services;
|
||||
using Moonlight.Frontend.UI;
|
||||
using Moonlight.Frontend.UI.Admin.Settings;
|
||||
using ShadcnBlazor;
|
||||
using ShadcnBlazor.Extras;
|
||||
|
||||
@@ -28,19 +25,5 @@ public partial class Startup
|
||||
builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>();
|
||||
|
||||
builder.Services.AddScoped<FrontendService>();
|
||||
|
||||
builder.Services.Configure<NavigationAssemblyOptions>(options =>
|
||||
{
|
||||
options.Assemblies.Add(typeof(Startup).Assembly);
|
||||
});
|
||||
|
||||
builder.Services.Configure<SystemSettingsOptions>(options =>
|
||||
{
|
||||
options.Add<TextCursorInputIcon, WhiteLabelingSetting>(
|
||||
"White Labeling",
|
||||
"Settings for white labeling your moonlight instance",
|
||||
0
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using SimplePlugin.Abstractions;
|
||||
|
||||
namespace Moonlight.Frontend.Startup;
|
||||
|
||||
[PluginModule]
|
||||
public partial class Startup : MoonlightPlugin
|
||||
public partial class Startup : IAppStartup
|
||||
{
|
||||
public override void PreBuild(WebAssemblyHostBuilder builder)
|
||||
public void PreBuild(WebAssemblyHostBuilder builder)
|
||||
{
|
||||
AddBase(builder);
|
||||
AddAuth(builder);
|
||||
}
|
||||
|
||||
public override void PostBuild(WebAssemblyHost application)
|
||||
public void PostBuild(WebAssemblyHost application)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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,19 +1,13 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http.Requests.ApiKeys
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create new API key</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -21,80 +15,56 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
<FormValidationSummary />
|
||||
|
||||
<FieldGroup>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="keyName">Name</FieldLabel>
|
||||
<TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyValidUntil">Valid until</FieldLabel>
|
||||
<DateTimeInputField @bind-Value="Request.ValidUntil" id="keyValidUntil" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Permissions</FieldLabel>
|
||||
<FieldContent>
|
||||
<PermissionSelector Permissions="Permissions"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
<div class="grid gap-2">
|
||||
<Label for="keyName">Name</Label>
|
||||
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="keyDescription">Description</Label>
|
||||
<textarea
|
||||
@bind="Request.Description"
|
||||
id="keyDescription"
|
||||
maxlength="100"
|
||||
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"
|
||||
placeholder="What this key is for">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
<PermissionSelector Permissions="Permissions" />
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="() => FormHandler.SubmitAsync()">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<CreateApiKeyDto, Task> OnSubmit { get; set; }
|
||||
|
||||
private CreateApiKeyDto Request;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
private List<string> Permissions = new();
|
||||
private List<string> Permissions = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = new()
|
||||
{
|
||||
Permissions = [],
|
||||
ValidUntil = DateTimeOffset.UtcNow
|
||||
};
|
||||
Request = new();
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
Request.Permissions = Permissions.ToArray();
|
||||
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 OnSubmit.Invoke(Request);
|
||||
await CloseAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Roles
|
||||
@using Moonlight.Shared.Http.Requests.Roles
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Create new role
|
||||
@@ -22,43 +18,49 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="roleName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="roleName"
|
||||
placeholder="My fancy role"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||
<TextareaInputField @bind-Value="Request.Description" id="keyDescription"
|
||||
placeholder="Describe what the role should be used for"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Permissions</FieldLabel>
|
||||
<FieldContent>
|
||||
<PermissionSelector Permissions="Permissions"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="roleName">Name</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Name"
|
||||
id="roleName"
|
||||
placeholder="My fancy role"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="roleDescription">Description</Label>
|
||||
<textarea
|
||||
@bind="Request.Description"
|
||||
id="roleDescription"
|
||||
maxlength="100"
|
||||
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"
|
||||
placeholder="Describe what the role should be used for">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
<PermissionSelector Permissions="Permissions" />
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<CreateRoleDto, Task> OnSubmit { get; set; }
|
||||
|
||||
private CreateRoleDto Request;
|
||||
private List<string> Permissions;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -66,31 +68,19 @@
|
||||
{
|
||||
Permissions = []
|
||||
};
|
||||
|
||||
|
||||
Permissions = new();
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
Request.Permissions = Permissions.ToArray();
|
||||
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"api/admin/roles",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
await FormHandler.SubmitAsync();
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync("Role creation", $"Role {Request.Name} has been successfully created");
|
||||
|
||||
await OnSubmit.Invoke();
|
||||
private async Task OnSubmitAsync()
|
||||
{
|
||||
await OnSubmit.Invoke(Request);
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Users
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Requests.Users
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Create new user
|
||||
@@ -22,67 +16,50 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="username">Username</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Username"
|
||||
id="username"
|
||||
placeholder="Name of the user"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="emailAddress">Email Address</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Email"
|
||||
id="emailAddress"
|
||||
placeholder="email@of.user"/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
<div class="grid gap-2">
|
||||
<Label for="username">Username</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Username"
|
||||
id="username"
|
||||
placeholder="Name of the user"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="emailAddress">Email Address</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Email"
|
||||
id="emailAddress"
|
||||
Type="email"
|
||||
placeholder="email@of.user"/>
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<Task> OnCompleted { get; set; }
|
||||
[Parameter] public Func<CreateUserDto, Task> OnSubmit { get; set; }
|
||||
|
||||
private CreateUserDto Request;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = new();
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/users",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
await OnSubmit.Invoke(Request);
|
||||
|
||||
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();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||
@using Moonlight.Shared.Http.Responses.Users
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@@ -32,9 +32,9 @@
|
||||
SearchPlaceholder="Search user"
|
||||
ValueSelector="dto => dto.Username"
|
||||
Source="LoadUsersAsync"/>
|
||||
<WButton OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
|
||||
<WButtom OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
|
||||
<PlusIcon/>
|
||||
</WButton>
|
||||
</WButtom>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
@@ -50,9 +50,9 @@
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
<div class="flex justify-end me-1.5">
|
||||
<WButton OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
|
||||
<WButtom OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
|
||||
<TrashIcon/>
|
||||
</WButton>
|
||||
</WButtom>
|
||||
</div>
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
||||
@using Moonlight.Shared.Http.Requests.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses.ApiKeys
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update API key</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -22,43 +17,45 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
<FormValidationSummary />
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="keyName">Name</FieldLabel>
|
||||
<TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyValidUntil">Valid until</FieldLabel>
|
||||
<DateTimeInputField @bind-Value="Request.ValidUntil" id="keyValidUntil" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Permissions</FieldLabel>
|
||||
<FieldContent>
|
||||
<PermissionSelector Permissions="Permissions"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
<div class="grid gap-2">
|
||||
<Label for="keyName">Name</Label>
|
||||
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="keyDescription">Description</Label>
|
||||
<textarea
|
||||
@bind="Request.Description"
|
||||
id="keyDescription"
|
||||
maxlength="100"
|
||||
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"
|
||||
placeholder="What this key is for">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
<PermissionSelector Permissions="Permissions" />
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<UpdateApiKeyDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public ApiKeyDto Key { get; set; }
|
||||
|
||||
private UpdateApiKeyDto Request;
|
||||
private FormHandler FormHandler;
|
||||
private List<string> Permissions = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
@@ -67,31 +64,10 @@
|
||||
Permissions = Key.Permissions.ToList();
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
Request.Permissions = Permissions.ToArray();
|
||||
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 OnSubmit.Invoke(Request);
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,37 @@
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@using System.Text.Json
|
||||
@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.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Progresses
|
||||
@using ShadcnBlazor.Spinners
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject AlertDialogService AlertService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Updating instance to @Version...
|
||||
Updating...
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<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++)
|
||||
<div class="text-base flex flex-col p-2 gap-y-0.5">
|
||||
@for (var i = 0; i < Steps.Length; i++)
|
||||
{
|
||||
if (CurrentStep == i)
|
||||
{
|
||||
if (CurrentStep == i)
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<Spinner ClassName="size-4" />
|
||||
<span>
|
||||
@Steps[i]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (i < CurrentStep)
|
||||
{
|
||||
<div class="flex flex-row items-center gap-x-1">
|
||||
@if (IsFailed)
|
||||
{
|
||||
<CircleXIcon ClassName="text-red-500 size-5"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Spinner ClassName="size-5"/>
|
||||
}
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<CheckIcon ClassName="text-green-500 size-4" />
|
||||
<span>
|
||||
@Steps[i]
|
||||
</span>
|
||||
@@ -40,205 +39,81 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
if (i < CurrentStep)
|
||||
{
|
||||
<div class="flex flex-row items-center gap-x-1 text-muted-foreground">
|
||||
<CircleCheckIcon ClassName="text-green-500 size-5"/>
|
||||
<span>
|
||||
@Steps[i]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-muted-foreground flex flex-row items-center gap-x-1">
|
||||
<span class="size-5"></span>
|
||||
@Steps[i]
|
||||
</div>
|
||||
}
|
||||
<div class="text-muted-foreground flex flex-row items-center gap-x-2">
|
||||
<span class="size-4"></span>
|
||||
@Steps[i]
|
||||
</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>
|
||||
|
||||
@if (CurrentStep == Steps.Length || IsFailed)
|
||||
{
|
||||
<DialogFooter ClassName="justify-end">
|
||||
<Button Variant="ButtonVariant.Outline" @onclick="CloseAsync">Close</Button>
|
||||
</DialogFooter>
|
||||
}
|
||||
else
|
||||
{
|
||||
<DialogFooter>
|
||||
<Progress ClassName="my-1" Value="@Progress"></Progress>
|
||||
</DialogFooter>
|
||||
}
|
||||
<DialogFooter>
|
||||
<Progress Value="@Progress"></Progress>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public string Version { get; set; }
|
||||
[Parameter] public bool NoBuildCache { get; set; }
|
||||
private int Progress = 0;
|
||||
|
||||
private bool IsFailed;
|
||||
private int Progress;
|
||||
private int CurrentStep;
|
||||
|
||||
private readonly string[] Steps =
|
||||
private string[] Steps =
|
||||
[
|
||||
"Checking", // 0
|
||||
"Updating configuration files", // 1
|
||||
"Starting rebuild task", // 2
|
||||
"Building docker image", // 3
|
||||
"Redeploying container instance", // 4
|
||||
"Waiting for container instance to start up", // 5
|
||||
"Update complete" // 6
|
||||
"Preparing",
|
||||
"Updating configuration files",
|
||||
"Building docker image",
|
||||
"Redeploying container instance",
|
||||
"Waiting for container instance to start up",
|
||||
"Update complete"
|
||||
];
|
||||
|
||||
private readonly List<string?> LogLines = new();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender)
|
||||
return;
|
||||
|
||||
// Checking
|
||||
CurrentStep = 0;
|
||||
Progress = 0;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(2000);
|
||||
|
||||
// Update configuration
|
||||
CurrentStep = 1;
|
||||
Progress = 20;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
|
||||
{
|
||||
Version = Version
|
||||
}, SerializationContext.Default.Options);
|
||||
await Task.Delay(6000);
|
||||
|
||||
// Starting rebuild task
|
||||
CurrentStep = 2;
|
||||
Progress = 30;
|
||||
Progress = 40;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild");
|
||||
await Task.Delay(2000);
|
||||
|
||||
request.Content = JsonContent.Create(
|
||||
new RequestRebuildDto(NoBuildCache),
|
||||
null,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
CurrentStep = 3;
|
||||
Progress = 60;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var response = await HttpClient.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead
|
||||
);
|
||||
await Task.Delay(4000);
|
||||
|
||||
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 = 4;
|
||||
Progress = 80;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(4000);
|
||||
|
||||
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);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
await AlertService.SuccessAsync(
|
||||
"Update completed",
|
||||
"Update successfully completed. Please refresh the page to load new frontend changes"
|
||||
);
|
||||
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Frontend.UI.Admin.Components
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Roles
|
||||
@using Moonlight.Shared.Http.Requests.Roles
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Update @Role.Name
|
||||
@@ -24,44 +20,50 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="roleName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="roleName"
|
||||
placeholder="My fancy role"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="keyDescription">Description</FieldLabel>
|
||||
<TextareaInputField @bind-Value="Request.Description" id="keyDescription"
|
||||
placeholder="Describe what the role should be used for"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Permissions</FieldLabel>
|
||||
<FieldContent>
|
||||
<PermissionSelector Permissions="Permissions"/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="roleName">Name</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Name"
|
||||
id="roleName"
|
||||
placeholder="My fancy role"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="roleDescription">Description</Label>
|
||||
<textarea
|
||||
@bind="Request.Description"
|
||||
id="roleDescription"
|
||||
maxlength="100"
|
||||
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"
|
||||
placeholder="Describe what the role should be used for">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
<PermissionSelector Permissions="Permissions" />
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
[Parameter] public Func<UpdateRoleDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public RoleDto Role { get; set; }
|
||||
|
||||
|
||||
private UpdateRoleDto Request;
|
||||
private List<string> Permissions;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -69,27 +71,15 @@
|
||||
Permissions = Role.Permissions.ToList();
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
Request.Permissions = Permissions.ToArray();
|
||||
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"api/admin/roles/{Role.Id}",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
await FormHandler.SubmitAsync();
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync("Role update", $"Role {Request.Name} has been successfully updated");
|
||||
|
||||
await OnSubmit.Invoke();
|
||||
private async Task OnSubmitAsync()
|
||||
{
|
||||
await OnSubmit.Invoke(Request);
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Users
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||
@using Moonlight.Frontend.Mappers
|
||||
@using Moonlight.Shared.Http.Requests.Users
|
||||
@using Moonlight.Shared.Http.Responses.Users
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.FormHandlers
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Labels
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Update @User.Username
|
||||
@@ -23,66 +18,51 @@
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="username">Username</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Username"
|
||||
id="username"
|
||||
placeholder="Name of the user"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="emailAddress">Email Address</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Email"
|
||||
id="emailAddress"
|
||||
placeholder="email@of.user"/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="username">Username</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Username"
|
||||
id="username"
|
||||
placeholder="Name of the user"/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="emailAddress">Email Address</Label>
|
||||
<InputField
|
||||
@bind-Value="Request.Email"
|
||||
id="emailAddress"
|
||||
Type="email"
|
||||
placeholder="email@of.user"/>
|
||||
</div>
|
||||
</div>
|
||||
</FormHandler>
|
||||
|
||||
<DialogFooter ClassName="justify-end gap-x-1">
|
||||
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<Task> OnCompleted { get; set; }
|
||||
[Parameter] public Func<UpdateUserDto, Task> OnSubmit { get; set; }
|
||||
[Parameter] public UserDto User { get; set; }
|
||||
|
||||
|
||||
private UpdateUserDto Request;
|
||||
private FormHandler FormHandler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = UserMapper.ToUpdate(User);
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
var response = await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/users/{User.Id}",
|
||||
Request
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
await OnSubmit.Invoke(Request);
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"User update",
|
||||
$"Successfully updated user {Request.Username}"
|
||||
);
|
||||
|
||||
await OnCompleted.Invoke();
|
||||
await CloseAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Helpers
|
||||
@using Moonlight.Frontend.Services
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests.Admin.Settings
|
||||
@using Moonlight.Shared.Http.Responses.Admin.Settings
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
@inject FrontendService FrontendService
|
||||
|
||||
<LazyLoader Load="LoadAsync">
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnValidSubmit">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<FieldSet>
|
||||
<FormValidationSummary />
|
||||
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel>Name</FieldLabel>
|
||||
<TextInputField @bind-Value="Request.Name"/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
|
||||
<SubmitButton ClassName="mt-3">
|
||||
<SaveIcon/>
|
||||
Save
|
||||
</SubmitButton>
|
||||
</EnhancedEditForm>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
private SetWhiteLabelingDto Request;
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
var dto = await HttpClient.GetFromJsonAsync<WhiteLabelingDto>(
|
||||
"api/admin/system/settings/whiteLabeling",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
Request = new SetWhiteLabelingDto()
|
||||
{
|
||||
Name = dto!.Name
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> OnValidSubmit(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"api/admin/system/settings/whiteLabeling",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
await FrontendService.ReloadAsync();
|
||||
await ToastService.SuccessAsync("Setting", "Successfully updated white labeling settings");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
@page "/admin"
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@@ -134,11 +133,7 @@
|
||||
{
|
||||
<CardTitle ClassName="text-lg text-primary">Update available</CardTitle>
|
||||
<CardAction ClassName="self-center">
|
||||
<Button>
|
||||
<Slot>
|
||||
<a href="/admin/system?tab=instance" @attributes="context">Update</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<Button @onclick="LaunchUpdateModalAsync">Update</Button>
|
||||
</CardAction>
|
||||
}
|
||||
</CardHeader>
|
||||
@@ -156,9 +151,14 @@
|
||||
if(!firstRender)
|
||||
return;
|
||||
|
||||
InfoResponse = await HttpClient.GetFromJsonAsync<SystemInfoDto>("api/admin/system/info", SerializationContext.Default.Options);
|
||||
InfoResponse = await HttpClient.GetFromJsonAsync<SystemInfoDto>("api/admin/system/info", Constants.SerializerOptions);
|
||||
IsInfoLoading = false;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync<UpdateInstanceModal>(onConfigure: model =>
|
||||
{
|
||||
model.ShowCloseButton = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Requests.ApiKeys
|
||||
@using Moonlight.Shared.Http.Responses.ApiKeys
|
||||
@using LucideBlazor
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Moonlight.Frontend.UI.Admin.Modals
|
||||
@using Moonlight.Shared
|
||||
@using Moonlight.Shared.Http
|
||||
@using Moonlight.Shared.Http.Requests
|
||||
@using Moonlight.Shared.Http.Responses
|
||||
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Dropdowns
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@@ -49,21 +49,7 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn IsFilterable="true"
|
||||
Identifier="@nameof(ApiKeyDto.Description)" Field="k => k.Description" HeadClassName="hidden lg:table-cell" CellClassName="hidden lg:table-cell" />
|
||||
<TemplateColumn Identifier="@nameof(ApiKeyDto.ValidUntil)" Title="Valid until" HeadClassName="hidden lg:table-cell">
|
||||
<CellTemplate>
|
||||
<TableCell ClassName="hidden lg:table-cell">
|
||||
@{
|
||||
var diff = context.ValidUntil - DateTimeOffset.UtcNow;
|
||||
var text = string.Format(diff.TotalSeconds < 0 ? "Expired since {0}" : "Expires in {0}", Formatter.FormatDuration(diff));
|
||||
}
|
||||
|
||||
<span>
|
||||
@text
|
||||
</span>
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
Identifier="@nameof(ApiKeyDto.Description)" Field="k => k.Description"/>
|
||||
<TemplateColumn>
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
@@ -127,7 +113,7 @@
|
||||
|
||||
var response = await HttpClient.GetFromJsonAsync<PagedData<ApiKeyDto>>(
|
||||
$"api/admin/apiKeys{query}&filterOptions={filterOptions}",
|
||||
SerializationContext.Default.Options
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
return new DataGridResponse<ApiKeyDto>(response!.Data, response.TotalLength);
|
||||
@@ -137,8 +123,19 @@
|
||||
{
|
||||
await DialogService.LaunchAsync<CreateApiKeyDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async () =>
|
||||
parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async (CreateApiKeyDto dto) =>
|
||||
{
|
||||
await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/apiKeys",
|
||||
dto,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"API Key creation",
|
||||
$"Successfully created API key {dto.Name}"
|
||||
);
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
};
|
||||
});
|
||||
@@ -149,8 +146,19 @@
|
||||
await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(UpdateApiKeyDialog.Key)] = key;
|
||||
parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async () =>
|
||||
parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async (UpdateApiKeyDto dto) =>
|
||||
{
|
||||
await HttpClient.PatchAsJsonAsync(
|
||||
$"/api/admin/apiKeys/{key.Id}",
|
||||
dto,
|
||||
Constants.SerializerOptions
|
||||
);
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"API Key update",
|
||||
$"Successfully updated API key {dto.Name}"
|
||||
);
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter ClassName="justify-end">
|
||||
<WButton OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)">
|
||||
<WButtom OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)">
|
||||
<StethoscopeIcon/>
|
||||
Start diagnostics
|
||||
</WButton>
|
||||
</WButtom>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user