2 Commits

142 changed files with 947 additions and 3549 deletions

View File

@@ -34,7 +34,7 @@ jobs:
# Publish frontend # Publish frontend
# We need to build it first so the class list files generate # We need to build it first so the class list files generate
- name: Build project - name: Build project
run: dotnet build Hosts/Moonlight.Frontend.Host --configuration Debug run: dotnet build Moonlight.Frontend --configuration Debug
- name: Build tailwind styles and extract class list - name: Build tailwind styles and extract class list
working-directory: Hosts/Moonlight.Frontend.Host/Styles working-directory: Hosts/Moonlight.Frontend.Host/Styles

4
.gitignore vendored
View File

@@ -400,10 +400,8 @@ FodyWeavers.xsd
# Style builds # Style builds
**/style.min.css **/style.min.css
**/package-lock.json **/package-lock.json
**/bun.lock
# Secrets # Secrets
**/.env **/.env
**/appsettings.json **/appsettings.json
**/appsettings.Development.json **/appsettings.Development.json
**/storage

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
# Base image # Base image
FROM git.battlestati.one/moonlight-panel/app_base:moonlight AS base FROM cgr.dev/chainguard/aspnet-runtime:latest AS base
WORKDIR /app WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# Install required packages # Build dependencies
RUN apt-get update; apt-get install unzip -y; apt-get clean RUN apt-get update; apt-get install nodejs npm -y; apt-get clean
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
# Build options # Build options
ARG BUILD_CONFIGURATION=Release ARG BUILD_CONFIGURATION=Release
@@ -17,7 +15,7 @@ WORKDIR /src/Hosts/Moonlight.Frontend.Host/Styles
COPY ["Hosts/Moonlight.Frontend.Host/Styles/package.json", "package.json"] COPY ["Hosts/Moonlight.Frontend.Host/Styles/package.json", "package.json"]
RUN bun install RUN npm install
# Restore nuget packages # Restore nuget packages
WORKDIR /src 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.Frontend.Host/Moonlight.Frontend.Host.csproj", "Hosts/Moonlight.Frontend.Host/"]
COPY ["Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj", "Hosts/Moonlight.Api.Host/"] COPY ["Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj", "Hosts/Moonlight.Api.Host/"]
COPY ["Hosts/Moonlight.Frontend.Host/Frontend.props", "Hosts/Moonlight.Frontend.Host/"]
COPY ["Hosts/Moonlight.Api.Host/Api.props", "Hosts/Moonlight.Api.Host/"]
RUN dotnet restore "Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj" RUN dotnet restore "Hosts/Moonlight.Api.Host/Moonlight.Api.Host.csproj"
RUN dotnet restore "Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj" RUN dotnet restore "Hosts/Moonlight.Frontend.Host/Moonlight.Frontend.Host.csproj"
@@ -44,7 +39,7 @@ WORKDIR "/src/Hosts/Moonlight.Frontend.Host"
RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend RUN dotnet build "./Moonlight.Frontend.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build-frontend
WORKDIR "/src/Hosts/Moonlight.Frontend.Host/Styles" WORKDIR "/src/Hosts/Moonlight.Frontend.Host/Styles"
RUN bun run build RUN npm run build
# Build projects # Build projects
WORKDIR "/src/Hosts/Moonlight.Api.Host" WORKDIR "/src/Hosts/Moonlight.Api.Host"
@@ -72,6 +67,4 @@ WORKDIR /app
COPY --from=publish /app/publish-api . COPY --from=publish /app/publish-api .
COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot COPY --from=publish /app/publish-frontend/wwwroot ./wwwroot
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD ["/usr/bin/curl", "-sf", "-o", "/dev/null", "http://localhost:8080/"]
ENTRYPOINT ["dotnet", "Moonlight.Api.Host.dll"] ENTRYPOINT ["dotnet", "Moonlight.Api.Host.dll"]

View File

@@ -7,13 +7,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="SimplePlugin" Version="1.0.2" />
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -27,5 +29,4 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<Import Project="Api.props"/>
</Project> </Project>

View File

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

View File

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

View File

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

View File

@@ -14,13 +14,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
<PackageReference Include="SimplePlugin" Version="1.0.2" /> <PackageReference Include="MoonCore.PluginFramework" Version="1.0.9"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" /> <PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj" /> <ProjectReference Include="..\..\Moonlight.Frontend\Moonlight.Frontend.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="Frontend.props"/>
</Project> </Project>

View File

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

View File

@@ -1,11 +1,11 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "../bin/ShadcnBlazor/scrollbar.css"; @import "../../../Moonlight.Frontend/bin/ShadcnBlazor/scrollbar.css";
@import "../bin/ShadcnBlazor/default-theme.css"; @import "../../../Moonlight.Frontend/bin/ShadcnBlazor/default-theme.css";
@import "./theme.css"; @import "./theme.css";
@source "../bin/ShadcnBlazor/ShadcnBlazor.map"; @source "../../../Moonlight.Frontend/bin/ShadcnBlazor/ShadcnBlazor.map";
@source "../../../Moonlight.Api/**/*.razor"; @source "../../../Moonlight.Api/**/*.razor";
@source "../../../Moonlight.Api/**/*.cs"; @source "../../../Moonlight.Api/**/*.cs";

View File

@@ -2,6 +2,5 @@ namespace Moonlight.Api.Configuration;
public class ApiOptions public class ApiOptions
{ {
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30); public int LookupCacheMinutes { get; set; } = 3;
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
} }

View File

@@ -1,6 +0,0 @@
namespace Moonlight.Api.Configuration;
public class CacheOptions
{
public bool EnableLayer2 { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace Moonlight.Api.Configuration;
public class ContainerHelperOptions
{
public bool IsEnabled { get; set; }
public string Url { get; set; } = "http://helper:8080";
}

View File

@@ -4,7 +4,6 @@ public class OidcOptions
{ {
public string Authority { get; set; } public string Authority { get; set; }
public bool RequireHttpsMetadata { get; set; } = true; public bool RequireHttpsMetadata { get; set; } = true;
public bool DisableHttpsOnlyCookies { get; set; }
public string ResponseType { get; set; } = "code"; public string ResponseType { get; set; } = "code";
public string[]? Scopes { get; set; } public string[]? Scopes { get; set; }
public string ClientId { get; set; } public string ClientId { get; set; }

View File

@@ -1,7 +0,0 @@
namespace Moonlight.Api.Configuration;
public class RedisOptions
{
public bool Enable { get; set; }
public string ConnectionString { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Configuration;
public class SessionOptions
{
public int ValidationCacheMinutes { get; set; } = 3;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -1,6 +0,0 @@
namespace Moonlight.Api.Constants;
public class FrontendSettingConstants
{
public const string Name = "Moonlight.Frontend.Name";
}

View File

@@ -14,7 +14,6 @@ public class ApiKey : IActionTimestamps
public required string Description { get; set; } public required string Description { get; set; }
public string[] Permissions { get; set; } = []; public string[] Permissions { get; set; } = [];
public DateTimeOffset ValidUntil { get; set; }
[MaxLength(32)] [MaxLength(32)]
public string Key { get; set; } public string Key { get; set; }

View File

@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Moonlight.Api.Database.Entities; namespace Moonlight.Api.Database.Entities;
@@ -11,6 +10,5 @@ public class SettingsOption
public required string Key { get; set; } public required string Key { get; set; }
[MaxLength(4096)] [MaxLength(4096)]
[Column(TypeName = "jsonb")] public required string Value { get; set; }
public required string ValueJson { get; set; }
} }

View File

@@ -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
}
}
}

View File

@@ -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: "");
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -56,9 +56,6 @@ namespace Moonlight.Api.Database.Migrations
b.Property<DateTimeOffset>("UpdatedAt") b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ValidUntil")
.HasColumnType("timestamp with time zone");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("ApiKeys", "core"); b.ToTable("ApiKeys", "core");
@@ -139,10 +136,10 @@ namespace Moonlight.Api.Database.Migrations
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<string>("ValueJson") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasMaxLength(4096) .HasMaxLength(4096)
.HasColumnType("jsonb"); .HasColumnType("character varying(4096)");
b.HasKey("Id"); b.HasKey("Id");

View File

@@ -1,16 +1,14 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Api.Implementations.ApiKeyScheme;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys; using Moonlight.Shared.Http.Responses.ApiKeys;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Http.Controllers.Admin;
@@ -20,12 +18,10 @@ namespace Moonlight.Api.Http.Controllers.Admin;
public class ApiKeyController : Controller public class ApiKeyController : Controller
{ {
private readonly DatabaseRepository<ApiKey> KeyRepository; private readonly DatabaseRepository<ApiKey> KeyRepository;
private readonly HybridCache HybridCache;
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache) public ApiKeyController(DatabaseRepository<ApiKey> keyRepository)
{ {
KeyRepository = keyRepository; KeyRepository = keyRepository;
HybridCache = hybridCache;
} }
[HttpGet] [HttpGet]
@@ -118,8 +114,6 @@ public class ApiKeyController : Controller
ApiKeyMapper.Merge(apiKey, request); ApiKeyMapper.Merge(apiKey, request);
await KeyRepository.UpdateAsync(apiKey); await KeyRepository.UpdateAsync(apiKey);
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
return ApiKeyMapper.ToDto(apiKey); return ApiKeyMapper.ToDto(apiKey);
} }
@@ -135,9 +129,6 @@ public class ApiKeyController : Controller
return Problem("No API key with this id found", statusCode: 404); return Problem("No API key with this id found", statusCode: 404);
await KeyRepository.RemoveAsync(apiKey); await KeyRepository.RemoveAsync(apiKey);
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
return NoContent(); return NoContent();
} }
} }

View File

@@ -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();
}
}

View File

@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin.Users; using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Http.Controllers.Admin;

View File

@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.Roles; using Moonlight.Shared.Http.Requests.Roles;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin;

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -7,11 +7,11 @@ using Moonlight.Api.Mappers;
using Moonlight.Api.Services; using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.Themes; using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Responses; 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] [ApiController]
[Route("api/admin/themes")] [Route("api/admin/themes")]

View File

@@ -6,9 +6,9 @@ using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Admin.Users; using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses; 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; namespace Moonlight.Api.Http.Controllers.Admin.Users;

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.Shared.Http.Responses.Admin.Auth; using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Api.Http.Controllers; namespace Moonlight.Api.Http.Controllers;

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Api.Services; using Moonlight.Api.Services;
using Moonlight.Shared.Http.Responses.Admin.Frontend; using Moonlight.Shared.Http.Responses.Frontend;
namespace Moonlight.Api.Http.Controllers; namespace Moonlight.Api.Http.Controllers;

View File

@@ -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");
}

View File

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

View File

@@ -1,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; }
}

View File

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

View File

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

View File

@@ -1,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
{
}

View File

@@ -2,7 +2,7 @@ using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Database; using Moonlight.Api.Database;
@@ -14,20 +14,18 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions> public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
{ {
private readonly DatabaseRepository<ApiKey> ApiKeyRepository; private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
private readonly HybridCache HybridCache; private readonly IMemoryCache MemoryCache;
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
public ApiKeySchemeHandler( public ApiKeySchemeHandler(
IOptionsMonitor<ApiKeySchemeOptions> options, IOptionsMonitor<ApiKeySchemeOptions> options,
ILoggerFactory logger, ILoggerFactory logger,
UrlEncoder encoder, UrlEncoder encoder,
DatabaseRepository<ApiKey> apiKeyRepository, DatabaseRepository<ApiKey> apiKeyRepository,
HybridCache hybridCache IMemoryCache memoryCache
) : base(options, logger, encoder) ) : base(options, logger, encoder)
{ {
ApiKeyRepository = apiKeyRepository; ApiKeyRepository = apiKeyRepository;
HybridCache = hybridCache; MemoryCache = memoryCache;
} }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
@@ -40,30 +38,24 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
if (authHeaderValue.Length > 32) if (authHeaderValue.Length > 32)
return AuthenticateResult.Fail("Invalid api key specified"); return AuthenticateResult.Fail("Invalid api key specified");
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?>( if (apiKey == null)
cacheKey, return AuthenticateResult.Fail("Invalid api key specified");
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) MemoryCache.Set(authHeaderValue, apiKey, Options.LookupCacheTime);
return AuthenticateResult.Fail("Invalid api key specified"); }
else
if (DateTimeOffset.UtcNow > apiKey.ValidUntil) {
return AuthenticateResult.Fail("Api key expired"); if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
}
return AuthenticateResult.Success(new AuthenticationTicket( return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal( new ClaimsPrincipal(
@@ -75,5 +67,5 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
)); ));
} }
private record ApiKeySession(string[] Permissions, DateTimeOffset ValidUntil); private record ApiKeySession(string[] Permissions);
} }

View File

@@ -4,6 +4,5 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeOptions : AuthenticationSchemeOptions public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{ {
public TimeSpan LookupL1CacheTime { get; set; } public TimeSpan LookupCacheTime { get; set; }
public TimeSpan LookupL2CacheTime { get; set; }
} }

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys; using Moonlight.Shared.Http.Responses.ApiKeys;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Mappers;

View File

@@ -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);
}

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models; using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Admin.Frontend; using Moonlight.Shared.Http.Responses.Frontend;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Mappers;

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Roles; using Moonlight.Shared.Http.Requests.Roles;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Themes; using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Responses.Admin.Themes; using Moonlight.Shared.Http.Responses.Themes;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Mappers;

View File

@@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses.Users;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Mappers;

View File

@@ -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; }
}

View File

@@ -25,14 +25,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/> <PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
<PackageReference Include="VYaml" Version="1.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -40,8 +35,4 @@
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible> <Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Http\Services\ContainerHelper\Responses\" />
</ItemGroup>
</Project> </Project>

View File

@@ -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)
{
}
}

View File

@@ -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}");
}
}

View File

@@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration; using Moonlight.Api.Configuration;
using Moonlight.Api.Constants;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Api.Models; using Moonlight.Api.Models;
@@ -14,16 +13,14 @@ public class FrontendService
private readonly IMemoryCache Cache; private readonly IMemoryCache Cache;
private readonly DatabaseRepository<Theme> ThemeRepository; private readonly DatabaseRepository<Theme> ThemeRepository;
private readonly IOptions<FrontendOptions> Options; private readonly IOptions<FrontendOptions> Options;
private readonly SettingsService SettingsService;
private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}"; private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}";
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options, SettingsService settingsService) public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options)
{ {
Cache = cache; Cache = cache;
ThemeRepository = themeRepository; ThemeRepository = themeRepository;
Options = options; Options = options;
SettingsService = settingsService;
} }
public async Task<FrontendConfiguration> GetConfigurationAsync() public async Task<FrontendConfiguration> GetConfigurationAsync()
@@ -38,9 +35,7 @@ public class FrontendService
.Query() .Query()
.FirstOrDefaultAsync(x => x.IsEnabled); .FirstOrDefaultAsync(x => x.IsEnabled);
var name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name); var config = new FrontendConfiguration("Moonlight", theme?.CssContent);
var config = new FrontendConfiguration(name ?? "Moonlight", theme?.CssContent);
Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes)); Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes));

View File

@@ -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);
}
}

View File

@@ -1,6 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration; using Moonlight.Api.Configuration;
@@ -14,10 +14,10 @@ namespace Moonlight.Api.Services;
public class UserAuthService public class UserAuthService
{ {
private readonly DatabaseRepository<User> UserRepository; private readonly DatabaseRepository<User> UserRepository;
private readonly IMemoryCache Cache;
private readonly ILogger<UserAuthService> Logger; private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<UserOptions> Options; private readonly IOptions<SessionOptions> Options;
private readonly IEnumerable<IUserAuthHook> Hooks; private readonly IEnumerable<IUserAuthHook> Hooks;
private readonly HybridCache HybridCache;
private const string UserIdClaim = "UserId"; private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt"; private const string IssuedAtClaim = "IssuedAt";
@@ -27,16 +27,15 @@ public class UserAuthService
public UserAuthService( public UserAuthService(
DatabaseRepository<User> userRepository, DatabaseRepository<User> userRepository,
ILogger<UserAuthService> logger, ILogger<UserAuthService> logger,
IOptions<UserOptions> options, IMemoryCache cache, IOptions<SessionOptions> options,
IEnumerable<IUserAuthHook> hooks, IEnumerable<IUserAuthHook> hooks
HybridCache hybridCache
) )
{ {
UserRepository = userRepository; UserRepository = userRepository;
Logger = logger; Logger = logger;
Cache = cache;
Options = options; Options = options;
Hooks = hooks; Hooks = hooks;
HybridCache = hybridCache;
} }
public async Task<bool> SyncAsync(ClaimsPrincipal? principal) public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
@@ -81,8 +80,8 @@ public class UserAuthService
foreach (var hook in Hooks) foreach (var hook in Hooks)
{ {
// Run every hook, and if any returns false, we return false as well // Run every hook and if any returns false we return false as well
if (!await hook.SyncAsync(principal, user)) if(!await hook.SyncAsync(principal, user))
return false; return false;
} }
@@ -102,29 +101,32 @@ public class UserAuthService
var cacheKey = string.Format(CacheKeyPattern, userId); var cacheKey = string.Format(CacheKeyPattern, userId);
var user = await HybridCache.GetOrCreateAsync<UserSession?>( if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
cacheKey, {
async ct => user = await UserRepository
{ .Query()
return await UserRepository .AsNoTracking()
.Query() .Where(u => u.Id == userId)
.AsNoTracking() .Select(u => new UserSession(
.Where(u => u.Id == userId) u.InvalidateTimestamp,
.Select(u => new UserSession( u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
u.InvalidateTimestamp, )
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray()) .FirstOrDefaultAsync();
)
.FirstOrDefaultAsync(cancellationToken: ct);
},
new HybridCacheEntryOptions()
{
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
Expiration = Options.Value.ValidationCacheL2Expiry
}
);
if (user == null) if (user == null)
return false; return false;
Cache.Set(
cacheKey,
user,
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
);
}
else
{
if (user == null)
return false;
}
var issuedAtString = principal.FindFirstValue(IssuedAtClaim); var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
@@ -144,11 +146,11 @@ public class UserAuthService
principal.Identities.First().AddClaims( principal.Identities.First().AddClaims(
user.Permissions.Select(x => new Claim(Permissions.ClaimType, x)) user.Permissions.Select(x => new Claim(Permissions.ClaimType, x))
); );
foreach (var hook in Hooks) foreach (var hook in Hooks)
{ {
// Run every hook, and if any returns false we return false as well // Run every hook and if any returns false we return false as well
if (!await hook.ValidateAsync(principal, userId)) if(!await hook.ValidateAsync(principal, userId))
return false; return false;
} }

View File

@@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Memory;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces; using Moonlight.Api.Interfaces;
@@ -10,17 +10,13 @@ public class UserDeletionService
{ {
private readonly DatabaseRepository<User> Repository; private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserDeletionHook> Hooks; private readonly IEnumerable<IUserDeletionHook> Hooks;
private readonly HybridCache HybridCache; private readonly IMemoryCache Cache;
public UserDeletionService( public UserDeletionService(DatabaseRepository<User> repository, IEnumerable<IUserDeletionHook> hooks, IMemoryCache cache)
DatabaseRepository<User> repository,
IEnumerable<IUserDeletionHook> hooks,
HybridCache hybridCache
)
{ {
Repository = repository; Repository = repository;
Hooks = hooks; Hooks = hooks;
HybridCache = hybridCache; Cache = cache;
} }
public async Task<UserDeletionValidationResult> ValidateAsync(int userId) public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
@@ -58,8 +54,7 @@ public class UserDeletionService
await hook.ExecuteAsync(user); await hook.ExecuteAsync(user);
await Repository.RemoveAsync(user); await Repository.RemoveAsync(user);
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
} }
} }

View File

@@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Memory;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces; using Moonlight.Api.Interfaces;
@@ -10,17 +10,17 @@ public class UserLogoutService
{ {
private readonly DatabaseRepository<User> Repository; private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserLogoutHook> Hooks; private readonly IEnumerable<IUserLogoutHook> Hooks;
private readonly HybridCache HybridCache; private readonly IMemoryCache Cache;
public UserLogoutService( public UserLogoutService(
DatabaseRepository<User> repository, DatabaseRepository<User> repository,
IEnumerable<IUserLogoutHook> hooks, IEnumerable<IUserLogoutHook> hooks,
HybridCache hybridCache IMemoryCache cache
) )
{ {
Repository = repository; Repository = repository;
Hooks = hooks; Hooks = hooks;
HybridCache = hybridCache; Cache = cache;
} }
public async Task LogoutAsync(int userId) public async Task LogoutAsync(int userId)
@@ -28,16 +28,16 @@ public class UserLogoutService
var user = await Repository var user = await Repository
.Query() .Query()
.FirstOrDefaultAsync(x => x.Id == userId); .FirstOrDefaultAsync(x => x.Id == userId);
if (user == null) if(user == null)
throw new AggregateException($"User with id {userId} not found"); throw new AggregateException($"User with id {userId} not found");
foreach (var hook in Hooks) foreach (var hook in Hooks)
await hook.ExecuteAsync(user); await hook.ExecuteAsync(user);
user.InvalidateTimestamp = DateTimeOffset.UtcNow; user.InvalidateTimestamp = DateTimeOffset.UtcNow;
await Repository.UpdateAsync(user); await Repository.UpdateAsync(user);
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id)); Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
} }
} }

View 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);
}

View File

@@ -17,26 +17,20 @@ public partial class Startup
{ {
private static void AddAuth(WebApplicationBuilder builder) private static void AddAuth(WebApplicationBuilder builder)
{ {
// OIDC
var oidcOptions = new OidcOptions(); var oidcOptions = new OidcOptions();
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions); builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
// API Key
var apiKeyOptions = new ApiOptions(); var apiKeyOptions = new ApiOptions();
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions); builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api"); builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
// Session
builder.Services.AddOptions<UserOptions>().BindConfiguration("Moonlight:User");
// Authentication builder.Services.AddScoped<UserAuthService>();
builder.Services.AddAuthentication("Main") builder.Services.AddAuthentication("Main")
.AddPolicyScheme("Main", null, .AddPolicyScheme("Main", null, options =>
options => {
{ options.ForwardDefaultSelector += context => context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
options.ForwardDefaultSelector += context => })
context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
})
.AddCookie("Session", null, options => .AddCookie("Session", null, options =>
{ {
options.Events.OnSigningIn += async context => options.Events.OnSigningIn += async context =>
@@ -80,16 +74,10 @@ public partial class Startup
options.Authority = oidcOptions.Authority; options.Authority = oidcOptions.Authority;
options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata; options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata;
if (oidcOptions.DisableHttpsOnlyCookies)
{
options.NonceCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
}
var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"]; var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
options.Scope.Clear(); options.Scope.Clear();
foreach (var scope in scopes) foreach (var scope in scopes)
options.Scope.Add(scope); options.Scope.Add(scope);
@@ -103,26 +91,15 @@ public partial class Startup
options.GetClaimsFromUserInfoEndpoint = true; options.GetClaimsFromUserInfoEndpoint = true;
}) })
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null, .AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null, options =>
options => {
{ options.LookupCacheTime = TimeSpan.FromMinutes(apiKeyOptions.LookupCacheMinutes);
options.LookupL1CacheTime = apiKeyOptions.LookupCacheL1Expiry; });
options.LookupL2CacheTime = apiKeyOptions.LookupCacheL2Expiry;
});
// Authorization
builder.Services.AddAuthorization();
// Reduce log noise
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning); builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
// Custom permission handling using named policies
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>(); builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>(); builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
builder.Services.AddScoped<UserDeletionService>();
builder.Services.AddScoped<UserLogoutService>();
builder.Services.AddScoped<UserAuthService>();
} }
private static void UseAuth(WebApplication application) private static void UseAuth(WebApplication application)

View File

@@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Logging.Console;
@@ -11,73 +9,43 @@ using Moonlight.Api.Helpers;
using Moonlight.Api.Implementations; using Moonlight.Api.Implementations;
using Moonlight.Api.Interfaces; using Moonlight.Api.Interfaces;
using Moonlight.Api.Services; using Moonlight.Api.Services;
using SessionOptions = Moonlight.Api.Configuration.SessionOptions;
namespace Moonlight.Api.Startup; namespace Moonlight.Api.Startup;
public partial class Startup public partial class Startup
{ {
private void AddBase(WebApplicationBuilder builder) private static void AddBase(WebApplicationBuilder builder)
{ {
// Create the base directory builder.Services.AddControllers().AddJsonOptions(options =>
Directory.CreateDirectory("storage"); {
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.ClearProviders();
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); }); builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>(); builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
// Application service
builder.Services.AddSingleton<ApplicationService>(); builder.Services.AddSingleton<ApplicationService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>()); builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
// Diagnose
builder.Services.AddSingleton<DiagnoseService>(); builder.Services.AddSingleton<DiagnoseService>();
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
// 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.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
builder.Services.AddScoped<FrontendService>(); builder.Services.AddScoped<FrontendService>();
// HTTP Client
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
// Version fetching configuration
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version"); builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
builder.Services.AddSingleton<VersionService>(); builder.Services.AddSingleton<VersionService>();
// Container Helper Options
builder.Configuration.GetSection("Moonlight:ContainerHelper").Bind(builder.Configuration);
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
builder.Services.AddSingleton<ContainerHelperService>();
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
{
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
client.BaseAddress =
new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
});
// User management services
builder.Services.AddScoped<UserDeletionService>(); builder.Services.AddScoped<UserDeletionService>();
builder.Services.AddScoped<UserLogoutService>(); builder.Services.AddScoped<UserLogoutService>();
// Settings options
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
builder.Services.AddScoped<SettingsService>();
// Setup key loading
var keysDirectory = new DirectoryInfo(Path.Combine("storage", "keys"));
builder.Services.AddDataProtection().PersistKeysToFileSystem(keysDirectory);
} }
private static void UseBase(WebApplication application) private static void UseBase(WebApplication application)
@@ -90,8 +58,8 @@ public partial class Startup
application.MapControllers(); application.MapControllers();
var options = application.Services.GetRequiredService<IOptions<FrontendOptions>>(); var options = application.Services.GetRequiredService<IOptions<FrontendOptions>>();
if (options.Value.Enabled) if(options.Value.Enabled)
application.MapFallbackToFile("index.html"); application.MapFallbackToFile("index.html");
} }
} }

View File

@@ -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:";
});
}
}

View File

@@ -1,29 +1,23 @@
using System.Reflection; using Microsoft.AspNetCore.Builder;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Moonlight.Shared.Http;
using SimplePlugin.Abstractions;
namespace Moonlight.Api.Startup; namespace Moonlight.Api.Startup;
[PluginModule] public partial class Startup : IAppStartup
public partial class Startup : MoonlightPlugin
{ {
public override void PreBuild(WebApplicationBuilder builder) public void PreBuild(WebApplicationBuilder builder)
{ {
AddBase(builder); AddBase(builder);
AddAuth(builder); AddAuth(builder);
AddDatabase(builder); AddDatabase(builder);
AddCache(builder);
} }
public override void PostBuild(WebApplication application) public void PostBuild(WebApplication application)
{ {
UseBase(application); UseBase(application);
UseAuth(application); UseAuth(application);
} }
public override void PostMiddleware(WebApplication application) public void PostMiddleware(WebApplication application)
{ {
MapBase(application); MapBase(application);
} }

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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
}

View File

@@ -1,8 +0,0 @@
using System.Reflection;
namespace Moonlight.Frontend.Configuration;
public class NavigationAssemblyOptions
{
public List<Assembly> Assemblies { get; private set; } = new();
}

View File

@@ -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
);

View 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;
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -28,8 +28,6 @@ public sealed class PermissionProvider : IPermissionProvider
new Permission(Permissions.System.Info, "Info", "View system info"), new Permission(Permissions.System.Info, "Info", "View system info"),
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"), new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"), new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"),
new Permission(Permissions.System.Instance, "Instance", "Update the moonlight instance and add plugins"),
new Permission(Permissions.System.Settings, "Settings", "Change settings of the instance"),
]), ]),
new PermissionCategory("API Keys", typeof(KeyIcon), [ new PermissionCategory("API Keys", typeof(KeyIcon), [
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"), new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),

View File

@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.Frontend.Interfaces;
public abstract class LayoutMiddlewareBase : ComponentBase
{
[Parameter] public RenderFragment ChildContent { get; set; }
}

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys; using Moonlight.Shared.Http.Responses.ApiKeys;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Frontend.Mappers; namespace Moonlight.Frontend.Mappers;

View File

@@ -1,5 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Admin.Roles; using Moonlight.Shared.Http.Requests.Roles;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Admin.Themes; using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Responses.Admin.Themes; using Moonlight.Shared.Http.Responses.Themes;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Frontend.Mappers; namespace Moonlight.Frontend.Mappers;

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Frontend.Mappers; namespace Moonlight.Frontend.Mappers;

View File

@@ -24,9 +24,8 @@
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/> <PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
<PackageReference Include="ShadcnBlazor" Version="1.0.13" /> <PackageReference Include="ShadcnBlazor" Version="1.0.9" />
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.13" /> <PackageReference Include="ShadcnBlazor.Extras" Version="1.0.9" />
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -36,6 +35,5 @@
<ItemGroup> <ItemGroup>
<None Include="Styles/*" Pack="true" PackagePath="Styles/" /> <None Include="Styles/*" Pack="true" PackagePath="Styles/" />
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\Moonlight.Frontend.targets" /> <None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\Moonlight.Frontend.targets" />
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="buildTransitive\Moonlight.Frontend.targets" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,7 +5,7 @@
</MoonlightCssClassDir> </MoonlightCssClassDir>
</PropertyGroup> </PropertyGroup>
<Target Name="Moonlight_CopyContents" BeforeTargets="Build"> <Target Name="CopyContents" BeforeTargets="Build">
<ItemGroup> <ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" /> <Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
</ItemGroup> </ItemGroup>

View File

@@ -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){}
}

View File

@@ -3,9 +3,7 @@ using System.Net.Http.Json;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic; using Moonlight.Shared.Http.Responses.Auth;
using Moonlight.Shared.Http;
using Moonlight.Shared.Http.Responses.Admin.Auth;
namespace Moonlight.Frontend.Services; namespace Moonlight.Frontend.Services;
@@ -25,7 +23,7 @@ public class RemoteAuthProvider : AuthenticationStateProvider
try try
{ {
var claimResponses = await HttpClient.GetFromJsonAsync<ClaimDto[]>( 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)); var claims = claimResponses!.Select(claim => new Claim(claim.Type, claim.Value));

View 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);
}

View File

@@ -1,13 +1,10 @@
using LucideBlazor;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moonlight.Frontend.Configuration;
using Moonlight.Frontend.Implementations; using Moonlight.Frontend.Implementations;
using Moonlight.Frontend.Interfaces; using Moonlight.Frontend.Interfaces;
using Moonlight.Frontend.Services; using Moonlight.Frontend.Services;
using Moonlight.Frontend.UI; using Moonlight.Frontend.UI;
using Moonlight.Frontend.UI.Admin.Settings;
using ShadcnBlazor; using ShadcnBlazor;
using ShadcnBlazor.Extras; using ShadcnBlazor.Extras;
@@ -28,19 +25,5 @@ public partial class Startup
builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>(); builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>();
builder.Services.AddScoped<FrontendService>(); builder.Services.AddScoped<FrontendService>();
builder.Services.Configure<NavigationAssemblyOptions>(options =>
{
options.Assemblies.Add(typeof(Startup).Assembly);
});
builder.Services.Configure<SystemSettingsOptions>(options =>
{
options.Add<TextCursorInputIcon, WhiteLabelingSetting>(
"White Labeling",
"Settings for white labeling your moonlight instance",
0
);
});
} }
} }

View File

@@ -1,18 +1,16 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using SimplePlugin.Abstractions;
namespace Moonlight.Frontend.Startup; namespace Moonlight.Frontend.Startup;
[PluginModule] public partial class Startup : IAppStartup
public partial class Startup : MoonlightPlugin
{ {
public override void PreBuild(WebAssemblyHostBuilder builder) public void PreBuild(WebAssemblyHostBuilder builder)
{ {
AddBase(builder); AddBase(builder);
AddAuth(builder); AddAuth(builder);
} }
public override void PostBuild(WebAssemblyHost application) public void PostBuild(WebAssemblyHost application)
{ {
} }

View File

@@ -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();
}
}

View File

@@ -1,19 +1,13 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Shared.Http.Requests.ApiKeys
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle>Create new API key</DialogTitle> <DialogTitle>Create new API key</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -21,80 +15,56 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync"> <FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
<div class="flex flex-col gap-6">
<FormValidationSummary />
<FieldGroup> <div class="grid gap-2">
<DataAnnotationsValidator/> <Label for="keyName">Name</Label>
<FormValidationSummary/> <InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
</div>
<FieldSet>
<Field> <div class="grid gap-2">
<FieldLabel for="keyName">Name</FieldLabel> <Label for="keyDescription">Description</Label>
<TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/> <textarea
</Field> @bind="Request.Description"
<Field> id="keyDescription"
<FieldLabel for="keyDescription">Description</FieldLabel> maxlength="100"
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/> 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"
</Field> placeholder="What this key is for">
<Field>
<FieldLabel for="keyValidUntil">Valid until</FieldLabel> </textarea>
<DateTimeInputField @bind-Value="Request.ValidUntil" id="keyValidUntil" /> </div>
</Field>
<Field> <div class="grid gap-2">
<FieldLabel>Permissions</FieldLabel> <Label>Permissions</Label>
<FieldContent> <PermissionSelector Permissions="Permissions" />
<PermissionSelector Permissions="Permissions"/> </div>
</FieldContent> </div>
</Field> </FormHandler>
</FieldSet>
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end"> <DialogFooter ClassName="justify-end gap-x-1">
<SubmitButton>Save changes</SubmitButton> <WButtom OnClick="() => FormHandler.SubmitAsync()">Save changes</WButtom>
</Field> </DialogFooter>
</FieldGroup>
</EnhancedEditForm>
@code @code
{ {
[Parameter] public Func<Task> OnSubmit { get; set; } [Parameter] public Func<CreateApiKeyDto, Task> OnSubmit { get; set; }
private CreateApiKeyDto Request; private CreateApiKeyDto Request;
private FormHandler FormHandler;
private List<string> Permissions = new(); private List<string> Permissions = new();
protected override void OnInitialized() protected override void OnInitialized()
{ {
Request = new() Request = new();
{
Permissions = [],
ValidUntil = DateTimeOffset.UtcNow
};
} }
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task SubmitAsync()
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
Request.ValidUntil = Request.ValidUntil.ToUniversalTime(); await OnSubmit.Invoke(Request);
var response = await HttpClient.PostAsJsonAsync(
"/api/admin/apiKeys",
Request,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"API Key creation",
$"Successfully created API key {Request.Name}"
);
await OnSubmit.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,18 +1,14 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http @using Moonlight.Shared.Http.Requests.Roles
@using Moonlight.Shared.Http.Requests.Admin.Roles @using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Create new role Create new role
@@ -22,43 +18,49 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync"> <FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
<FieldGroup> <div class="flex flex-col gap-6">
<FormValidationSummary/>
<DataAnnotationsValidator/>
<FieldSet> <FormValidationSummary/>
<Field>
<FieldLabel for="roleName">Name</FieldLabel> <div class="grid gap-2">
<TextInputField <Label for="roleName">Name</Label>
@bind-Value="Request.Name" <InputField
id="roleName" @bind-Value="Request.Name"
placeholder="My fancy role"/> id="roleName"
</Field> placeholder="My fancy role"/>
<Field> </div>
<FieldLabel for="keyDescription">Description</FieldLabel>
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" <div class="grid gap-2">
placeholder="Describe what the role should be used for"/> <Label for="roleDescription">Description</Label>
</Field> <textarea
<Field> @bind="Request.Description"
<FieldLabel>Permissions</FieldLabel> id="roleDescription"
<FieldContent> maxlength="100"
<PermissionSelector Permissions="Permissions"/> class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
</FieldContent> placeholder="Describe what the role should be used for">
</Field>
</FieldSet> </textarea>
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end"> </div>
<SubmitButton>Save changes</SubmitButton>
</Field> <div class="grid gap-2">
</FieldGroup> <Label>Permissions</Label>
</EnhancedEditForm> <PermissionSelector Permissions="Permissions" />
</div>
</div>
</FormHandler>
<DialogFooter ClassName="justify-end gap-x-1">
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<Task> OnSubmit { get; set; } [Parameter] public Func<CreateRoleDto, Task> OnSubmit { get; set; }
private CreateRoleDto Request; private CreateRoleDto Request;
private List<string> Permissions; private List<string> Permissions;
private FormHandler FormHandler;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@@ -66,31 +68,19 @@
{ {
Permissions = [] Permissions = []
}; };
Permissions = new(); Permissions = new();
} }
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task SubmitAsync()
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
await FormHandler.SubmitAsync();
var response = await HttpClient.PostAsJsonAsync( }
"api/admin/roles",
Request,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync("Role creation", $"Role {Request.Name} has been successfully created"); private async Task OnSubmitAsync()
{
await OnSubmit.Invoke(); await OnSubmit.Invoke(Request);
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,18 +1,12 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Shared.Http.Requests.Users
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Create new user Create new user
@@ -22,67 +16,50 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync"> <FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
<div class="flex flex-col gap-6">
<FieldGroup>
<FormValidationSummary/> <FormValidationSummary/>
<DataAnnotationsValidator />
<FieldSet> <div class="grid gap-2">
<Field> <Label for="username">Username</Label>
<FieldLabel for="username">Username</FieldLabel> <InputField
<TextInputField @bind-Value="Request.Username"
@bind-Value="Request.Username" id="username"
id="username" placeholder="Name of the user"/>
placeholder="Name of the user"/> </div>
</Field>
<Field> <div class="grid gap-2">
<FieldLabel for="emailAddress">Email Address</FieldLabel> <Label for="emailAddress">Email Address</Label>
<TextInputField <InputField
@bind-Value="Request.Email" @bind-Value="Request.Email"
id="emailAddress" id="emailAddress"
placeholder="email@of.user"/> Type="email"
</Field> placeholder="email@of.user"/>
</FieldSet> </div>
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end"> </div>
<SubmitButton>Save changes</SubmitButton> </FormHandler>
</Field>
</FieldGroup> <DialogFooter ClassName="justify-end gap-x-1">
</EnhancedEditForm> <WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<Task> OnCompleted { get; set; } [Parameter] public Func<CreateUserDto, Task> OnSubmit { get; set; }
private CreateUserDto Request; private CreateUserDto Request;
private FormHandler FormHandler;
protected override void OnInitialized() protected override void OnInitialized()
{ {
Request = new(); Request = new();
} }
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task SubmitAsync()
{ {
var response = await HttpClient.PostAsJsonAsync( await OnSubmit.Invoke(Request);
"/api/admin/users",
Request,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"User creation",
$"Successfully created user {Request.Username}"
);
await OnCompleted.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,7 +1,7 @@
@using LucideBlazor @using LucideBlazor
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using Moonlight.Shared.Http.Responses.Admin.Users @using Moonlight.Shared.Http.Responses.Users
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using ShadcnBlazor.DataGrids @using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@@ -32,9 +32,9 @@
SearchPlaceholder="Search user" SearchPlaceholder="Search user"
ValueSelector="dto => dto.Username" ValueSelector="dto => dto.Username"
Source="LoadUsersAsync"/> Source="LoadUsersAsync"/>
<WButton OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon"> <WButtom OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
<PlusIcon/> <PlusIcon/>
</WButton> </WButtom>
</div> </div>
</div> </div>
<div class="mt-3"> <div class="mt-3">
@@ -50,9 +50,9 @@
<CellTemplate> <CellTemplate>
<TableCell> <TableCell>
<div class="flex justify-end me-1.5"> <div class="flex justify-end me-1.5">
<WButton OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon"> <WButtom OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
<TrashIcon/> <TrashIcon/>
</WButton> </WButtom>
</div> </div>
</TableCell> </TableCell>
</CellTemplate> </CellTemplate>

View File

@@ -1,20 +1,15 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http @using Moonlight.Shared.Http.Requests.ApiKeys
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys @using Moonlight.Shared.Http.Responses.ApiKeys
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle>Update API key</DialogTitle> <DialogTitle>Update API key</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -22,43 +17,45 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync"> <FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
<FieldGroup> <div class="flex flex-col gap-6">
<FormValidationSummary/> <FormValidationSummary />
<DataAnnotationsValidator/>
<FieldSet> <div class="grid gap-2">
<Field> <Label for="keyName">Name</Label>
<FieldLabel for="keyName">Name</FieldLabel> <InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" />
<TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/> </div>
</Field>
<Field> <div class="grid gap-2">
<FieldLabel for="keyDescription">Description</FieldLabel> <Label for="keyDescription">Description</Label>
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/> <textarea
</Field> @bind="Request.Description"
<Field> id="keyDescription"
<FieldLabel for="keyValidUntil">Valid until</FieldLabel> maxlength="100"
<DateTimeInputField @bind-Value="Request.ValidUntil" id="keyValidUntil" /> 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"
</Field> placeholder="What this key is for">
<Field>
<FieldLabel>Permissions</FieldLabel> </textarea>
<FieldContent> </div>
<PermissionSelector Permissions="Permissions"/>
</FieldContent> <div class="grid gap-2">
</Field> <Label>Permissions</Label>
</FieldSet> <PermissionSelector Permissions="Permissions" />
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end"> </div>
<SubmitButton>Save changes</SubmitButton> </div>
</Field> </FormHandler>
</FieldGroup>
</EnhancedEditForm> <DialogFooter ClassName="justify-end gap-x-1">
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<Task> OnSubmit { get; set; } [Parameter] public Func<UpdateApiKeyDto, Task> OnSubmit { get; set; }
[Parameter] public ApiKeyDto Key { get; set; } [Parameter] public ApiKeyDto Key { get; set; }
private UpdateApiKeyDto Request; private UpdateApiKeyDto Request;
private FormHandler FormHandler;
private List<string> Permissions = new(); private List<string> Permissions = new();
protected override void OnInitialized() protected override void OnInitialized()
@@ -67,31 +64,10 @@
Permissions = Key.Permissions.ToList(); Permissions = Key.Permissions.ToList();
} }
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task SubmitAsync()
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
Request.ValidUntil = Request.ValidUntil.ToUniversalTime(); await OnSubmit.Invoke(Request);
var response = await HttpClient.PatchAsJsonAsync(
$"/api/admin/apiKeys/{Key.Id}",
Request,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"API Key update",
$"Successfully updated API key {Request.Name}"
);
await OnSubmit.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,38 +1,37 @@
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@using System.Text.Json
@using LucideBlazor @using LucideBlazor
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Events
@using Moonlight.Shared.Http.Requests.Admin.ContainerHelper
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Progresses @using ShadcnBlazor.Progresses
@using ShadcnBlazor.Spinners @using ShadcnBlazor.Spinners
@inject HttpClient HttpClient @inject AlertDialogService AlertService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Updating instance to @Version... Updating...
</DialogTitle> </DialogTitle>
</DialogHeader> </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-0.5">
<div class="text-base flex flex-col p-2 gap-y-1"> @for (var i = 0; i < Steps.Length; i++)
@for (var i = 0; i < Steps.Length; i++) {
if (CurrentStep == i)
{ {
if (CurrentStep == i) <div class="flex flex-row items-center gap-x-2">
<Spinner ClassName="size-4" />
<span>
@Steps[i]
</span>
</div>
}
else
{
if (i < CurrentStep)
{ {
<div class="flex flex-row items-center gap-x-1"> <div class="flex flex-row items-center gap-x-2">
@if (IsFailed) <CheckIcon ClassName="text-green-500 size-4" />
{
<CircleXIcon ClassName="text-red-500 size-5"/>
}
else
{
<Spinner ClassName="size-5"/>
}
<span> <span>
@Steps[i] @Steps[i]
</span> </span>
@@ -40,205 +39,81 @@
} }
else else
{ {
if (i < CurrentStep) <div class="text-muted-foreground flex flex-row items-center gap-x-2">
{ <span class="size-4"></span>
<div class="flex flex-row items-center gap-x-1 text-muted-foreground"> @Steps[i]
<CircleCheckIcon ClassName="text-green-500 size-5"/> </div>
<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> }
<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> </div>
@if (CurrentStep == Steps.Length || IsFailed) <DialogFooter>
{ <Progress Value="@Progress"></Progress>
<DialogFooter ClassName="justify-end"> </DialogFooter>
<Button Variant="ButtonVariant.Outline" @onclick="CloseAsync">Close</Button>
</DialogFooter>
}
else
{
<DialogFooter>
<Progress ClassName="my-1" Value="@Progress"></Progress>
</DialogFooter>
}
@code @code
{ {
[Parameter] public string Version { get; set; } private int Progress = 0;
[Parameter] public bool NoBuildCache { get; set; }
private bool IsFailed;
private int Progress;
private int CurrentStep; private int CurrentStep;
private readonly string[] Steps = private string[] Steps =
[ [
"Checking", // 0 "Preparing",
"Updating configuration files", // 1 "Updating configuration files",
"Starting rebuild task", // 2 "Building docker image",
"Building docker image", // 3 "Redeploying container instance",
"Redeploying container instance", // 4 "Waiting for container instance to start up",
"Waiting for container instance to start up", // 5 "Update complete"
"Update complete" // 6
]; ];
private readonly List<string?> LogLines = new();
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (!firstRender) if (!firstRender)
return; return;
// Checking
CurrentStep = 0; CurrentStep = 0;
Progress = 0; Progress = 0;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Task.Delay(2000); await Task.Delay(2000);
// Update configuration
CurrentStep = 1; CurrentStep = 1;
Progress = 20; Progress = 20;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto() await Task.Delay(6000);
{
Version = Version
}, SerializationContext.Default.Options);
// Starting rebuild task
CurrentStep = 2; CurrentStep = 2;
Progress = 30; Progress = 40;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
var request = new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild"); await Task.Delay(2000);
request.Content = JsonContent.Create( CurrentStep = 3;
new RequestRebuildDto(NoBuildCache), Progress = 60;
null, await InvokeAsync(StateHasChanged);
SerializationContext.Default.Options
);
var response = await HttpClient.SendAsync( await Task.Delay(4000);
request,
HttpCompletionOption.ResponseHeadersRead
);
await using var responseStream = await response.Content.ReadAsStreamAsync(); CurrentStep = 4;
using var streamReader = new StreamReader(responseStream); Progress = 80;
await InvokeAsync(StateHasChanged);
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
await Task.Delay(4000);
CurrentStep = 5; 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; Progress = 100;
await InvokeAsync(StateHasChanged); 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();
} }
} }

View File

@@ -1,20 +1,16 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http @using Moonlight.Shared.Http.Requests.Roles
@using Moonlight.Shared.Http.Requests.Admin.Roles
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Update @Role.Name Update @Role.Name
@@ -24,44 +20,50 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync"> <FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync">
<FieldGroup> <div class="flex flex-col gap-6">
<FormValidationSummary/>
<DataAnnotationsValidator/>
<FieldSet> <FormValidationSummary/>
<Field>
<FieldLabel for="roleName">Name</FieldLabel> <div class="grid gap-2">
<TextInputField <Label for="roleName">Name</Label>
@bind-Value="Request.Name" <InputField
id="roleName" @bind-Value="Request.Name"
placeholder="My fancy role"/> id="roleName"
</Field> placeholder="My fancy role"/>
<Field> </div>
<FieldLabel for="keyDescription">Description</FieldLabel>
<TextareaInputField @bind-Value="Request.Description" id="keyDescription" <div class="grid gap-2">
placeholder="Describe what the role should be used for"/> <Label for="roleDescription">Description</Label>
</Field> <textarea
<Field> @bind="Request.Description"
<FieldLabel>Permissions</FieldLabel> id="roleDescription"
<FieldContent> maxlength="100"
<PermissionSelector Permissions="Permissions"/> class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
</FieldContent> placeholder="Describe what the role should be used for">
</Field>
</FieldSet> </textarea>
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end"> </div>
<SubmitButton>Save changes</SubmitButton>
</Field> <div class="grid gap-2">
</FieldGroup> <Label>Permissions</Label>
</EnhancedEditForm> <PermissionSelector Permissions="Permissions" />
</div>
</div>
</FormHandler>
<DialogFooter ClassName="justify-end gap-x-1">
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<Task> OnSubmit { get; set; } [Parameter] public Func<UpdateRoleDto, Task> OnSubmit { get; set; }
[Parameter] public RoleDto Role { get; set; } [Parameter] public RoleDto Role { get; set; }
private UpdateRoleDto Request; private UpdateRoleDto Request;
private List<string> Permissions; private List<string> Permissions;
private FormHandler FormHandler;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@@ -69,27 +71,15 @@
Permissions = Role.Permissions.ToList(); Permissions = Role.Permissions.ToList();
} }
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task SubmitAsync()
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
await FormHandler.SubmitAsync();
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 ToastService.SuccessAsync("Role update", $"Role {Request.Name} has been successfully updated"); private async Task OnSubmitAsync()
{
await OnSubmit.Invoke(); await OnSubmit.Invoke(Request);
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,19 +1,14 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.Mappers @using Moonlight.Shared.Http.Requests.Users
@using Moonlight.Shared.Http.Requests.Admin.Users @using Moonlight.Shared.Http.Responses.Users
@using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin.Users
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.FormHandlers
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Update @User.Username Update @User.Username
@@ -23,66 +18,51 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync"> <FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync">
<FieldGroup> <div class="flex flex-col gap-6">
<FormValidationSummary/>
<DataAnnotationsValidator/>
<FieldSet> <FormValidationSummary/>
<Field>
<FieldLabel for="username">Username</FieldLabel> <div class="grid gap-2">
<TextInputField <Label for="username">Username</Label>
@bind-Value="Request.Username" <InputField
id="username" @bind-Value="Request.Username"
placeholder="Name of the user"/> id="username"
</Field> placeholder="Name of the user"/>
<Field> </div>
<FieldLabel for="emailAddress">Email Address</FieldLabel>
<TextInputField <div class="grid gap-2">
@bind-Value="Request.Email" <Label for="emailAddress">Email Address</Label>
id="emailAddress" <InputField
placeholder="email@of.user"/> @bind-Value="Request.Email"
</Field> id="emailAddress"
</FieldSet> Type="email"
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end"> placeholder="email@of.user"/>
<SubmitButton>Save changes</SubmitButton> </div>
</Field> </div>
</FieldGroup> </FormHandler>
</EnhancedEditForm>
<DialogFooter ClassName="justify-end gap-x-1">
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<Task> OnCompleted { get; set; } [Parameter] public Func<UpdateUserDto, Task> OnSubmit { get; set; }
[Parameter] public UserDto User { get; set; } [Parameter] public UserDto User { get; set; }
private UpdateUserDto Request; private UpdateUserDto Request;
private FormHandler FormHandler;
protected override void OnInitialized() protected override void OnInitialized()
{ {
Request = UserMapper.ToUpdate(User); Request = UserMapper.ToUpdate(User);
} }
private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore) private async Task SubmitAsync()
{ {
var response = await HttpClient.PatchAsJsonAsync( await OnSubmit.Invoke(Request);
$"/api/admin/users/{User.Id}",
Request
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"User update",
$"Successfully updated user {Request.Username}"
);
await OnCompleted.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -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;
}
}

View File

@@ -1,7 +1,6 @@
@page "/admin" @page "/admin"
@using LucideBlazor @using LucideBlazor
@using Moonlight.Frontend.UI.Admin.Modals @using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using ShadcnBlazor.Cards @using ShadcnBlazor.Cards
@@ -134,11 +133,7 @@
{ {
<CardTitle ClassName="text-lg text-primary">Update available</CardTitle> <CardTitle ClassName="text-lg text-primary">Update available</CardTitle>
<CardAction ClassName="self-center"> <CardAction ClassName="self-center">
<Button> <Button @onclick="LaunchUpdateModalAsync">Update</Button>
<Slot>
<a href="/admin/system?tab=instance" @attributes="context">Update</a>
</Slot>
</Button>
</CardAction> </CardAction>
} }
</CardHeader> </CardHeader>
@@ -156,9 +151,14 @@
if(!firstRender) if(!firstRender)
return; 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; IsInfoLoading = false;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync<UpdateInstanceModal>(onConfigure: model =>
{
model.ShowCloseButton = false;
});
} }

View File

@@ -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.Authorization
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Frontend.UI.Admin.Modals @using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared @using Moonlight.Shared
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using ShadcnBlazor.DataGrids @using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Dropdowns @using ShadcnBlazor.Dropdowns
@using ShadcnBlazor.Extras.AlertDialogs @using ShadcnBlazor.Extras.AlertDialogs
@@ -49,21 +49,7 @@
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<PropertyColumn IsFilterable="true" <PropertyColumn IsFilterable="true"
Identifier="@nameof(ApiKeyDto.Description)" Field="k => k.Description" HeadClassName="hidden lg:table-cell" CellClassName="hidden lg:table-cell" /> Identifier="@nameof(ApiKeyDto.Description)" Field="k => k.Description"/>
<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>
<TemplateColumn> <TemplateColumn>
<CellTemplate> <CellTemplate>
<TableCell> <TableCell>
@@ -127,7 +113,7 @@
var response = await HttpClient.GetFromJsonAsync<PagedData<ApiKeyDto>>( var response = await HttpClient.GetFromJsonAsync<PagedData<ApiKeyDto>>(
$"api/admin/apiKeys{query}&filterOptions={filterOptions}", $"api/admin/apiKeys{query}&filterOptions={filterOptions}",
SerializationContext.Default.Options Constants.SerializerOptions
); );
return new DataGridResponse<ApiKeyDto>(response!.Data, response.TotalLength); return new DataGridResponse<ApiKeyDto>(response!.Data, response.TotalLength);
@@ -137,8 +123,19 @@
{ {
await DialogService.LaunchAsync<CreateApiKeyDialog>(parameters => 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(); await Grid.RefreshAsync();
}; };
}); });
@@ -149,8 +146,19 @@
await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters => await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters =>
{ {
parameters[nameof(UpdateApiKeyDialog.Key)] = key; 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(); await Grid.RefreshAsync();
}; };
}); });

View File

@@ -44,10 +44,10 @@
</Alert> </Alert>
</CardContent> </CardContent>
<CardFooter ClassName="justify-end"> <CardFooter ClassName="justify-end">
<WButton OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)"> <WButtom OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)">
<StethoscopeIcon/> <StethoscopeIcon/>
Start diagnostics Start diagnostics
</WButton> </WButtom>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>

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