Merge pull request #457

Updated dependencies. Refactored to async scheme
This commit is contained in:
2025-09-21 19:24:11 +02:00
committed by GitHub
101 changed files with 1115 additions and 2409 deletions

View File

@@ -10,15 +10,15 @@ await startup.Run(args, pluginLoader.Instances);
var cs = new Startup(); var cs = new Startup();
await cs.Initialize(args, pluginLoader.Instances); await cs.InitializeAsync(args, pluginLoader.Instances);
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
await cs.AddMoonlight(builder); await cs.AddMoonlightAsync(builder);
var app = builder.Build(); var app = builder.Build();
await cs.AddMoonlight(app); await cs.AddMoonlightAsync(app);
// Handle setup of wasm app hosting in the runtime // Handle setup of wasm app hosting in the runtime
// so the Moonlight.ApiServer doesn't need the wasm package // so the Moonlight.ApiServer doesn't need the wasm package

View File

@@ -62,7 +62,7 @@ public record AppConfiguration
public record FilesData public record FilesData
{ {
[YamlMember(Description = "The maximum file size limit a combine operation is allowed to process")] [YamlMember(Description = "The maximum file size limit a combine operation is allowed to process")]
public long CombineLimit { get; set; } = ByteConverter.FromGigaBytes(5).MegaBytes; public double CombineLimit { get; set; } = ByteConverter.FromGigaBytes(5).MegaBytes;
} }
public record FrontendData public record FrontendData

View File

@@ -1,34 +1,47 @@
using Hangfire.EntityFrameworkCore; using Hangfire.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MoonCore.Extended.SingleDb;
using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Models; using Moonlight.ApiServer.Models;
namespace Moonlight.ApiServer.Database; namespace Moonlight.ApiServer.Database;
public class CoreDataContext : DatabaseContext public class CoreDataContext : DbContext
{ {
public override string Prefix { get; } = "Core"; private readonly AppConfiguration Configuration;
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; } public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Theme> Themes { get; set; } public DbSet<Theme> Themes { get; set; }
public CoreDataContext(AppConfiguration configuration) public CoreDataContext(AppConfiguration configuration)
{ {
Options = new() Configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(optionsBuilder.IsConfigured)
return;
var database = Configuration.Database;
var connectionString = $"Host={database.Host};" +
$"Port={database.Port};" +
$"Database={database.Database};" +
$"Username={database.Username};" +
$"Password={database.Password}";
optionsBuilder.UseNpgsql(connectionString, builder =>
{ {
Host = configuration.Database.Host, builder.MigrationsHistoryTable("MigrationsHistory", "core");
Port = configuration.Database.Port, });
Username = configuration.Database.Username,
Password = configuration.Database.Password,
Database = configuration.Database.Database
};
} }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Model.SetDefaultSchema("core");
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.OnHangfireModelCreating(); modelBuilder.OnHangfireModelCreating();

View File

@@ -1,90 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
[DbContext(typeof(CoreDataContext))]
[Migration("20250226080942_AddedUsersAndApiKey")]
partial class AddedUsersAndApiKey
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Secret")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,59 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedUsersAndApiKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Core_ApiKeys",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Secret = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
PermissionsJson = table.Column<string>(type: "jsonb", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Core_ApiKeys", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Core_Users",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false),
TokenValidTimestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
PermissionsJson = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Core_Users", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Core_ApiKeys");
migrationBuilder.DropTable(
name: "Core_Users");
}
}
}

View File

@@ -1,89 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
[DbContext(typeof(CoreDataContext))]
[Migration("20250314095412_ModifiedApiKeyEntity")]
partial class ModifiedApiKeyEntity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,41 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class ModifiedApiKeyEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Secret",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Core_ApiKeys",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<string>(
name: "Secret",
table: "Core_ApiKeys",
type: "text",
nullable: false,
defaultValue: "");
}
}
}

View File

@@ -1,393 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
[DbContext(typeof(CoreDataContext))]
[Migration("20250405172522_AddedHangfireTables")]
partial class AddedHangfireTables
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long>("Value")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Value");
b.ToTable("HangfireCounter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Field")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Field");
b.HasIndex("ExpireAt");
b.ToTable("HangfireHash");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvocationData")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("StateId")
.HasColumnType("bigint");
b.Property<string>("StateName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("StateId");
b.HasIndex("StateName");
b.ToTable("HangfireJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("JobId", "Name");
b.ToTable("HangfireJobParameter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Position");
b.HasIndex("ExpireAt");
b.ToTable("HangfireList");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("AcquiredAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("HangfireLock");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("FetchedAt")
.IsConcurrencyToken()
.HasColumnType("timestamp with time zone");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Queue")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("Queue", "FetchedAt");
b.ToTable("HangfireQueuedJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("Heartbeat")
.HasColumnType("timestamp with time zone");
b.Property<string>("Queues")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("WorkerCount")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Heartbeat");
b.ToTable("HangfireServer");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
{
b.Property<string>("Key")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Value")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<double>("Score")
.HasColumnType("double precision");
b.HasKey("Key", "Value");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Score");
b.ToTable("HangfireSet");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("text");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Reason")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("JobId");
b.ToTable("HangfireState");
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State")
.WithMany()
.HasForeignKey("StateId");
b.Navigation("State");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("Parameters")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("QueuedJobs")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("States")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Navigation("Parameters");
b.Navigation("QueuedJobs");
b.Navigation("States");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,393 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.ApiServer.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
[DbContext(typeof(CoreDataContext))]
[Migration("20250712202608_SwitchedToPgArraysForPermissions")]
partial class SwitchedToPgArraysForPermissions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long>("Value")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Value");
b.ToTable("HangfireCounter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Field")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Field");
b.HasIndex("ExpireAt");
b.ToTable("HangfireHash");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvocationData")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("StateId")
.HasColumnType("bigint");
b.Property<string>("StateName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("ExpireAt");
b.HasIndex("StateId");
b.HasIndex("StateName");
b.ToTable("HangfireJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("JobId", "Name");
b.ToTable("HangfireJobParameter");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
{
b.Property<string>("Key")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key", "Position");
b.HasIndex("ExpireAt");
b.ToTable("HangfireList");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("AcquiredAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("HangfireLock");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("FetchedAt")
.IsConcurrencyToken()
.HasColumnType("timestamp with time zone");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Queue")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("Queue", "FetchedAt");
b.ToTable("HangfireQueuedJob");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("Heartbeat")
.HasColumnType("timestamp with time zone");
b.Property<string>("Queues")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("WorkerCount")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Heartbeat");
b.ToTable("HangfireServer");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
{
b.Property<string>("Key")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Value")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<double>("Score")
.HasColumnType("double precision");
b.HasKey("Key", "Value");
b.HasIndex("ExpireAt");
b.HasIndex("Key", "Score");
b.ToTable("HangfireSet");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("text");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Reason")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("JobId");
b.ToTable("HangfireState");
});
modelBuilder.Entity("Moonlight.ApiServer.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()
.HasColumnType("text");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null);
});
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.PrimitiveCollection<string[]>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("TokenValidTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Core_Users", (string)null);
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State")
.WithMany()
.HasForeignKey("StateId");
b.Navigation("State");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("Parameters")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("QueuedJobs")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
{
b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job")
.WithMany("States")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Job");
});
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
{
b.Navigation("Parameters");
b.Navigation("QueuedJobs");
b.Navigation("States");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,62 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class SwitchedToPgArraysForPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PermissionsJson",
table: "Core_Users");
migrationBuilder.DropColumn(
name: "PermissionsJson",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<string[]>(
name: "Permissions",
table: "Core_Users",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "Permissions",
table: "Core_ApiKeys",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Permissions",
table: "Core_Users");
migrationBuilder.DropColumn(
name: "Permissions",
table: "Core_ApiKeys");
migrationBuilder.AddColumn<string>(
name: "PermissionsJson",
table: "Core_Users",
type: "jsonb",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "PermissionsJson",
table: "Core_ApiKeys",
type: "jsonb",
nullable: false,
defaultValue: "");
}
}
}

View File

@@ -1,41 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedThemes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Core_Themes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Author = table.Column<string>(type: "text", nullable: false),
Version = table.Column<string>(type: "text", nullable: false),
UpdateUrl = table.Column<string>(type: "text", nullable: true),
DonateUrl = table.Column<string>(type: "text", nullable: true),
Content = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Core_Themes", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Core_Themes");
}
}
}

View File

@@ -12,15 +12,16 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Moonlight.ApiServer.Database.Migrations namespace Moonlight.ApiServer.Database.Migrations
{ {
[DbContext(typeof(CoreDataContext))] [DbContext(typeof(CoreDataContext))]
[Migration("20250720203346_AddedThemes")] [Migration("20250919201409_RecreatedMigrationsForChangeOfSchema")]
partial class AddedThemes partial class RecreatedMigrationsForChangeOfSchema
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.7") .HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -50,7 +51,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("Key", "Value"); b.HasIndex("Key", "Value");
b.ToTable("HangfireCounter"); b.ToTable("HangfireCounter", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
@@ -73,7 +74,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("ExpireAt"); b.HasIndex("ExpireAt");
b.ToTable("HangfireHash"); b.ToTable("HangfireHash", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
@@ -109,7 +110,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("StateName"); b.HasIndex("StateName");
b.ToTable("HangfireJob"); b.ToTable("HangfireJob", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
@@ -126,7 +127,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("JobId", "Name"); b.HasKey("JobId", "Name");
b.ToTable("HangfireJobParameter"); b.ToTable("HangfireJobParameter", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
@@ -148,7 +149,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("ExpireAt"); b.HasIndex("ExpireAt");
b.ToTable("HangfireList"); b.ToTable("HangfireList", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
@@ -162,7 +163,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("HangfireLock"); b.ToTable("HangfireLock", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
@@ -191,7 +192,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("Queue", "FetchedAt"); b.HasIndex("Queue", "FetchedAt");
b.ToTable("HangfireQueuedJob"); b.ToTable("HangfireQueuedJob", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
@@ -217,7 +218,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("Heartbeat"); b.HasIndex("Heartbeat");
b.ToTable("HangfireServer"); b.ToTable("HangfireServer", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
@@ -242,7 +243,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("Key", "Score"); b.HasIndex("Key", "Score");
b.ToTable("HangfireSet"); b.ToTable("HangfireSet", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
@@ -275,7 +276,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("JobId"); b.HasIndex("JobId");
b.ToTable("HangfireState"); b.ToTable("HangfireState", "core");
}); });
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b => modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
@@ -302,7 +303,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null); b.ToTable("ApiKeys", "core");
}); });
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b => modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
@@ -336,7 +337,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Core_Themes", (string)null); b.ToTable("Themes", "core");
}); });
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b => modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
@@ -368,7 +369,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Core_Users", (string)null); b.ToTable("Users", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
@@ -515,11 +516,11 @@ namespace Moonlight.ApiServer.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b1.Property<float>("Depth") b1.Property<int>("Depth")
.HasColumnType("real"); .HasColumnType("integer");
b1.Property<float>("Noise") b1.Property<int>("Noise")
.HasColumnType("real"); .HasColumnType("integer");
b1.Property<float>("RadiusBox") b1.Property<float>("RadiusBox")
.HasColumnType("real"); .HasColumnType("real");
@@ -538,7 +539,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b1.HasKey("ThemeId"); b1.HasKey("ThemeId");
b1.ToTable("Core_Themes"); b1.ToTable("Themes", "core");
b1.ToJson("Content"); b1.ToJson("Content");

View File

@@ -7,13 +7,34 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Moonlight.ApiServer.Database.Migrations namespace Moonlight.ApiServer.Database.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class AddedHangfireTables : Migration public partial class RecreatedMigrationsForChangeOfSchema : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.EnsureSchema(
name: "core");
migrationBuilder.CreateTable(
name: "ApiKeys",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Description = table.Column<string>(type: "text", nullable: false),
Permissions = table.Column<string[]>(type: "text[]", nullable: false),
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiKeys", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireCounter", name: "HangfireCounter",
schema: "core",
columns: table => new columns: table => new
{ {
Id = table.Column<long>(type: "bigint", nullable: false) Id = table.Column<long>(type: "bigint", nullable: false)
@@ -29,6 +50,7 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireHash", name: "HangfireHash",
schema: "core",
columns: table => new columns: table => new
{ {
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
@@ -43,6 +65,7 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireList", name: "HangfireList",
schema: "core",
columns: table => new columns: table => new
{ {
Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Key = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
@@ -57,6 +80,7 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireLock", name: "HangfireLock",
schema: "core",
columns: table => new columns: table => new
{ {
Id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
@@ -69,6 +93,7 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireServer", name: "HangfireServer",
schema: "core",
columns: table => new columns: table => new
{ {
Id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
@@ -84,6 +109,7 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireSet", name: "HangfireSet",
schema: "core",
columns: table => new columns: table => new
{ {
Key = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), Key = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
@@ -96,8 +122,47 @@ namespace Moonlight.ApiServer.Database.Migrations
table.PrimaryKey("PK_HangfireSet", x => new { x.Key, x.Value }); table.PrimaryKey("PK_HangfireSet", x => new { x.Key, x.Value });
}); });
migrationBuilder.CreateTable(
name: "Themes",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Author = table.Column<string>(type: "text", nullable: false),
Version = table.Column<string>(type: "text", nullable: false),
UpdateUrl = table.Column<string>(type: "text", nullable: true),
DonateUrl = table.Column<string>(type: "text", nullable: true),
Content = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Themes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
schema: "core",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false),
TokenValidTimestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
Permissions = table.Column<string[]>(type: "text[]", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireJob", name: "HangfireJob",
schema: "core",
columns: table => new columns: table => new
{ {
Id = table.Column<long>(type: "bigint", nullable: false) Id = table.Column<long>(type: "bigint", nullable: false)
@@ -115,6 +180,7 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireJobParameter", name: "HangfireJobParameter",
schema: "core",
columns: table => new columns: table => new
{ {
JobId = table.Column<long>(type: "bigint", nullable: false), JobId = table.Column<long>(type: "bigint", nullable: false),
@@ -127,6 +193,7 @@ namespace Moonlight.ApiServer.Database.Migrations
table.ForeignKey( table.ForeignKey(
name: "FK_HangfireJobParameter_HangfireJob_JobId", name: "FK_HangfireJobParameter_HangfireJob_JobId",
column: x => x.JobId, column: x => x.JobId,
principalSchema: "core",
principalTable: "HangfireJob", principalTable: "HangfireJob",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@@ -134,6 +201,7 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireQueuedJob", name: "HangfireQueuedJob",
schema: "core",
columns: table => new columns: table => new
{ {
Id = table.Column<long>(type: "bigint", nullable: false) Id = table.Column<long>(type: "bigint", nullable: false)
@@ -148,6 +216,7 @@ namespace Moonlight.ApiServer.Database.Migrations
table.ForeignKey( table.ForeignKey(
name: "FK_HangfireQueuedJob_HangfireJob_JobId", name: "FK_HangfireQueuedJob_HangfireJob_JobId",
column: x => x.JobId, column: x => x.JobId,
principalSchema: "core",
principalTable: "HangfireJob", principalTable: "HangfireJob",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@@ -155,6 +224,7 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "HangfireState", name: "HangfireState",
schema: "core",
columns: table => new columns: table => new
{ {
Id = table.Column<long>(type: "bigint", nullable: false) Id = table.Column<long>(type: "bigint", nullable: false)
@@ -171,6 +241,7 @@ namespace Moonlight.ApiServer.Database.Migrations
table.ForeignKey( table.ForeignKey(
name: "FK_HangfireState_HangfireJob_JobId", name: "FK_HangfireState_HangfireJob_JobId",
column: x => x.JobId, column: x => x.JobId,
principalSchema: "core",
principalTable: "HangfireJob", principalTable: "HangfireJob",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@@ -178,73 +249,88 @@ namespace Moonlight.ApiServer.Database.Migrations
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireCounter_ExpireAt", name: "IX_HangfireCounter_ExpireAt",
schema: "core",
table: "HangfireCounter", table: "HangfireCounter",
column: "ExpireAt"); column: "ExpireAt");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireCounter_Key_Value", name: "IX_HangfireCounter_Key_Value",
schema: "core",
table: "HangfireCounter", table: "HangfireCounter",
columns: new[] { "Key", "Value" }); columns: new[] { "Key", "Value" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireHash_ExpireAt", name: "IX_HangfireHash_ExpireAt",
schema: "core",
table: "HangfireHash", table: "HangfireHash",
column: "ExpireAt"); column: "ExpireAt");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireJob_ExpireAt", name: "IX_HangfireJob_ExpireAt",
schema: "core",
table: "HangfireJob", table: "HangfireJob",
column: "ExpireAt"); column: "ExpireAt");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireJob_StateId", name: "IX_HangfireJob_StateId",
schema: "core",
table: "HangfireJob", table: "HangfireJob",
column: "StateId"); column: "StateId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireJob_StateName", name: "IX_HangfireJob_StateName",
schema: "core",
table: "HangfireJob", table: "HangfireJob",
column: "StateName"); column: "StateName");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireList_ExpireAt", name: "IX_HangfireList_ExpireAt",
schema: "core",
table: "HangfireList", table: "HangfireList",
column: "ExpireAt"); column: "ExpireAt");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireQueuedJob_JobId", name: "IX_HangfireQueuedJob_JobId",
schema: "core",
table: "HangfireQueuedJob", table: "HangfireQueuedJob",
column: "JobId"); column: "JobId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireQueuedJob_Queue_FetchedAt", name: "IX_HangfireQueuedJob_Queue_FetchedAt",
schema: "core",
table: "HangfireQueuedJob", table: "HangfireQueuedJob",
columns: new[] { "Queue", "FetchedAt" }); columns: new[] { "Queue", "FetchedAt" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireServer_Heartbeat", name: "IX_HangfireServer_Heartbeat",
schema: "core",
table: "HangfireServer", table: "HangfireServer",
column: "Heartbeat"); column: "Heartbeat");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireSet_ExpireAt", name: "IX_HangfireSet_ExpireAt",
schema: "core",
table: "HangfireSet", table: "HangfireSet",
column: "ExpireAt"); column: "ExpireAt");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireSet_Key_Score", name: "IX_HangfireSet_Key_Score",
schema: "core",
table: "HangfireSet", table: "HangfireSet",
columns: new[] { "Key", "Score" }); columns: new[] { "Key", "Score" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_HangfireState_JobId", name: "IX_HangfireState_JobId",
schema: "core",
table: "HangfireState", table: "HangfireState",
column: "JobId"); column: "JobId");
migrationBuilder.AddForeignKey( migrationBuilder.AddForeignKey(
name: "FK_HangfireJob_HangfireState_StateId", name: "FK_HangfireJob_HangfireState_StateId",
schema: "core",
table: "HangfireJob", table: "HangfireJob",
column: "StateId", column: "StateId",
principalSchema: "core",
principalTable: "HangfireState", principalTable: "HangfireState",
principalColumn: "Id"); principalColumn: "Id");
} }
@@ -254,37 +340,60 @@ namespace Moonlight.ApiServer.Database.Migrations
{ {
migrationBuilder.DropForeignKey( migrationBuilder.DropForeignKey(
name: "FK_HangfireJob_HangfireState_StateId", name: "FK_HangfireJob_HangfireState_StateId",
schema: "core",
table: "HangfireJob"); table: "HangfireJob");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireCounter"); name: "ApiKeys",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireHash"); name: "HangfireCounter",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireJobParameter"); name: "HangfireHash",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireList"); name: "HangfireJobParameter",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireLock"); name: "HangfireList",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireQueuedJob"); name: "HangfireLock",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireServer"); name: "HangfireQueuedJob",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireSet"); name: "HangfireServer",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireState"); name: "HangfireSet",
schema: "core");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "HangfireJob"); name: "Themes",
schema: "core");
migrationBuilder.DropTable(
name: "Users",
schema: "core");
migrationBuilder.DropTable(
name: "HangfireState",
schema: "core");
migrationBuilder.DropTable(
name: "HangfireJob",
schema: "core");
} }
} }
} }

View File

@@ -17,7 +17,8 @@ namespace Moonlight.ApiServer.Database.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.7") .HasDefaultSchema("core")
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -47,7 +48,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("Key", "Value"); b.HasIndex("Key", "Value");
b.ToTable("HangfireCounter"); b.ToTable("HangfireCounter", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b =>
@@ -70,7 +71,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("ExpireAt"); b.HasIndex("ExpireAt");
b.ToTable("HangfireHash"); b.ToTable("HangfireHash", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
@@ -106,7 +107,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("StateName"); b.HasIndex("StateName");
b.ToTable("HangfireJob"); b.ToTable("HangfireJob", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b =>
@@ -123,7 +124,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("JobId", "Name"); b.HasKey("JobId", "Name");
b.ToTable("HangfireJobParameter"); b.ToTable("HangfireJobParameter", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b =>
@@ -145,7 +146,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("ExpireAt"); b.HasIndex("ExpireAt");
b.ToTable("HangfireList"); b.ToTable("HangfireList", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b =>
@@ -159,7 +160,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("HangfireLock"); b.ToTable("HangfireLock", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b =>
@@ -188,7 +189,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("Queue", "FetchedAt"); b.HasIndex("Queue", "FetchedAt");
b.ToTable("HangfireQueuedJob"); b.ToTable("HangfireQueuedJob", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b =>
@@ -214,7 +215,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("Heartbeat"); b.HasIndex("Heartbeat");
b.ToTable("HangfireServer"); b.ToTable("HangfireServer", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b =>
@@ -239,7 +240,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("Key", "Score"); b.HasIndex("Key", "Score");
b.ToTable("HangfireSet"); b.ToTable("HangfireSet", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b =>
@@ -272,7 +273,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasIndex("JobId"); b.HasIndex("JobId");
b.ToTable("HangfireState"); b.ToTable("HangfireState", "core");
}); });
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b => modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b =>
@@ -299,7 +300,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Core_ApiKeys", (string)null); b.ToTable("ApiKeys", "core");
}); });
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b => modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.Theme", b =>
@@ -333,7 +334,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Core_Themes", (string)null); b.ToTable("Themes", "core");
}); });
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b => modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
@@ -365,7 +366,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Core_Users", (string)null); b.ToTable("Users", "core");
}); });
modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b =>
@@ -512,11 +513,11 @@ namespace Moonlight.ApiServer.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b1.Property<float>("Depth") b1.Property<int>("Depth")
.HasColumnType("real"); .HasColumnType("integer");
b1.Property<float>("Noise") b1.Property<int>("Noise")
.HasColumnType("real"); .HasColumnType("integer");
b1.Property<float>("RadiusBox") b1.Property<float>("RadiusBox")
.HasColumnType("real"); .HasColumnType("real");
@@ -535,7 +536,7 @@ namespace Moonlight.ApiServer.Database.Migrations
b1.HasKey("ThemeId"); b1.HasKey("ThemeId");
b1.ToTable("Core_Themes"); b1.ToTable("Themes", "core");
b1.ToJson("Content"); b1.ToJson("Content");

View File

@@ -1,12 +1,10 @@
using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions; using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Models;
using MoonCore.Models; using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Mappers;
using Moonlight.ApiServer.Services; using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys; using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
@@ -28,69 +26,82 @@ public class ApiKeysController : Controller
[HttpGet] [HttpGet]
[Authorize(Policy = "permissions:admin.apikeys.get")] [Authorize(Policy = "permissions:admin.apikeys.get")]
public async Task<IPagedData<ApiKeyResponse>> Get([FromQuery] PagedOptions options) public async Task<ActionResult<ICountedData<ApiKeyResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count,
[FromQuery] string? orderBy,
[FromQuery] string? filter,
[FromQuery] string orderByDir = "asc"
)
{ {
var count = await ApiKeyRepository.Get().CountAsync(); if (count > 100)
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
IQueryable<ApiKey> query = ApiKeyRepository.Get();
var apiKeys = await ApiKeyRepository query = orderBy switch
.Get() {
.OrderBy(x => x.Id) nameof(ApiKey.Id) => orderByDir == "desc"
.Skip(options.Page * options.PageSize) ? query.OrderByDescending(x => x.Id)
.Take(options.PageSize) : query.OrderBy(x => x.Id),
nameof(ApiKey.ExpiresAt) => orderByDir == "desc"
? query.OrderByDescending(x => x.ExpiresAt)
: query.OrderBy(x => x.ExpiresAt),
nameof(ApiKey.CreatedAt) => orderByDir == "desc"
? query.OrderByDescending(x => x.CreatedAt)
: query.OrderBy(x => x.CreatedAt),
_ => query.OrderBy(x => x.Id)
};
if (!string.IsNullOrEmpty(filter))
{
query = query.Where(x =>
EF.Functions.ILike(x.Description, $"%{filter}%")
);
}
var totalCount = await query.CountAsync();
var items = await query
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToResponse()
.ToArrayAsync(); .ToArrayAsync();
var mappedApiKey = apiKeys return new CountedData<ApiKeyResponse>()
.Select(x => new ApiKeyResponse()
{
Id = x.Id,
Permissions = x.Permissions,
Description = x.Description,
ExpiresAt = x.ExpiresAt
})
.ToArray();
return new PagedData<ApiKeyResponse>()
{ {
CurrentPage = options.Page, Items = items,
Items = mappedApiKey, TotalCount = totalCount
PageSize = options.PageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize
}; };
} }
[HttpGet("{id}")] [HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.apikeys.get")] [Authorize(Policy = "permissions:admin.apikeys.get")]
public async Task<ApiKeyResponse> GetSingle(int id) public async Task<ActionResult<ApiKeyResponse>> GetSingleAsync(int id)
{ {
var apiKey = await ApiKeyRepository var apiKey = await ApiKeyRepository
.Get() .Get()
.AsNoTracking()
.ProjectToResponse()
.FirstOrDefaultAsync(x => x.Id == id); .FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null) if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404); return Problem("No api key with that id found", statusCode: 404);
return new ApiKeyResponse() return apiKey;
{
Id = apiKey.Id,
Permissions = apiKey.Permissions,
Description = apiKey.Description,
ExpiresAt = apiKey.ExpiresAt
};
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "permissions:admin.apikeys.create")] [Authorize(Policy = "permissions:admin.apikeys.create")]
public async Task<CreateApiKeyResponse> Create([FromBody] CreateApiKeyRequest request) public async Task<CreateApiKeyResponse> CreateAsync([FromBody] CreateApiKeyRequest request)
{ {
var apiKey = new ApiKey() var apiKey = ApiKeyMapper.ToApiKey(request);
{
Description = request.Description, var finalApiKey = await ApiKeyRepository.AddAsync(apiKey);
Permissions = request.Permissions,
ExpiresAt = request.ExpiresAt
};
var finalApiKey = await ApiKeyRepository.Add(apiKey);
var response = new CreateApiKeyResponse var response = new CreateApiKeyResponse
{ {
@@ -104,41 +115,36 @@ public class ApiKeysController : Controller
return response; return response;
} }
[HttpPatch("{id}")] [HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.apikeys.update")] [Authorize(Policy = "permissions:admin.apikeys.update")]
public async Task<ApiKeyResponse> Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request) public async Task<ActionResult<ApiKeyResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateApiKeyRequest request)
{ {
var apiKey = await ApiKeyRepository var apiKey = await ApiKeyRepository
.Get() .Get()
.FirstOrDefaultAsync(x => x.Id == id); .FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null) if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404); return Problem("No api key with that id found", statusCode: 404);
apiKey.Description = request.Description; ApiKeyMapper.Merge(apiKey, request);
await ApiKeyRepository.Update(apiKey); await ApiKeyRepository.UpdateAsync(apiKey);
return new ApiKeyResponse() return ApiKeyMapper.ToResponse(apiKey);
{
Id = apiKey.Id,
Description = apiKey.Description,
Permissions = apiKey.Permissions,
ExpiresAt = apiKey.ExpiresAt
};
} }
[HttpDelete("{id}")] [HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.apikeys.delete")] [Authorize(Policy = "permissions:admin.apikeys.delete")]
public async Task Delete([FromRoute] int id) public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{ {
var apiKey = await ApiKeyRepository var apiKey = await ApiKeyRepository
.Get() .Get()
.FirstOrDefaultAsync(x => x.Id == id); .FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null) if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404); return Problem("No api key with that id found", statusCode: 404);
await ApiKeyRepository.Remove(apiKey); await ApiKeyRepository.RemoveAsync(apiKey);
return NoContent();
} }
} }

View File

@@ -19,9 +19,9 @@ public class AdvancedController : Controller
[HttpGet("frontend")] [HttpGet("frontend")]
[Authorize(Policy = "permissions:admin.system.advanced.frontend")] [Authorize(Policy = "permissions:admin.system.advanced.frontend")]
public async Task Frontend() public async Task FrontendAsync()
{ {
var stream = await FrontendService.GenerateZip(); var stream = await FrontendService.GenerateZipAsync();
await Results.File(stream, fileDownloadName: "frontend.zip").ExecuteAsync(HttpContext); await Results.File(stream, fileDownloadName: "frontend.zip").ExecuteAsync(HttpContext);
} }
} }

View File

@@ -26,65 +26,96 @@ public class ThemesController : Controller
[HttpGet] [HttpGet]
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")] [Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
public async Task<PagedData<ThemeResponse>> Get([FromQuery] PagedOptions options) public async Task<ActionResult<ICountedData<ThemeResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count,
[FromQuery] string? orderBy,
[FromQuery] string? filter,
[FromQuery] string orderByDir = "asc"
)
{ {
var count = await ThemeRepository.Get().CountAsync(); if (count > 100)
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
var items = await ThemeRepository IQueryable<Theme> query = ThemeRepository.Get();
.Get()
.Skip(options.Page * options.PageSize) query = orderBy switch
.Take(options.PageSize)
.ToArrayAsync();
var mappedItems = items
.Select(ThemeMapper.ToResponse)
.ToArray();
return new PagedData<ThemeResponse>()
{ {
CurrentPage = options.Page, nameof(Theme.Id) => orderByDir == "desc"
Items = mappedItems, ? query.OrderByDescending(x => x.Id)
PageSize = options.PageSize, : query.OrderBy(x => x.Id),
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize nameof(Theme.Name) => orderByDir == "desc"
? query.OrderByDescending(x => x.Name)
: query.OrderBy(x => x.Name),
nameof(Theme.Version) => orderByDir == "desc"
? query.OrderByDescending(x => x.Version)
: query.OrderBy(x => x.Version),
_ => query.OrderBy(x => x.Id)
};
if (!string.IsNullOrEmpty(filter))
{
query = query.Where(x =>
EF.Functions.ILike(x.Name, $"%{filter}%")
);
}
var totalCount = await query.CountAsync();
var items = await query
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToResponse()
.ToArrayAsync();
return new CountedData<ThemeResponse>()
{
Items = items,
TotalCount = totalCount
}; };
} }
[HttpGet("{id:int}")] [HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")] [Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
public async Task<ThemeResponse> GetSingle([FromRoute] int id) public async Task<ActionResult<ThemeResponse>> GetSingleAsync([FromRoute] int id)
{ {
var theme = await ThemeRepository var theme = await ThemeRepository
.Get() .Get()
.AsNoTracking()
.ProjectToResponse()
.FirstOrDefaultAsync(t => t.Id == id); .FirstOrDefaultAsync(t => t.Id == id);
if (theme == null) if (theme == null)
throw new HttpApiException("Theme with this id not found", 404); return Problem("Theme with this id not found", statusCode: 404);
return ThemeMapper.ToResponse(theme); return theme;
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")] [Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task<ThemeResponse> Create([FromBody] CreateThemeRequest request) public async Task<ActionResult<ThemeResponse>> CreateAsync([FromBody] CreateThemeRequest request)
{ {
var theme = ThemeMapper.ToTheme(request); var theme = ThemeMapper.ToTheme(request);
var finalTheme = await ThemeRepository.Add(theme); var finalTheme = await ThemeRepository.AddAsync(theme);
return ThemeMapper.ToResponse(finalTheme); return ThemeMapper.ToResponse(finalTheme);
} }
[HttpPatch("{id:int}")] [HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")] [Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task<ThemeResponse> Update([FromRoute] int id, [FromBody] UpdateThemeRequest request) public async Task<ActionResult<ThemeResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateThemeRequest request)
{ {
var theme = await ThemeRepository var theme = await ThemeRepository
.Get() .Get()
.FirstOrDefaultAsync(t => t.Id == id); .FirstOrDefaultAsync(t => t.Id == id);
if (theme == null) if (theme == null)
throw new HttpApiException("Theme with this id not found", 404); return Problem("Theme with this id not found", statusCode: 404);
// Disable all other enabled themes if we are enabling the current theme. // Disable all other enabled themes if we are enabling the current theme.
// This ensures only one theme is enabled at the time // This ensures only one theme is enabled at the time
@@ -98,29 +129,28 @@ public class ThemesController : Controller
foreach (var otherTheme in otherThemes) foreach (var otherTheme in otherThemes)
otherTheme.IsEnabled = false; otherTheme.IsEnabled = false;
await ThemeRepository.RunTransaction(set => await ThemeRepository.RunTransactionAsync(set => { set.UpdateRange(otherThemes); });
{
set.UpdateRange(otherThemes);
});
} }
ThemeMapper.Merge(theme, request); ThemeMapper.Merge(theme, request);
await ThemeRepository.Update(theme);
await ThemeRepository.UpdateAsync(theme);
return ThemeMapper.ToResponse(theme); return ThemeMapper.ToResponse(theme);
} }
[HttpDelete("{id:int}")] [HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")] [Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task Delete([FromRoute] int id) public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{ {
var theme = await ThemeRepository var theme = await ThemeRepository
.Get() .Get()
.FirstOrDefaultAsync(x => x.Id == id); .FirstOrDefaultAsync(x => x.Id == id);
if (theme == null) if (theme == null)
throw new HttpApiException("Theme with this id not found", 404); return Problem("Theme with this id not found", statusCode: 404);
await ThemeRepository.Remove(theme); await ThemeRepository.RemoveAsync(theme);
return NoContent();
} }
} }

View File

@@ -21,7 +21,7 @@ public class DiagnoseController : Controller
} }
[HttpPost] [HttpPost]
public async Task<ActionResult> Diagnose([FromBody] GenerateDiagnoseRequest request) public async Task<ActionResult> DiagnoseAsync([FromBody] GenerateDiagnoseRequest request)
{ {
var stream = await DiagnoseService.GenerateDiagnoseAsync(request.Providers); var stream = await DiagnoseService.GenerateDiagnoseAsync(request.Providers);
@@ -29,7 +29,7 @@ public class DiagnoseController : Controller
} }
[HttpGet("providers")] [HttpGet("providers")]
public async Task<ActionResult<DiagnoseProvideResponse[]>> GetProviders() public async Task<ActionResult<DiagnoseProvideResponse[]>> GetProvidersAsync()
{ {
return await DiagnoseService.GetProvidersAsync(); return await DiagnoseService.GetProvidersAsync();
} }

View File

@@ -23,7 +23,7 @@ public class CombineController : Controller
} }
[HttpPost("combine")] [HttpPost("combine")]
public async Task<IResult> Combine([FromBody] CombineRequest request) public async Task<IResult> CombineAsync([FromBody] CombineRequest request)
{ {
// Validate file lenght // Validate file lenght
if (request.Files.Length < 2) if (request.Files.Length < 2)

View File

@@ -19,7 +19,7 @@ public class CompressController : Controller
private const string BaseDirectory = "storage"; private const string BaseDirectory = "storage";
[HttpPost("compress")] [HttpPost("compress")]
public async Task<IResult> Compress([FromBody] CompressRequest request) public async Task<IResult> CompressAsync([FromBody] CompressRequest request)
{ {
// Validate item length // Validate item length
if (request.Items.Length == 0) if (request.Items.Length == 0)
@@ -48,11 +48,11 @@ public class CompressController : Controller
switch (request.Format) switch (request.Format)
{ {
case "tar.gz": case "tar.gz":
await CompressTarGz(destinationPath, itemsPaths, rootPath); await CompressTarGzAsync(destinationPath, itemsPaths, rootPath);
break; break;
case "zip": case "zip":
await CompressZip(destinationPath, itemsPaths, rootPath); await CompressZipAsync(destinationPath, itemsPaths, rootPath);
break; break;
default: default:
@@ -66,14 +66,14 @@ public class CompressController : Controller
#region Tar Gz #region Tar Gz
private async Task CompressTarGz(string destination, IEnumerable<string> items, string root) private async Task CompressTarGzAsync(string destination, IEnumerable<string> items, string root)
{ {
await using var outStream = System.IO.File.Create(destination); await using var outStream = System.IO.File.Create(destination);
await using var gzoStream = new GZipOutputStream(outStream); await using var gzoStream = new GZipOutputStream(outStream);
await using var tarStream = new TarOutputStream(gzoStream, Encoding.UTF8); await using var tarStream = new TarOutputStream(gzoStream, Encoding.UTF8);
foreach (var item in items) foreach (var item in items)
await CompressItemToTarGz(tarStream, item, root); await CompressItemToTarGzAsync(tarStream, item, root);
await tarStream.FlushAsync(); await tarStream.FlushAsync();
await gzoStream.FlushAsync(); await gzoStream.FlushAsync();
@@ -84,7 +84,7 @@ public class CompressController : Controller
outStream.Close(); outStream.Close();
} }
private async Task CompressItemToTarGz(TarOutputStream tarOutputStream, string item, string root) private async Task CompressItemToTarGzAsync(TarOutputStream tarOutputStream, string item, string root)
{ {
if (System.IO.File.Exists(item)) if (System.IO.File.Exists(item))
{ {
@@ -117,7 +117,7 @@ public class CompressController : Controller
if (Directory.Exists(item)) if (Directory.Exists(item))
{ {
foreach (var fsEntry in Directory.EnumerateFileSystemEntries(item)) foreach (var fsEntry in Directory.EnumerateFileSystemEntries(item))
await CompressItemToTarGz(tarOutputStream, fsEntry, root); await CompressItemToTarGzAsync(tarOutputStream, fsEntry, root);
} }
} }
@@ -125,13 +125,13 @@ public class CompressController : Controller
#region ZIP #region ZIP
private async Task CompressZip(string destination, IEnumerable<string> items, string root) private async Task CompressZipAsync(string destination, IEnumerable<string> items, string root)
{ {
await using var outStream = System.IO.File.Create(destination); await using var outStream = System.IO.File.Create(destination);
await using var zipOutputStream = new ZipOutputStream(outStream); await using var zipOutputStream = new ZipOutputStream(outStream);
foreach (var item in items) foreach (var item in items)
await AddItemToZip(zipOutputStream, item, root); await AddItemToZipAsync(zipOutputStream, item, root);
await zipOutputStream.FlushAsync(); await zipOutputStream.FlushAsync();
await outStream.FlushAsync(); await outStream.FlushAsync();
@@ -140,7 +140,7 @@ public class CompressController : Controller
outStream.Close(); outStream.Close();
} }
private async Task AddItemToZip(ZipOutputStream outputStream, string item, string root) private async Task AddItemToZipAsync(ZipOutputStream outputStream, string item, string root)
{ {
if (System.IO.File.Exists(item)) if (System.IO.File.Exists(item))
{ {
@@ -175,7 +175,7 @@ public class CompressController : Controller
if (Directory.Exists(item)) if (Directory.Exists(item))
{ {
foreach (var subItem in Directory.EnumerateFileSystemEntries(item)) foreach (var subItem in Directory.EnumerateFileSystemEntries(item))
await AddItemToZip(outputStream, subItem, root); await AddItemToZipAsync(outputStream, subItem, root);
} }
} }

View File

@@ -17,7 +17,7 @@ public class DecompressController : Controller
private const string BaseDirectory = "storage"; private const string BaseDirectory = "storage";
[HttpPost("decompress")] [HttpPost("decompress")]
public async Task Decompress([FromBody] DecompressRequest request) public async Task DecompressAsync([FromBody] DecompressRequest request)
{ {
var path = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Path)); var path = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Path));
var destination = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Destination)); var destination = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Destination));
@@ -25,18 +25,18 @@ public class DecompressController : Controller
switch (request.Format) switch (request.Format)
{ {
case "tar.gz": case "tar.gz":
await DecompressTarGz(path, destination); await DecompressTarGzAsync(path, destination);
break; break;
case "zip": case "zip":
await DecompressZip(path, destination); await DecompressZipAsync(path, destination);
break; break;
} }
} }
#region Tar Gz #region Tar Gz
private async Task DecompressTarGz(string path, string destination) private async Task DecompressTarGzAsync(string path, string destination)
{ {
await using var fs = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); await using var fs = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await using var gzipInputStream = new GZipInputStream(fs); await using var gzipInputStream = new GZipInputStream(fs);
@@ -74,7 +74,7 @@ public class DecompressController : Controller
#region Zip #region Zip
private async Task DecompressZip(string path, string destination) private async Task DecompressZipAsync(string path, string destination)
{ {
await using var fs = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); await using var fs = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await using var zipInputStream = new ZipInputStream(fs); await using var zipInputStream = new ZipInputStream(fs);

View File

@@ -25,7 +25,7 @@ public class DownloadUrlController : Controller
} }
[HttpGet] [HttpGet]
public async Task Get([FromQuery] string path) public async Task GetAsync([FromQuery] string path)
{ {
var physicalPath = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(path)); var physicalPath = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(path));
var name = Path.GetFileName(physicalPath); var name = Path.GetFileName(physicalPath);
@@ -55,7 +55,7 @@ public class DownloadUrlController : Controller
await using var zipStream = new ZipOutputStream(Response.Body); await using var zipStream = new ZipOutputStream(Response.Body);
zipStream.IsStreamOwner = false; zipStream.IsStreamOwner = false;
await StreamFolderAsZip(zipStream, physicalPath, baseDirectory, HttpContext.RequestAborted); await StreamFolderAsZipAsync(zipStream, physicalPath, baseDirectory, HttpContext.RequestAborted);
} }
catch (ZipException) catch (ZipException)
{ {
@@ -68,7 +68,7 @@ public class DownloadUrlController : Controller
} }
} }
private async Task StreamFolderAsZip( private async Task StreamFolderAsZipAsync(
ZipOutputStream zipStream, ZipOutputStream zipStream,
string path, string rootPath, string path, string rootPath,
CancellationToken cancellationToken CancellationToken cancellationToken
@@ -102,7 +102,7 @@ public class DownloadUrlController : Controller
if (HttpContext.RequestAborted.IsCancellationRequested) if (HttpContext.RequestAborted.IsCancellationRequested)
return; return;
await StreamFolderAsZip(zipStream, directory, rootPath, cancellationToken); await StreamFolderAsZipAsync(zipStream, directory, rootPath, cancellationToken);
} }
} }
@@ -110,7 +110,7 @@ public class DownloadUrlController : Controller
// Yes I know we can just create that url on the client as the exist validation is done on both endpoints, // Yes I know we can just create that url on the client as the exist validation is done on both endpoints,
// but we leave it here for future modifications. E.g. using a distributed file provider or smth like that // but we leave it here for future modifications. E.g. using a distributed file provider or smth like that
[HttpPost] [HttpPost]
public Task<DownloadUrlResponse> Post([FromQuery] string path) public Task<DownloadUrlResponse> PostAsync([FromQuery] string path)
{ {
var safePath = FilePathHelper.SanitizePath(path); var safePath = FilePathHelper.SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath); var physicalPath = Path.Combine(BaseDirectory, safePath);

View File

@@ -15,7 +15,7 @@ public class FilesController : Controller
private const string BaseDirectory = "storage"; private const string BaseDirectory = "storage";
[HttpPost("touch")] [HttpPost("touch")]
public async Task CreateFile([FromQuery] string path) public async Task CreateFileAsync([FromQuery] string path)
{ {
var safePath = FilePathHelper.SanitizePath(path); var safePath = FilePathHelper.SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath); var physicalPath = Path.Combine(BaseDirectory, safePath);
@@ -31,7 +31,7 @@ public class FilesController : Controller
} }
[HttpPost("mkdir")] [HttpPost("mkdir")]
public Task CreateFolder([FromQuery] string path) public Task CreateFolderAsync([FromQuery] string path)
{ {
var safePath = FilePathHelper.SanitizePath(path); var safePath = FilePathHelper.SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath); var physicalPath = Path.Combine(BaseDirectory, safePath);
@@ -47,7 +47,7 @@ public class FilesController : Controller
} }
[HttpGet("list")] [HttpGet("list")]
public Task<FileSystemEntryResponse[]> List([FromQuery] string path) public Task<FileSystemEntryResponse[]> ListAsync([FromQuery] string path)
{ {
var safePath = FilePathHelper.SanitizePath(path); var safePath = FilePathHelper.SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath); var physicalPath = Path.Combine(BaseDirectory, safePath);
@@ -92,7 +92,7 @@ public class FilesController : Controller
} }
[HttpPost("move")] [HttpPost("move")]
public Task Move([FromQuery] string oldPath, [FromQuery] string newPath) public Task MoveAsync([FromQuery] string oldPath, [FromQuery] string newPath)
{ {
var oldSafePath = FilePathHelper.SanitizePath(oldPath); var oldSafePath = FilePathHelper.SanitizePath(oldPath);
var newSafePath = FilePathHelper.SanitizePath(newPath); var newSafePath = FilePathHelper.SanitizePath(newPath);
@@ -123,7 +123,7 @@ public class FilesController : Controller
} }
[HttpDelete("delete")] [HttpDelete("delete")]
public Task Delete([FromQuery] string path) public Task DeleteAsync([FromQuery] string path)
{ {
var safePath = FilePathHelper.SanitizePath(path); var safePath = FilePathHelper.SanitizePath(path);
var physicalDirPath = Path.Combine(BaseDirectory, safePath); var physicalDirPath = Path.Combine(BaseDirectory, safePath);
@@ -141,7 +141,7 @@ public class FilesController : Controller
} }
[HttpPost("upload")] [HttpPost("upload")]
public async Task<IResult> Upload([FromQuery] string path) public async Task<IResult> UploadAsync([FromQuery] string path)
{ {
if (Request.Form.Files.Count != 1) if (Request.Form.Files.Count != 1)
return Results.Problem("Only one file is allowed in the request", statusCode: 400); return Results.Problem("Only one file is allowed in the request", statusCode: 400);
@@ -179,7 +179,7 @@ public class FilesController : Controller
} }
[HttpGet("download")] [HttpGet("download")]
public async Task Download([FromQuery] string path) public async Task DownloadAsync([FromQuery] string path)
{ {
var safePath = FilePathHelper.SanitizePath(path); var safePath = FilePathHelper.SanitizePath(path);
var physicalPath = Path.Combine(BaseDirectory, safePath); var physicalPath = Path.Combine(BaseDirectory, safePath);

View File

@@ -18,7 +18,7 @@ public class HangfireController : Controller
} }
[HttpGet("stats")] [HttpGet("stats")]
public Task<HangfireStatsResponse> GetStats() public Task<HangfireStatsResponse> GetStatsAsync()
{ {
var statistics = JobStorage.GetMonitoringApi().GetStatistics(); var statistics = JobStorage.GetMonitoringApi().GetStatistics();

View File

@@ -18,21 +18,21 @@ public class SystemController : Controller
[HttpGet] [HttpGet]
[Authorize(Policy = "permissions:admin.system.overview")] [Authorize(Policy = "permissions:admin.system.overview")]
public async Task<SystemOverviewResponse> GetOverview() public async Task<SystemOverviewResponse> GetOverviewAsync()
{ {
return new() return new()
{ {
Uptime = await ApplicationService.GetUptime(), Uptime = await ApplicationService.GetUptimeAsync(),
CpuUsage = await ApplicationService.GetCpuUsage(), CpuUsage = await ApplicationService.GetCpuUsageAsync(),
MemoryUsage = await ApplicationService.GetMemoryUsage(), MemoryUsage = await ApplicationService.GetMemoryUsageAsync(),
OperatingSystem = await ApplicationService.GetOsName() OperatingSystem = await ApplicationService.GetOsNameAsync()
}; };
} }
[HttpPost("shutdown")] [HttpPost("shutdown")]
[Authorize(Policy = "permissions:admin.system.shutdown")] [Authorize(Policy = "permissions:admin.system.shutdown")]
public async Task Shutdown() public async Task ShutdownAsync()
{ {
await ApplicationService.Shutdown(); await ApplicationService.ShutdownAsync();
} }
} }

View File

@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -6,10 +5,10 @@ using Microsoft.Extensions.DependencyInjection;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions; using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers; using MoonCore.Extended.Helpers;
using MoonCore.Extended.Models;
using MoonCore.Models; using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Services; using Moonlight.ApiServer.Services;
using Moonlight.ApiServer.Mappers;
using Moonlight.Shared.Http.Requests.Admin.Users; using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users; using Moonlight.Shared.Http.Responses.Admin.Users;
@@ -28,60 +27,78 @@ public class UsersController : Controller
[HttpGet] [HttpGet]
[Authorize(Policy = "permissions:admin.users.get")] [Authorize(Policy = "permissions:admin.users.get")]
public async Task<IPagedData<UserResponse>> Get([FromQuery] PagedOptions options) public async Task<ActionResult<ICountedData<UserResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int count,
[FromQuery] string? orderBy,
[FromQuery] string? filter,
[FromQuery] string orderByDir = "asc"
)
{ {
var count = await UserRepository.Get().CountAsync(); if (count > 100)
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
IQueryable<User> query = UserRepository.Get();
var users = await UserRepository query = orderBy switch
.Get() {
.OrderBy(x => x.Id) nameof(Database.Entities.User.Id) => orderByDir == "desc"
.Skip(options.Page * options.PageSize) ? query.OrderByDescending(x => x.Id)
.Take(options.PageSize) : query.OrderBy(x => x.Id),
nameof(Database.Entities.User.Username) => orderByDir == "desc"
? query.OrderByDescending(x => x.Username)
: query.OrderBy(x => x.Username),
nameof(Database.Entities.User.Email) => orderByDir == "desc"
? query.OrderByDescending(x => x.Email)
: query.OrderBy(x => x.Email),
_ => query.OrderBy(x => x.Id)
};
if (!string.IsNullOrEmpty(filter))
{
query = query.Where(x =>
EF.Functions.ILike(x.Username, $"%{filter}%") ||
EF.Functions.ILike(x.Email, $"%{filter}%")
);
}
var totalCount = await query.CountAsync();
var items = await query
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToResponse()
.ToArrayAsync(); .ToArrayAsync();
var mappedUsers = users return new CountedData<UserResponse>()
.Select(x => new UserResponse()
{
Id = x.Id,
Email = x.Email,
Username = x.Username,
Permissions = x.Permissions
})
.ToArray();
return new PagedData<UserResponse>()
{ {
CurrentPage = options.Page, Items = items,
Items = mappedUsers, TotalCount = totalCount
PageSize = options.PageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize
}; };
} }
[HttpGet("{id}")] [HttpGet("{id}")]
[Authorize(Policy = "permissions:admin.users.get")] [Authorize(Policy = "permissions:admin.users.get")]
public async Task<UserResponse> GetSingle(int id) public async Task<ActionResult<UserResponse>> GetSingleAsync(int id)
{ {
var user = await UserRepository var user = await UserRepository
.Get() .Get()
.ProjectToResponse()
.FirstOrDefaultAsync(x => x.Id == id); .FirstOrDefaultAsync(x => x.Id == id);
if (user == null) if (user == null)
throw new HttpApiException("No user with that id found", 404); return Problem("No user with that id found", statusCode: 404);
return new UserResponse() return user;
{
Id = user.Id,
Email = user.Email,
Username = user.Username,
Permissions = user.Permissions
};
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "permissions:admin.users.create")] [Authorize(Policy = "permissions:admin.users.create")]
public async Task<UserResponse> Create([FromBody] CreateUserRequest request) public async Task<ActionResult<UserResponse>> CreateAsync([FromBody] CreateUserRequest request)
{ {
// Reformat values // Reformat values
request.Username = request.Username.ToLower().Trim(); request.Username = request.Username.ToLower().Trim();
@@ -89,10 +106,10 @@ public class UsersController : Controller
// Check for users with the same values // Check for users with the same values
if (UserRepository.Get().Any(x => x.Username == request.Username)) if (UserRepository.Get().Any(x => x.Username == request.Username))
throw new HttpApiException("A user with that username already exists", 400); return Problem("A user with that username already exists", statusCode: 400);
if (UserRepository.Get().Any(x => x.Email == request.Email)) if (UserRepository.Get().Any(x => x.Email == request.Email))
throw new HttpApiException("A user with that email address already exists", 400); return Problem("A user with that email address already exists", statusCode: 400);
var hashedPassword = HashHelper.Hash(request.Password); var hashedPassword = HashHelper.Hash(request.Password);
@@ -104,27 +121,21 @@ public class UsersController : Controller
Permissions = request.Permissions Permissions = request.Permissions
}; };
var finalUser = await UserRepository.Add(user); var finalUser = await UserRepository.AddAsync(user);
return new UserResponse() return UserMapper.ToResponse(finalUser);
{
Id = finalUser.Id,
Email = finalUser.Email,
Username = finalUser.Username,
Permissions = finalUser.Permissions
};
} }
[HttpPatch("{id}")] [HttpPatch("{id}")]
[Authorize(Policy = "permissions:admin.users.update")] [Authorize(Policy = "permissions:admin.users.update")]
public async Task<UserResponse> Update([FromRoute] int id, [FromBody] UpdateUserRequest request) public async Task<ActionResult<UserResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateUserRequest request)
{ {
var user = await UserRepository var user = await UserRepository
.Get() .Get()
.FirstOrDefaultAsync(x => x.Id == id); .FirstOrDefaultAsync(x => x.Id == id);
if (user == null) if (user == null)
throw new HttpApiException("No user with that id found", 404); return Problem("No user with that id found", statusCode: 404);
// Reformat values // Reformat values
request.Username = request.Username.ToLower().Trim(); request.Username = request.Username.ToLower().Trim();
@@ -132,10 +143,10 @@ public class UsersController : Controller
// Check for users with the same values // Check for users with the same values
if (UserRepository.Get().Any(x => x.Username == request.Username && x.Id != user.Id)) if (UserRepository.Get().Any(x => x.Username == request.Username && x.Id != user.Id))
throw new HttpApiException("A user with that username already exists", 400); return Problem("Another user with that username already exists", statusCode: 400);
if (UserRepository.Get().Any(x => x.Email == request.Email && x.Id != user.Id)) if (UserRepository.Get().Any(x => x.Email == request.Email && x.Id != user.Id))
throw new HttpApiException("A user with that email address already exists", 400); return Problem("Another user with that email address already exists", statusCode: 400);
// Perform hashing the password if required // Perform hashing the password if required
if (!string.IsNullOrEmpty(request.Password)) if (!string.IsNullOrEmpty(request.Password))
@@ -153,38 +164,33 @@ public class UsersController : Controller
user.Email = request.Email; user.Email = request.Email;
user.Username = request.Username; user.Username = request.Username;
await UserRepository.Update(user); await UserRepository.UpdateAsync(user);
return new UserResponse() return UserMapper.ToResponse(user);
{
Id = user.Id,
Email = user.Email,
Username = user.Username,
Permissions = user.Permissions
};
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
[Authorize(Policy = "permissions:admin.users.delete")] [Authorize(Policy = "permissions:admin.users.delete")]
public async Task Delete([FromRoute] int id, [FromQuery] bool force = false) public async Task<ActionResult> DeleteAsync([FromRoute] int id, [FromQuery] bool force = false)
{ {
var user = await UserRepository var user = await UserRepository
.Get() .Get()
.FirstOrDefaultAsync(x => x.Id == id); .FirstOrDefaultAsync(x => x.Id == id);
if (user == null) if (user == null)
throw new HttpApiException("No user with that id found", 404); return Problem("No user with that id found", statusCode: 404);
var deletionService = HttpContext.RequestServices.GetRequiredService<UserDeletionService>(); var deletionService = HttpContext.RequestServices.GetRequiredService<UserDeletionService>();
if (!force) if (!force)
{ {
var validationResult = await deletionService.Validate(user); var validationResult = await deletionService.ValidateAsync(user);
if (!validationResult.IsAllowed) if (!validationResult.IsAllowed)
throw new HttpApiException($"Unable to delete user", 400, validationResult.Reason); return Problem("Unable to delete user", statusCode: 400, title: validationResult.Reason);
} }
await deletionService.Delete(user, force); await deletionService.DeleteAsync(user, force);
return NoContent();
} }
} }

View File

@@ -30,7 +30,7 @@ public class AuthController : Controller
} }
[HttpGet] [HttpGet]
public async Task<AuthSchemeResponse[]> GetSchemes() public async Task<AuthSchemeResponse[]> GetSchemesAsync()
{ {
var schemes = await SchemeProvider.GetAllSchemesAsync(); var schemes = await SchemeProvider.GetAllSchemesAsync();
@@ -47,7 +47,7 @@ public class AuthController : Controller
} }
[HttpGet("{identifier:alpha}")] [HttpGet("{identifier:alpha}")]
public async Task StartScheme([FromRoute] string identifier) public async Task StartSchemeAsync([FromRoute] string identifier)
{ {
// Validate identifier against our enable list // Validate identifier against our enable list
var allowedSchemes = Configuration.Authentication.EnabledSchemes; var allowedSchemes = Configuration.Authentication.EnabledSchemes;
@@ -91,7 +91,7 @@ public class AuthController : Controller
[Authorize] [Authorize]
[HttpGet("check")] [HttpGet("check")]
public async Task<AuthClaimResponse[]> Check() public async Task<AuthClaimResponse[]> CheckAsync()
{ {
var username = User.FindFirstValue(ClaimTypes.Name)!; var username = User.FindFirstValue(ClaimTypes.Name)!;
var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!; var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
@@ -113,7 +113,7 @@ public class AuthController : Controller
foreach (var extension in Extensions) foreach (var extension in Extensions)
{ {
claims.AddRange( claims.AddRange(
await extension.GetFrontendClaims(User) await extension.GetFrontendClaimsAsync(User)
); );
} }
@@ -121,7 +121,7 @@ public class AuthController : Controller
} }
[HttpGet("logout")] [HttpGet("logout")]
public async Task Logout() public async Task LogoutAsync()
{ {
await HttpContext.SignOutAsync(); await HttpContext.SignOutAsync();
await Results.Redirect("/").ExecuteAsync(HttpContext); await Results.Redirect("/").ExecuteAsync(HttpContext);

View File

@@ -18,13 +18,13 @@ public class FrontendController : Controller
} }
[HttpGet("frontend.json")] [HttpGet("frontend.json")]
public async Task<FrontendConfiguration> GetConfiguration() public async Task<FrontendConfiguration> GetConfigurationAsync()
=> await FrontendService.GetConfiguration(); => await FrontendService.GetConfigurationAsync();
[HttpGet] [HttpGet]
public async Task<IResult> Index() public async Task<IResult> IndexAsync()
{ {
var content = await FrontendService.GenerateIndexHtml(); var content = await FrontendService.GenerateIndexHtmlAsync();
return Results.Text(content, "text/html", Encoding.UTF8); return Results.Text(content, "text/html", Encoding.UTF8);
} }

View File

@@ -45,29 +45,29 @@ public class LocalAuthController : Controller
[HttpGet] [HttpGet]
[HttpGet("login")] [HttpGet("login")]
public async Task<IResult> Login() public async Task<ActionResult> LoginAsync()
{ {
var html = await ComponentHelper.RenderComponent<Login>(ServiceProvider); var html = await ComponentHelper.RenderToHtmlAsync<Login>(ServiceProvider);
return Results.Content(html, "text/html"); return Content(html, "text/html");
} }
[HttpGet("register")] [HttpGet("register")]
public async Task<IResult> Register() public async Task<ActionResult> RegisterAsync()
{ {
var html = await ComponentHelper.RenderComponent<Register>(ServiceProvider); var html = await ComponentHelper.RenderToHtmlAsync<Register>(ServiceProvider);
return Results.Content(html, "text/html"); return Content(html, "text/html");
} }
[HttpPost] [HttpPost]
[HttpPost("login")] [HttpPost("login")]
public async Task<IResult> Login([FromForm] string email, [FromForm] string password) public async Task<ActionResult> LoginAsync([FromForm] string email, [FromForm] string password)
{ {
try try
{ {
// Perform login // Perform login
var user = await InternalLogin(email, password); var user = await InternalLoginAsync(email, password);
// Login user // Login user
var options = Options.Get(LocalAuthConstants.AuthenticationScheme); var options = Options.Get(LocalAuthConstants.AuthenticationScheme);
@@ -84,34 +84,34 @@ public class LocalAuthController : Controller
), new AuthenticationProperties()); ), new AuthenticationProperties());
// Redirect back to wasm app // Redirect back to wasm app
return Results.Redirect("/"); return Redirect("/");
} }
catch (Exception e) catch (Exception e)
{ {
string errorMessage; string errorMessage;
if (e is HttpApiException apiException) if (e is AggregateException aggregateException)
errorMessage = apiException.Title; errorMessage = aggregateException.Message;
else else
{ {
errorMessage = "An internal error occured"; errorMessage = "An internal error occured";
Logger.LogError(e, "An unhandled error occured while logging in user"); Logger.LogError(e, "An unhandled error occured while logging in user");
} }
var html = await ComponentHelper.RenderComponent<Login>(ServiceProvider, var html = await ComponentHelper.RenderToHtmlAsync<Login>(ServiceProvider,
parameters => { parameters["ErrorMessage"] = errorMessage; }); parameters => { parameters["ErrorMessage"] = errorMessage; });
return Results.Content(html, "text/html"); return Content(html, "text/html");
} }
} }
[HttpPost("register")] [HttpPost("register")]
public async Task<IResult> Register([FromForm] string email, [FromForm] string password, [FromForm] string username) public async Task<ActionResult> RegisterAsync([FromForm] string email, [FromForm] string password, [FromForm] string username)
{ {
try try
{ {
// Perform register // Perform register
var user = await InternalRegister(username, email, password); var user = await InternalRegisterAsync(username, email, password);
// Login user // Login user
var options = Options.Get(LocalAuthConstants.AuthenticationScheme); var options = Options.Get(LocalAuthConstants.AuthenticationScheme);
@@ -128,37 +128,37 @@ public class LocalAuthController : Controller
), new AuthenticationProperties()); ), new AuthenticationProperties());
// Redirect back to wasm app // Redirect back to wasm app
return Results.Redirect("/"); return Redirect("/");
} }
catch (Exception e) catch (Exception e)
{ {
string errorMessage; string errorMessage;
if (e is HttpApiException apiException) if (e is AggregateException aggregateException)
errorMessage = apiException.Title; errorMessage = aggregateException.Message;
else else
{ {
errorMessage = "An internal error occured"; errorMessage = "An internal error occured";
Logger.LogError(e, "An unhandled error occured while logging in user"); Logger.LogError(e, "An unhandled error occured while logging in user");
} }
var html = await ComponentHelper.RenderComponent<Register>(ServiceProvider, var html = await ComponentHelper.RenderToHtmlAsync<Register>(ServiceProvider,
parameters => { parameters["ErrorMessage"] = errorMessage; }); parameters => { parameters["ErrorMessage"] = errorMessage; });
return Results.Content(html, "text/html"); return Content(html, "text/html");
} }
} }
private async Task<User> InternalRegister(string username, string email, string password) private async Task<User> InternalRegisterAsync(string username, string email, string password)
{ {
email = email.ToLower(); email = email.ToLower();
username = username.ToLower(); username = username.ToLower();
if (await UserRepository.Get().AnyAsync(x => x.Username == username)) if (await UserRepository.Get().AnyAsync(x => x.Username == username))
throw new HttpApiException("A account with that username already exists", 400); throw new AggregateException("A account with that username already exists");
if (await UserRepository.Get().AnyAsync(x => x.Email == email)) if (await UserRepository.Get().AnyAsync(x => x.Email == email))
throw new HttpApiException("A account with that email already exists", 400); throw new AggregateException("A account with that email already exists");
string[] permissions = []; string[] permissions = [];
@@ -180,12 +180,12 @@ public class LocalAuthController : Controller
Permissions = permissions Permissions = permissions
}; };
var finalUser = await UserRepository.Add(user); var finalUser = await UserRepository.AddAsync(user);
return finalUser; return finalUser;
} }
private async Task<User> InternalLogin(string email, string password) private async Task<User> InternalLoginAsync(string email, string password)
{ {
email = email.ToLower(); email = email.ToLower();
@@ -194,10 +194,10 @@ public class LocalAuthController : Controller
.FirstOrDefaultAsync(x => x.Email == email); .FirstOrDefaultAsync(x => x.Email == email);
if (user == null) if (user == null)
throw new HttpApiException("Invalid combination of email and password", 400); throw new AggregateException("Invalid combination of email and password");
if (!HashHelper.Verify(password, user.Password)) if (!HashHelper.Verify(password, user.Password))
throw new HttpApiException("Invalid combination of email and password", 400); throw new AggregateException("Invalid combination of email and password");
return user; return user;
} }

View File

@@ -24,7 +24,7 @@ public class SwaggerController : Controller
[HttpGet] [HttpGet]
[Authorize] [Authorize]
public async Task<ActionResult> Get() public async Task<ActionResult> GetAsync()
{ {
if (!Configuration.Development.EnableApiDocs) if (!Configuration.Development.EnableApiDocs)
return BadRequest("Api docs are disabled"); return BadRequest("Api docs are disabled");
@@ -32,7 +32,7 @@ public class SwaggerController : Controller
var options = new ApiDocsOptions(); var options = new ApiDocsOptions();
var optionsJson = JsonSerializer.Serialize(options); var optionsJson = JsonSerializer.Serialize(options);
var html = await ComponentHelper.RenderComponent<SwaggerPage>( var html = await ComponentHelper.RenderToHtmlAsync<SwaggerPage>(
ServiceProvider, ServiceProvider,
parameters => parameters =>
{ {

View File

@@ -7,7 +7,7 @@ namespace Moonlight.ApiServer.Http.Hubs;
public class DiagnoseHub : Hub public class DiagnoseHub : Hub
{ {
[HubMethodName("Ping")] [HubMethodName("Ping")]
public async Task Ping() public async Task PingAsync()
{ {
await Clients.All.SendAsync("Pong"); await Clients.All.SendAsync("Pong");
} }

View File

@@ -11,7 +11,7 @@ public class ApplicationMetric : IMetric
private Gauge<int> CpuUsage; private Gauge<int> CpuUsage;
private Gauge<double> Uptime; private Gauge<double> Uptime;
public Task Initialize(Meter meter) public Task InitializeAsync(Meter meter)
{ {
MemoryUsage = meter.CreateGauge<long>("moonlight_memory_usage"); MemoryUsage = meter.CreateGauge<long>("moonlight_memory_usage");
CpuUsage = meter.CreateGauge<int>("moonlight_cpu_usage"); CpuUsage = meter.CreateGauge<int>("moonlight_cpu_usage");
@@ -20,17 +20,17 @@ public class ApplicationMetric : IMetric
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task Run(IServiceProvider provider, CancellationToken cancellationToken) public async Task RunAsync(IServiceProvider provider, CancellationToken cancellationToken)
{ {
var applicationService = provider.GetRequiredService<ApplicationService>(); var applicationService = provider.GetRequiredService<ApplicationService>();
var memory = await applicationService.GetMemoryUsage(); var memory = await applicationService.GetMemoryUsageAsync();
MemoryUsage.Record(memory); MemoryUsage.Record(memory);
var uptime = await applicationService.GetUptime(); var uptime = await applicationService.GetUptimeAsync();
Uptime.Record(uptime.TotalSeconds); Uptime.Record(uptime.TotalSeconds);
var cpu = await applicationService.GetCpuUsage(); var cpu = await applicationService.GetCpuUsageAsync();
CpuUsage.Record(cpu); CpuUsage.Record(cpu);
} }
} }

View File

@@ -11,14 +11,14 @@ public class UsersMetric : IMetric
{ {
private Gauge<int> Users; private Gauge<int> Users;
public Task Initialize(Meter meter) public Task InitializeAsync(Meter meter)
{ {
Users = meter.CreateGauge<int>("moonlight_users"); Users = meter.CreateGauge<int>("moonlight_users");
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task Run(IServiceProvider provider, CancellationToken cancellationToken) public async Task RunAsync(IServiceProvider provider, CancellationToken cancellationToken)
{ {
var usersRepo = provider.GetRequiredService<DatabaseRepository<User>>(); var usersRepo = provider.GetRequiredService<DatabaseRepository<User>>();
var count = await usersRepo.Get().CountAsync(cancellationToken: cancellationToken); var count = await usersRepo.Get().CountAsync(cancellationToken: cancellationToken);

View File

@@ -21,7 +21,7 @@ namespace Moonlight.ApiServer.Implementations.Startup;
public class CoreStartup : IPluginStartup public class CoreStartup : IPluginStartup
{ {
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder) public Task BuildApplicationAsync(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
{ {
var configuration = serviceProvider.GetRequiredService<AppConfiguration>(); var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
@@ -142,7 +142,7 @@ public class CoreStartup : IPluginStartup
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app) public Task ConfigureApplicationAsync(IServiceProvider serviceProvider, IApplicationBuilder app)
{ {
var configuration = serviceProvider.GetRequiredService<AppConfiguration>(); var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
@@ -156,7 +156,7 @@ public class CoreStartup : IPluginStartup
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder) public Task ConfigureEndpointsAsync(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder)
{ {
var configuration = serviceProvider.GetRequiredService<AppConfiguration>(); var configuration = serviceProvider.GetRequiredService<AppConfiguration>();

View File

@@ -12,5 +12,5 @@ public interface IAuthCheckExtension
/// </summary> /// </summary>
/// <param name="principal">The principal of the current signed-in user</param> /// <param name="principal">The principal of the current signed-in user</param>
/// <returns>An array of claim responses which gets added to the list of claims to send to the frontend</returns> /// <returns>An array of claim responses which gets added to the list of claims to send to the frontend</returns>
public Task<AuthClaimResponse[]> GetFrontendClaims(ClaimsPrincipal principal); public Task<AuthClaimResponse[]> GetFrontendClaimsAsync(ClaimsPrincipal principal);
} }

View File

@@ -4,6 +4,6 @@ namespace Moonlight.ApiServer.Interfaces;
public interface IMetric public interface IMetric
{ {
public Task Initialize(Meter meter); public Task InitializeAsync(Meter meter);
public Task Run(IServiceProvider provider, CancellationToken cancellationToken); public Task RunAsync(IServiceProvider provider, CancellationToken cancellationToken);
} }

View File

@@ -12,7 +12,7 @@ public interface IUserAuthExtension
/// <param name="user">The current user this method is called for</param> /// <param name="user">The current user this method is called for</param>
/// <param name="principal">The principal after being processed by moonlight itself</param> /// <param name="principal">The principal after being processed by moonlight itself</param>
/// <returns>The result of the synchronisation. Returning false will immediately invalidate the sign-in and no other extensions will be called</returns> /// <returns>The result of the synchronisation. Returning false will immediately invalidate the sign-in and no other extensions will be called</returns>
public Task<bool> Sync(User user, ClaimsPrincipal principal); public Task<bool> SyncAsync(User user, ClaimsPrincipal principal);
/// <summary> /// <summary>
/// IMPORTANT: Please note that heavy operations should not occur in this method as it will be called for every request /// IMPORTANT: Please note that heavy operations should not occur in this method as it will be called for every request
@@ -21,5 +21,5 @@ public interface IUserAuthExtension
/// <param name="user">The current user this method is called for</param> /// <param name="user">The current user this method is called for</param>
/// <param name="principal">The principal after being processed by moonlight itself</param> /// <param name="principal">The principal after being processed by moonlight itself</param>
/// <returns>The result of the validation. Returning false will immediately invalidate the users session and no other extensions will be called</returns> /// <returns>The result of the validation. Returning false will immediately invalidate the users session and no other extensions will be called</returns>
public Task<bool> Validate(User user, ClaimsPrincipal principal); public Task<bool> ValidateAsync(User user, ClaimsPrincipal principal);
} }

View File

@@ -5,6 +5,6 @@ namespace Moonlight.ApiServer.Interfaces;
public interface IUserDeleteHandler public interface IUserDeleteHandler
{ {
public Task<UserDeleteValidationResult> Validate(User user); public Task<UserDeleteValidationResult> ValidateAsync(User user);
public Task Delete(User user, bool force); public Task DeleteAsync(User user, bool force);
} }

View File

@@ -0,0 +1,19 @@
using Moonlight.ApiServer.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Riok.Mapperly.Abstractions;
namespace Moonlight.ApiServer.Mappers;
[Mapper]
public static partial class ApiKeyMapper
{
// Mappers
public static partial ApiKeyResponse ToResponse(ApiKey apiKey);
public static partial ApiKey ToApiKey(CreateApiKeyRequest request);
public static partial void Merge([MappingTarget] ApiKey apiKey, UpdateApiKeyRequest request);
// EF Relations
public static partial IQueryable<ApiKeyResponse> ProjectToResponse(this IQueryable<ApiKey> apiKeys);
}

View File

@@ -8,7 +8,12 @@ namespace Moonlight.ApiServer.Mappers;
[Mapper] [Mapper]
public static partial class ThemeMapper public static partial class ThemeMapper
{ {
// Mappers
public static partial ThemeResponse ToResponse(Theme theme); public static partial ThemeResponse ToResponse(Theme theme);
public static partial Theme ToTheme(CreateThemeRequest request); public static partial Theme ToTheme(CreateThemeRequest request);
public static partial void Merge([MappingTarget] Theme theme, UpdateThemeRequest request); public static partial void Merge([MappingTarget] Theme theme, UpdateThemeRequest request);
// EF Relations
public static partial IQueryable<ThemeResponse> ProjectToResponse(this IQueryable<Theme> themes);
} }

View File

@@ -0,0 +1,15 @@
using Moonlight.ApiServer.Database.Entities;
using Moonlight.Shared.Http.Responses.Admin.Users;
using Riok.Mapperly.Abstractions;
namespace Moonlight.ApiServer.Mappers;
[Mapper]
public static partial class UserMapper
{
// Mappers
public static partial UserResponse ToResponse(User user);
// EF Relations
public static partial IQueryable<UserResponse> ProjectToResponse(this IQueryable<User> users);
}

View File

@@ -13,7 +13,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId> <PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.10</Version> <Version>2.1.11</Version>
<Authors>Moonlight Panel</Authors> <Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description> <Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl> <PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -25,10 +25,12 @@
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20"/> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.20"/>
<PackageReference Include="Hangfire.Core" Version="1.8.20"/> <PackageReference Include="Hangfire.Core" Version="1.8.20"/>
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/> <PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
<PackageReference Include="MoonCore" Version="1.9.7" /> <PackageReference Include="MoonCore" Version="2.0.1" />
<PackageReference Include="MoonCore.Extended" Version="1.3.7" /> <PackageReference Include="MoonCore.Extended" Version="1.4.0" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2"/> <PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1"/> <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1"/>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/>

View File

@@ -6,7 +6,7 @@ namespace Moonlight.ApiServer.Plugins;
public interface IPluginStartup public interface IPluginStartup
{ {
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder); public Task BuildApplicationAsync(IServiceProvider serviceProvider, IHostApplicationBuilder builder);
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app); public Task ConfigureApplicationAsync(IServiceProvider serviceProvider, IApplicationBuilder app);
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder); public Task ConfigureEndpointsAsync(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder);
} }

View File

@@ -14,7 +14,7 @@ public class ApiKeyAuthService
ApiKeyRepository = apiKeyRepository; ApiKeyRepository = apiKeyRepository;
} }
public async Task<bool> Validate(ClaimsPrincipal? principal) public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
{ {
// Ignore malformed claims principal // Ignore malformed claims principal
if (principal is not { Identity.IsAuthenticated: true }) if (principal is not { Identity.IsAuthenticated: true })

View File

@@ -19,7 +19,7 @@ public class ApplicationService
Host = host; Host = host;
} }
public Task<string> GetOsName() public Task<string> GetOsNameAsync()
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
@@ -58,7 +58,7 @@ public class ApplicationService
return Task.FromResult("N/A"); return Task.FromResult("N/A");
} }
public async Task<long> GetMemoryUsage() public async Task<long> GetMemoryUsageAsync()
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
@@ -87,14 +87,14 @@ public class ApplicationService
} }
} }
public Task<TimeSpan> GetUptime() public Task<TimeSpan> GetUptimeAsync()
{ {
var process = Process.GetCurrentProcess(); var process = Process.GetCurrentProcess();
var uptime = DateTime.Now - process.StartTime; var uptime = DateTime.Now - process.StartTime;
return Task.FromResult(uptime); return Task.FromResult(uptime);
} }
public Task<int> GetCpuUsage() public Task<int> GetCpuUsageAsync()
{ {
var process = Process.GetCurrentProcess(); var process = Process.GetCurrentProcess();
var cpuTime = process.TotalProcessorTime; var cpuTime = process.TotalProcessorTime;
@@ -105,7 +105,7 @@ public class ApplicationService
return Task.FromResult(cpuUsage); return Task.FromResult(cpuUsage);
} }
public Task Shutdown() public Task ShutdownAsync()
{ {
Logger.LogCritical("Restart of api server has been requested"); Logger.LogCritical("Restart of api server has been requested");

View File

@@ -40,7 +40,7 @@ public class FrontendService
ThemeRepository = themeRepository; ThemeRepository = themeRepository;
} }
public Task<FrontendConfiguration> GetConfiguration() public Task<FrontendConfiguration> GetConfigurationAsync()
{ {
var configuration = new FrontendConfiguration() var configuration = new FrontendConfiguration()
{ {
@@ -51,7 +51,7 @@ public class FrontendService
return Task.FromResult(configuration); return Task.FromResult(configuration);
} }
public async Task<string> GenerateIndexHtml() // TODO: Cache public async Task<string> GenerateIndexHtmlAsync() // TODO: Cache
{ {
// Load requested theme // Load requested theme
var theme = await ThemeRepository var theme = await ThemeRepository
@@ -70,7 +70,7 @@ public class FrontendService
.Distinct() .Distinct()
.ToArray(); .ToArray();
return await ComponentHelper.RenderComponent<FrontendPage>( return await ComponentHelper.RenderToHtmlAsync<FrontendPage>(
ServiceProvider, ServiceProvider,
parameters => parameters =>
{ {
@@ -82,7 +82,7 @@ public class FrontendService
); );
} }
public async Task<Stream> GenerateZip() // TODO: Rework to be able to extract everything successfully public async Task<Stream> GenerateZipAsync() // TODO: Rework to be able to extract everything successfully
{ {
// We only allow the access to this function when we are actually hosting the frontend // We only allow the access to this function when we are actually hosting the frontend
if (!Configuration.Frontend.EnableHosting) if (!Configuration.Frontend.EnableHosting)
@@ -109,16 +109,16 @@ public class FrontendService
var zipArchive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true); var zipArchive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true);
// Add wasm application // Add wasm application
await ArchiveFsItem(zipArchive, wasmPath, wasmPath); await ArchiveFsItemAsync(zipArchive, wasmPath, wasmPath);
// Add blazor files // Add blazor files
await ArchiveFsItem(zipArchive, blazorPath, blazorPath, "_framework/"); await ArchiveFsItemAsync(zipArchive, blazorPath, blazorPath, "_framework/");
// Add frontend.json // Add frontend.json
var frontendConfig = await GetConfiguration(); var frontendConfig = await GetConfigurationAsync();
frontendConfig.HostEnvironment = "Static"; frontendConfig.HostEnvironment = "Static";
var frontendJson = JsonSerializer.Serialize(frontendConfig); var frontendJson = JsonSerializer.Serialize(frontendConfig);
await ArchiveText(zipArchive, "frontend.json", frontendJson); await ArchiveTextAsync(zipArchive, "frontend.json", frontendJson);
// Finish zip archive and reset stream so the code calling this function can process it // Finish zip archive and reset stream so the code calling this function can process it
zipArchive.Dispose(); zipArchive.Dispose();
@@ -128,7 +128,7 @@ public class FrontendService
return memoryStream; return memoryStream;
} }
private async Task ArchiveFsItem(ZipArchive archive, string path, string prefixToRemove, string prefixToAdd = "") private async Task ArchiveFsItemAsync(ZipArchive archive, string path, string prefixToRemove, string prefixToAdd = "")
{ {
if (File.Exists(path)) if (File.Exists(path))
{ {
@@ -147,17 +147,17 @@ public class FrontendService
else else
{ {
foreach (var directoryItem in Directory.EnumerateFileSystemEntries(path)) foreach (var directoryItem in Directory.EnumerateFileSystemEntries(path))
await ArchiveFsItem(archive, directoryItem, prefixToRemove, prefixToAdd); await ArchiveFsItemAsync(archive, directoryItem, prefixToRemove, prefixToAdd);
} }
} }
private async Task ArchiveText(ZipArchive archive, string path, string content) private async Task ArchiveTextAsync(ZipArchive archive, string path, string content)
{ {
var data = Encoding.UTF8.GetBytes(content); var data = Encoding.UTF8.GetBytes(content);
await ArchiveBytes(archive, path, data); await ArchiveBytesAsync(archive, path, data);
} }
private async Task ArchiveBytes(ZipArchive archive, string path, byte[] bytes) private async Task ArchiveBytesAsync(ZipArchive archive, string path, byte[] bytes)
{ {
var entry = archive.CreateEntry(path); var entry = archive.CreateEntry(path);
await using var dataStream = entry.Open(); await using var dataStream = entry.Open();

View File

@@ -33,7 +33,7 @@ public class MetricsBackgroundService : BackgroundService
Metrics = metrics.ToArray(); Metrics = metrics.ToArray();
} }
private async Task Initialize() private async Task InitializeAsync()
{ {
Logger.LogDebug( Logger.LogDebug(
"Initializing metrics: {names}", "Initializing metrics: {names}",
@@ -41,12 +41,12 @@ public class MetricsBackgroundService : BackgroundService
); );
foreach (var metric in Metrics) foreach (var metric in Metrics)
await metric.Initialize(Meter); await metric.InitializeAsync(Meter);
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
await Initialize(); await InitializeAsync();
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
@@ -56,7 +56,7 @@ public class MetricsBackgroundService : BackgroundService
{ {
try try
{ {
await metric.Run(scope.ServiceProvider, stoppingToken); await metric.RunAsync(scope.ServiceProvider, stoppingToken);
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {

View File

@@ -33,7 +33,7 @@ public class UserAuthService
Extensions = extensions; Extensions = extensions;
} }
public async Task<bool> Sync(ClaimsPrincipal? principal) public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
{ {
// Ignore malformed claims principal // Ignore malformed claims principal
if (principal is not { Identity.IsAuthenticated: true }) if (principal is not { Identity.IsAuthenticated: true })
@@ -80,7 +80,7 @@ public class UserAuthService
permissions = ["*"]; permissions = ["*"];
} }
user = await UserRepository.Add(new User() user = await UserRepository.AddAsync(new User()
{ {
Email = email, Email = email,
TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1), TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1),
@@ -94,7 +94,7 @@ public class UserAuthService
if (user.Username != username) if (user.Username != username)
{ {
user.Username = username; user.Username = username;
await UserRepository.Update(user); await UserRepository.UpdateAsync(user);
} }
// Enrich claims with required metadata // Enrich claims with required metadata
@@ -107,7 +107,7 @@ public class UserAuthService
// Call extensions // Call extensions
foreach (var extension in Extensions) foreach (var extension in Extensions)
{ {
var result = await extension.Sync(user, principal); var result = await extension.SyncAsync(user, principal);
if (!result) // Exit immediately if result is false if (!result) // Exit immediately if result is false
return false; return false;
@@ -116,7 +116,7 @@ public class UserAuthService
return true; return true;
} }
public async Task<bool> Validate(ClaimsPrincipal? principal) public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
{ {
// Ignore malformed claims principal // Ignore malformed claims principal
if (principal is not { Identity.IsAuthenticated: true }) if (principal is not { Identity.IsAuthenticated: true })
@@ -157,7 +157,7 @@ public class UserAuthService
// Call extensions // Call extensions
foreach (var extension in Extensions) foreach (var extension in Extensions)
{ {
var result = await extension.Validate(user, principal); var result = await extension.ValidateAsync(user, principal);
if (!result) // Exit immediately if result is false if (!result) // Exit immediately if result is false
return false; return false;

View File

@@ -19,11 +19,11 @@ public class UserDeletionService
Handlers = handlers.ToArray(); Handlers = handlers.ToArray();
} }
public async Task<UserDeleteValidationResult> Validate(User user) public async Task<UserDeleteValidationResult> ValidateAsync(User user)
{ {
foreach (var handler in Handlers) foreach (var handler in Handlers)
{ {
var result = await handler.Validate(user); var result = await handler.ValidateAsync(user);
if (!result.IsAllowed) if (!result.IsAllowed)
return result; return result;
@@ -32,11 +32,11 @@ public class UserDeletionService
return UserDeleteValidationResult.Allow(); return UserDeleteValidationResult.Allow();
} }
public async Task Delete(User user, bool force) public async Task DeleteAsync(User user, bool force)
{ {
foreach (var handler in Handlers) foreach (var handler in Handlers)
await Delete(user, force); await handler.DeleteAsync(user, force);
await UserRepository.Remove(user); await UserRepository.RemoveAsync(user);
} }
} }

View File

@@ -13,7 +13,7 @@ namespace Moonlight.ApiServer.Startup;
public partial class Startup public partial class Startup
{ {
private Task RegisterAuth() private Task RegisterAuthAsync()
{ {
WebApplicationBuilder.Services WebApplicationBuilder.Services
.AddAuthentication(options => { options.DefaultScheme = "MainScheme"; }) .AddAuthentication(options => { options.DefaultScheme = "MainScheme"; })
@@ -62,7 +62,7 @@ public partial class Startup
.RequestServices .RequestServices
.GetRequiredService<ApiKeyAuthService>(); .GetRequiredService<ApiKeyAuthService>();
var result = await apiKeyAuthService.Validate(context.Principal); var result = await apiKeyAuthService.ValidateAsync(context.Principal);
if (!result) if (!result)
context.Fail("API key has been deleted"); context.Fail("API key has been deleted");
@@ -120,7 +120,7 @@ public partial class Startup
.RequestServices .RequestServices
.GetRequiredService<UserAuthService>(); .GetRequiredService<UserAuthService>();
var result = await userSyncService.Sync(context.Principal); var result = await userSyncService.SyncAsync(context.Principal);
if (!result) if (!result)
context.Principal = new(); context.Principal = new();
@@ -135,7 +135,7 @@ public partial class Startup
.RequestServices .RequestServices
.GetRequiredService<UserAuthService>(); .GetRequiredService<UserAuthService>();
var result = await userSyncService.Validate(context.Principal); var result = await userSyncService.ValidateAsync(context.Principal);
if (!result) if (!result)
context.RejectPrincipal(); context.RejectPrincipal();
@@ -178,7 +178,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task UseAuth() private Task UseAuthAsync()
{ {
WebApplication.UseAuthentication(); WebApplication.UseAuthentication();

View File

@@ -9,7 +9,7 @@ namespace Moonlight.ApiServer.Startup;
public partial class Startup public partial class Startup
{ {
private Task RegisterBase() private Task RegisterBaseAsync()
{ {
WebApplicationBuilder.Services.AutoAddServices<Startup>(); WebApplicationBuilder.Services.AutoAddServices<Startup>();
WebApplicationBuilder.Services.AddHttpClient(); WebApplicationBuilder.Services.AddHttpClient();
@@ -29,7 +29,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task UseBase() private Task UseBaseAsync()
{ {
WebApplication.UseRouting(); WebApplication.UseRouting();
WebApplication.UseExceptionHandler(); WebApplication.UseExceptionHandler();
@@ -37,7 +37,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task MapBase() private Task MapBaseAsync()
{ {
WebApplication.MapControllers(); WebApplication.MapControllers();
@@ -47,7 +47,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task ConfigureKestrel() private Task ConfigureKestrelAsync()
{ {
WebApplicationBuilder.WebHost.ConfigureKestrel(kestrelOptions => WebApplicationBuilder.WebHost.ConfigureKestrel(kestrelOptions =>
{ {

View File

@@ -8,11 +8,11 @@ namespace Moonlight.ApiServer.Startup;
public partial class Startup public partial class Startup
{ {
private async Task SetupAppConfiguration() private async Task SetupAppConfigurationAsync()
{ {
var configPath = Path.Combine("storage", "config.yml"); var configPath = Path.Combine("storage", "config.yml");
await YamlDefaultGenerator.Generate<AppConfiguration>(configPath); await YamlDefaultGenerator.GenerateAsync<AppConfiguration>(configPath);
// Configure configuration (wow) // Configure configuration (wow)
var configurationBuilder = new ConfigurationBuilder(); var configurationBuilder = new ConfigurationBuilder();
@@ -27,7 +27,7 @@ public partial class Startup
configurationRoot.Bind(Configuration); configurationRoot.Bind(Configuration);
} }
private Task RegisterAppConfiguration() private Task RegisterAppConfigurationAsync()
{ {
WebApplicationBuilder.Services.AddSingleton(Configuration); WebApplicationBuilder.Services.AddSingleton(Configuration);
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -6,7 +6,7 @@ namespace Moonlight.ApiServer.Startup;
public partial class Startup public partial class Startup
{ {
private Task RegisterDatabase() private Task RegisterDatabaseAsync()
{ {
WebApplicationBuilder.Services.AddDatabaseMappings(); WebApplicationBuilder.Services.AddDatabaseMappings();
WebApplicationBuilder.Services.AddServiceCollectionAccessor(); WebApplicationBuilder.Services.AddServiceCollectionAccessor();
@@ -16,9 +16,9 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task PrepareDatabase() private async Task PrepareDatabaseAsync()
{ {
await WebApplication.Services.EnsureDatabaseMigrated(); await WebApplication.Services.EnsureDatabaseMigratedAsync();
WebApplication.Services.GenerateDatabaseMappings(); WebApplication.Services.GenerateDatabaseMappings();
} }

View File

@@ -9,7 +9,7 @@ namespace Moonlight.ApiServer.Startup;
public partial class Startup public partial class Startup
{ {
private Task RegisterHangfire() private Task RegisterHangfireAsync()
{ {
WebApplicationBuilder.Services.AddHangfire((provider, configuration) => WebApplicationBuilder.Services.AddHangfire((provider, configuration) =>
{ {
@@ -38,7 +38,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task UseHangfire() private Task UseHangfireAsync()
{ {
if (WebApplication.Environment.IsDevelopment()) if (WebApplication.Environment.IsDevelopment())
WebApplication.UseHangfireDashboard(); WebApplication.UseHangfireDashboard();

View File

@@ -6,7 +6,7 @@ namespace Moonlight.ApiServer.Startup;
public partial class Startup public partial class Startup
{ {
private Task SetupLogging() private Task SetupLoggingAsync()
{ {
var loggerFactory = new LoggerFactory(); var loggerFactory = new LoggerFactory();
loggerFactory.AddAnsiConsole(); loggerFactory.AddAnsiConsole();
@@ -16,7 +16,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task RegisterLogging() private async Task RegisterLoggingAsync()
{ {
// Configure application logging // Configure application logging
WebApplicationBuilder.Logging.ClearProviders(); WebApplicationBuilder.Logging.ClearProviders();

View File

@@ -6,7 +6,7 @@ namespace Moonlight.ApiServer.Startup;
public partial class Startup public partial class Startup
{ {
private Task PrintVersion() private Task PrintVersionAsync()
{ {
// Fancy start console output... yes very fancy :> // Fancy start console output... yes very fancy :>
var rainbow = new Crayon.Rainbow(0.5); var rainbow = new Crayon.Rainbow(0.5);
@@ -25,7 +25,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task CreateStorage() private Task CreateStorageAsync()
{ {
Directory.CreateDirectory("storage"); Directory.CreateDirectory("storage");
Directory.CreateDirectory(Path.Combine("storage", "logs")); Directory.CreateDirectory(Path.Combine("storage", "logs"));
@@ -33,7 +33,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task RegisterCors() private Task RegisterCorsAsync()
{ {
var allowedOrigins = Configuration.Kestrel.AllowedOrigins; var allowedOrigins = Configuration.Kestrel.AllowedOrigins;
@@ -64,7 +64,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task UseCors() private Task UseCorsAsync()
{ {
WebApplication.UseCors(); WebApplication.UseCors();

View File

@@ -10,7 +10,7 @@ public partial class Startup
private IServiceProvider PluginLoadServiceProvider; private IServiceProvider PluginLoadServiceProvider;
private IPluginStartup[] PluginStartups; private IPluginStartup[] PluginStartups;
private Task InitializePlugins() private Task InitializePluginsAsync()
{ {
// Create service provider for starting up // Create service provider for starting up
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
@@ -28,13 +28,13 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task HookPluginBuild() private async Task HookPluginBuildAsync()
{ {
foreach (var pluginAppStartup in PluginStartups) foreach (var pluginAppStartup in PluginStartups)
{ {
try try
{ {
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder); await pluginAppStartup.BuildApplicationAsync(PluginLoadServiceProvider, WebApplicationBuilder);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -47,13 +47,13 @@ public partial class Startup
} }
} }
private async Task HookPluginConfigure() private async Task HookPluginConfigureAsync()
{ {
foreach (var pluginAppStartup in PluginStartups) foreach (var pluginAppStartup in PluginStartups)
{ {
try try
{ {
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication); await pluginAppStartup.ConfigureApplicationAsync(PluginLoadServiceProvider, WebApplication);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -66,13 +66,13 @@ public partial class Startup
} }
} }
private async Task HookPluginEndpoints() private async Task HookPluginEndpointsAsync()
{ {
foreach (var pluginEndpointStartup in PluginStartups) foreach (var pluginEndpointStartup in PluginStartups)
{ {
try try
{ {
await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication); await pluginEndpointStartup.ConfigureEndpointsAsync(PluginLoadServiceProvider, WebApplication);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -6,7 +6,7 @@ namespace Moonlight.ApiServer.Startup;
public partial class Startup public partial class Startup
{ {
public Task RegisterSignalR() public Task RegisterSignalRAsync()
{ {
var signalRBuilder = WebApplicationBuilder.Services.AddSignalR(); var signalRBuilder = WebApplicationBuilder.Services.AddSignalR();
@@ -16,7 +16,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task MapSignalR() public Task MapSignalRAsync()
{ {
WebApplication.MapHub<DiagnoseHub>("/api/admin/system/diagnose/ws"); WebApplication.MapHub<DiagnoseHub>("/api/admin/system/diagnose/ws");

View File

@@ -19,7 +19,7 @@ public partial class Startup
public WebApplication WebApplication { get; private set; } public WebApplication WebApplication { get; private set; }
public WebApplicationBuilder WebApplicationBuilder { get; private set; } public WebApplicationBuilder WebApplicationBuilder { get; private set; }
public Task Initialize(string[] args, IPluginStartup[]? plugins = null) public Task InitializeAsync(string[] args, IPluginStartup[]? plugins = null)
{ {
Args = args; Args = args;
PluginStartups = plugins ?? []; PluginStartups = plugins ?? [];
@@ -27,43 +27,43 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task AddMoonlight(WebApplicationBuilder builder) public async Task AddMoonlightAsync(WebApplicationBuilder builder)
{ {
WebApplicationBuilder = builder; WebApplicationBuilder = builder;
await PrintVersion(); await PrintVersionAsync();
await CreateStorage(); await CreateStorageAsync();
await SetupAppConfiguration(); await SetupAppConfigurationAsync();
await SetupLogging(); await SetupLoggingAsync();
await InitializePlugins(); await InitializePluginsAsync();
await ConfigureKestrel(); await ConfigureKestrelAsync();
await RegisterAppConfiguration(); await RegisterAppConfigurationAsync();
await RegisterLogging(); await RegisterLoggingAsync();
await RegisterBase(); await RegisterBaseAsync();
await RegisterDatabase(); await RegisterDatabaseAsync();
await RegisterAuth(); await RegisterAuthAsync();
await RegisterCors(); await RegisterCorsAsync();
await RegisterHangfire(); await RegisterHangfireAsync();
await RegisterSignalR(); await RegisterSignalRAsync();
await HookPluginBuild(); await HookPluginBuildAsync();
} }
public async Task AddMoonlight(WebApplication application) public async Task AddMoonlightAsync(WebApplication application)
{ {
WebApplication = application; WebApplication = application;
await PrepareDatabase(); await PrepareDatabaseAsync();
await UseCors(); await UseCorsAsync();
await UseBase(); await UseBaseAsync();
await UseAuth(); await UseAuthAsync();
await UseHangfire(); await UseHangfireAsync();
await HookPluginConfigure(); await HookPluginConfigureAsync();
await MapBase(); await MapBaseAsync();
await MapSignalR(); await MapSignalRAsync();
await HookPluginEndpoints(); await HookPluginEndpointsAsync();
} }
} }

View File

@@ -15,8 +15,8 @@
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.8" /> <PackageReference Include="MoonCore.PluginFramework" Version="1.0.8" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2" /> <PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.8" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.9" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<Import Project="Plugins.props" /> <Import Project="Plugins.props" />

View File

@@ -7,14 +7,14 @@ pluginLoader.Initialize();
var startup = new Startup(); var startup = new Startup();
await startup.Initialize(pluginLoader.Instances); await startup.InitializeAsync(pluginLoader.Instances);
var wasmHostBuilder = WebAssemblyHostBuilder.CreateDefault(args); var wasmHostBuilder = WebAssemblyHostBuilder.CreateDefault(args);
await startup.AddMoonlight(wasmHostBuilder); await startup.AddMoonlightAsync(wasmHostBuilder);
var wasmApp = wasmHostBuilder.Build(); var wasmApp = wasmHostBuilder.Build();
await startup.AddMoonlight(wasmApp); await startup.AddMoonlightAsync(wasmApp);
await wasmApp.RunAsync(); await wasmApp.RunAsync();

View File

@@ -100,18 +100,38 @@
.select { .select {
@apply !border-base-content/20 border-2 ring-0! outline-0! focus:border-primary! focus-within:border-primary! bg-base-200/50; @apply !border-base-content/20 border-2 ring-0! outline-0! focus:border-primary! focus-within:border-primary! bg-base-200/50;
} }
.table { .table {
:where(th, td) { :where(th, td) {
@apply py-1.5; @apply py-1.5;
} }
} }
.dropdown-item { .dropdown-item {
@apply px-2.5 py-1.5 text-sm; @apply px-2.5 py-1.5 text-sm;
} }
.dropdown-menu { .dropdown-menu {
@apply bg-base-150; @apply bg-base-150;
} }
.advance-select-menu {
@apply !border-base-content/20 border-2 ring-0! outline-0! bg-base-200/50 !px-0;
}
.advance-select-option {
@apply !rounded-none hover:!bg-primary;
}
.advance-select-option.selected {
@apply !bg-primary !text-primary-content;
}
.advance-select-toggle {
@apply !border-base-content/20 border-2 ring-0! outline-0! focus:border-primary! focus-within:border-primary! bg-base-200/50;
}
.table thead {
@apply !normal-case;
}
} }

View File

@@ -7,7 +7,7 @@ namespace Moonlight.Client.Implementations;
public class CoreStartup : IPluginStartup public class CoreStartup : IPluginStartup
{ {
public Task BuildApplication(IServiceProvider serviceProvider, WebAssemblyHostBuilder builder) public Task BuildApplicationAsync(IServiceProvider serviceProvider, WebAssemblyHostBuilder builder)
{ {
builder.Services.AddSingleton<ISidebarItemProvider, DefaultSidebarItemProvider>(); builder.Services.AddSingleton<ISidebarItemProvider, DefaultSidebarItemProvider>();
builder.Services.AddSingleton<IOverviewElementProvider, DefaultOverviewElementProvider>(); builder.Services.AddSingleton<IOverviewElementProvider, DefaultOverviewElementProvider>();
@@ -15,6 +15,6 @@ public class CoreStartup : IPluginStartup
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task ConfigureApplication(IServiceProvider serviceProvider, WebAssemblyHost app) public Task ConfigureApplicationAsync(IServiceProvider serviceProvider, WebAssemblyHost app)
=> Task.CompletedTask; => Task.CompletedTask;
} }

View File

@@ -11,7 +11,7 @@ public class LogErrorFilter : IGlobalErrorFilter
Logger = logger; Logger = logger;
} }
public Task<bool> HandleException(Exception ex) public Task<bool> HandleExceptionAsync(Exception ex)
{ {
Logger.LogError(ex, "Global error processed"); Logger.LogError(ex, "Global error processed");
return Task.FromResult(false); return Task.FromResult(false);

View File

@@ -18,21 +18,21 @@ public class SystemFsAccess : IFsAccess, ICombineAccess, IArchiveAccess, IDownlo
ApiClient = apiClient; ApiClient = apiClient;
} }
public async Task CreateFile(string path) public async Task CreateFileAsync(string path)
{ {
await ApiClient.Post( await ApiClient.Post(
$"{BaseApiUrl}/touch?path={path}" $"{BaseApiUrl}/touch?path={path}"
); );
} }
public async Task CreateDirectory(string path) public async Task CreateDirectoryAsync(string path)
{ {
await ApiClient.Post( await ApiClient.Post(
$"{BaseApiUrl}/mkdir?path={path}" $"{BaseApiUrl}/mkdir?path={path}"
); );
} }
public async Task<FsEntry[]> List(string path) public async Task<FsEntry[]> ListAsync(string path)
{ {
var entries = await ApiClient.GetJson<FileSystemEntryResponse[]>( var entries = await ApiClient.GetJson<FileSystemEntryResponse[]>(
$"{BaseApiUrl}/list?path={path}" $"{BaseApiUrl}/list?path={path}"
@@ -48,14 +48,14 @@ public class SystemFsAccess : IFsAccess, ICombineAccess, IArchiveAccess, IDownlo
}).ToArray(); }).ToArray();
} }
public async Task Move(string oldPath, string newPath) public async Task MoveAsync(string oldPath, string newPath)
{ {
await ApiClient.Post( await ApiClient.Post(
$"{BaseApiUrl}/move?oldPath={oldPath}&newPath={newPath}" $"{BaseApiUrl}/move?oldPath={oldPath}&newPath={newPath}"
); );
} }
public async Task Read(string path, Func<Stream, Task> onHandleData) public async Task ReadAsync(string path, Func<Stream, Task> onHandleData)
{ {
await using var stream = await ApiClient.GetStream( await using var stream = await ApiClient.GetStream(
$"{BaseApiUrl}/download?path={path}" $"{BaseApiUrl}/download?path={path}"
@@ -66,7 +66,7 @@ public class SystemFsAccess : IFsAccess, ICombineAccess, IArchiveAccess, IDownlo
stream.Close(); stream.Close();
} }
public async Task Write(string path, Stream dataStream) public async Task WriteAsync(string path, Stream dataStream)
{ {
using var multiPartForm = new MultipartFormDataContent(); using var multiPartForm = new MultipartFormDataContent();
@@ -78,14 +78,14 @@ public class SystemFsAccess : IFsAccess, ICombineAccess, IArchiveAccess, IDownlo
); );
} }
public async Task Delete(string path) public async Task DeleteAsync(string path)
{ {
await ApiClient.Delete( await ApiClient.Delete(
$"{BaseApiUrl}/delete?path={path}" $"{BaseApiUrl}/delete?path={path}"
); );
} }
public async Task Combine(string destination, string[] files) public async Task CombineAsync(string destination, string[] files)
{ {
await ApiClient.Post( await ApiClient.Post(
$"{BaseApiUrl}/combine", $"{BaseApiUrl}/combine",
@@ -103,7 +103,7 @@ public class SystemFsAccess : IFsAccess, ICombineAccess, IArchiveAccess, IDownlo
new("tar.gz", ["tar.gz"], "Tar.gz Archive") new("tar.gz", ["tar.gz"], "Tar.gz Archive")
]; ];
public async Task Archive( public async Task ArchiveAsync(
string destination, string destination,
ArchiveFormat format, ArchiveFormat format,
string root, string root,
@@ -120,7 +120,7 @@ public class SystemFsAccess : IFsAccess, ICombineAccess, IArchiveAccess, IDownlo
}); });
} }
public async Task Unarchive( public async Task UnarchiveAsync(
string path, string path,
ArchiveFormat format, ArchiveFormat format,
string destination, string destination,
@@ -137,13 +137,13 @@ public class SystemFsAccess : IFsAccess, ICombineAccess, IArchiveAccess, IDownlo
); );
} }
public async Task<string> GetFileUrl(string path) public async Task<string> GetFileUrlAsync(string path)
=> await GetDownloadUrl(path); => await GetDownloadUrlAsync(path);
public async Task<string> GetFolderUrl(string path) public async Task<string> GetFolderUrlAsync(string path)
=> await GetDownloadUrl(path); => await GetDownloadUrlAsync(path);
private async Task<string> GetDownloadUrl(string path) private async Task<string> GetDownloadUrlAsync(string path)
{ {
var response = await ApiClient.PostJson<DownloadUrlResponse>( var response = await ApiClient.PostJson<DownloadUrlResponse>(
$"{BaseApiUrl}/downloadUrl?path={path}" $"{BaseApiUrl}/downloadUrl?path={path}"

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MoonCore.Blazor.FlyonUi.Exceptions; using MoonCore.Blazor.FlyonUi.Exceptions;
using MoonCore.Blazor.FlyonUi.Toasts;
using MoonCore.Exceptions; using MoonCore.Exceptions;
namespace Moonlight.Client.Implementations; namespace Moonlight.Client.Implementations;
@@ -7,18 +8,25 @@ namespace Moonlight.Client.Implementations;
public class UnauthenticatedErrorFilter : IGlobalErrorFilter public class UnauthenticatedErrorFilter : IGlobalErrorFilter
{ {
private readonly NavigationManager Navigation; private readonly NavigationManager Navigation;
private readonly ToastService ToastService;
public UnauthenticatedErrorFilter(NavigationManager navigation) public UnauthenticatedErrorFilter(
NavigationManager navigation,
ToastService toastService
)
{ {
Navigation = navigation; Navigation = navigation;
ToastService = toastService;
} }
public Task<bool> HandleException(Exception ex) public async Task<bool> HandleExceptionAsync(Exception ex)
{ {
if (ex is not HttpApiException { Status: 401 }) if (ex is not HttpApiException { Status: 401 })
return Task.FromResult(false); return false;
await ToastService.InfoAsync("Session expired", "Your session has expired. Reloading..");
Navigation.NavigateTo("/api/auth/logout", true); Navigation.NavigateTo("/api/auth/logout", true);
return Task.FromResult(true); return true;
} }
} }

View File

@@ -12,7 +12,7 @@
<PropertyGroup> <PropertyGroup>
<PackageTags>frontend</PackageTags> <PackageTags>frontend</PackageTags>
<PackageId>Moonlight.Client</PackageId> <PackageId>Moonlight.Client</PackageId>
<Version>2.1.10</Version> <Version>2.1.11</Version>
<Authors>Moonlight Panel</Authors> <Authors>Moonlight Panel</Authors>
<Description>A build of the client for moonlight development</Description> <Description>A build of the client for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl> <PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -22,11 +22,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazor-ApexCharts" Version="6.0.2" /> <PackageReference Include="Blazor-ApexCharts" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.9" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
<PackageReference Include="MoonCore" Version="1.9.7" /> <PackageReference Include="MoonCore" Version="2.0.1" />
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" /> <PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.2.5" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.1.9" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" /> <PackageReference Include="System.Security.Claims" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -4,6 +4,6 @@ namespace Moonlight.Client.Plugins;
public interface IPluginStartup public interface IPluginStartup
{ {
public Task BuildApplication(IServiceProvider serviceProvider, WebAssemblyHostBuilder builder); public Task BuildApplicationAsync(IServiceProvider serviceProvider, WebAssemblyHostBuilder builder);
public Task ConfigureApplication(IServiceProvider serviceProvider, WebAssemblyHost app); public Task ConfigureApplicationAsync(IServiceProvider serviceProvider, WebAssemblyHost app);
} }

View File

@@ -17,21 +17,14 @@ public class ThemeService
ApiClient = apiClient; ApiClient = apiClient;
} }
public async Task<PagedData<ThemeResponse>> Get(int page, int pageSize) public async Task<ThemeResponse> GetAsync(int id)
{
return await ApiClient.GetJson<PagedData<ThemeResponse>>(
$"api/admin/system/customisation/themes?page={page}&pageSize={pageSize}"
);
}
public async Task<ThemeResponse> Get(int id)
{ {
return await ApiClient.GetJson<ThemeResponse>( return await ApiClient.GetJson<ThemeResponse>(
$"api/admin/system/customisation/themes/{id}" $"api/admin/system/customisation/themes/{id}"
); );
} }
public async Task<ThemeResponse> Create(CreateThemeRequest request) public async Task<ThemeResponse> CreateAsync(CreateThemeRequest request)
{ {
return await ApiClient.PostJson<ThemeResponse>( return await ApiClient.PostJson<ThemeResponse>(
"api/admin/system/customisation/themes", "api/admin/system/customisation/themes",
@@ -39,7 +32,7 @@ public class ThemeService
); );
} }
public async Task<ThemeResponse> Update(int id, UpdateThemeRequest request) public async Task<ThemeResponse> UpdateAsync(int id, UpdateThemeRequest request)
{ {
return await ApiClient.PatchJson<ThemeResponse>( return await ApiClient.PatchJson<ThemeResponse>(
$"api/admin/system/customisation/themes/{id}", $"api/admin/system/customisation/themes/{id}",
@@ -47,7 +40,7 @@ public class ThemeService
); );
} }
public async Task Delete(int id) public async Task DeleteAsync(int id)
{ {
await ApiClient.Delete( await ApiClient.Delete(
$"api/admin/system/customisation/themes/{id}" $"api/admin/system/customisation/themes/{id}"

View File

@@ -9,9 +9,9 @@ public class WindowService
JsRuntime = jsRuntime; JsRuntime = jsRuntime;
} }
public async Task Open(string url, string title, int height, int width) public async Task OpenAsync(string url, string title, int height, int width)
=> await JsRuntime.InvokeVoidAsync("moonlight.window.open", url, title, height, width); => await JsRuntime.InvokeVoidAsync("moonlight.window.open", url, title, height, width);
public async Task Close() public async Task CloseAsync()
=> await JsRuntime.InvokeVoidAsync("moonlight.window.closeCurrent"); => await JsRuntime.InvokeVoidAsync("moonlight.window.closeCurrent");
} }

View File

@@ -9,7 +9,7 @@ namespace Moonlight.Client.Startup;
public partial class Startup public partial class Startup
{ {
private Task RegisterAuthentication() private Task RegisterAuthenticationAsync()
{ {
WebAssemblyHostBuilder.Services.AddAuthorizationCore(); WebAssemblyHostBuilder.Services.AddAuthorizationCore();
WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState(); WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState();

View File

@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using MoonCore.Blazor.FlyonUi; using MoonCore.Blazor.FlyonUi;
using MoonCore.Blazor.Services;
using MoonCore.Extensions; using MoonCore.Extensions;
using MoonCore.Helpers; using MoonCore.Helpers;
using Moonlight.Client.Services; using Moonlight.Client.Services;
@@ -10,7 +9,7 @@ namespace Moonlight.Client.Startup;
public partial class Startup public partial class Startup
{ {
private Task RegisterBase() private Task RegisterBaseAsync()
{ {
WebAssemblyHostBuilder.RootComponents.Add<App>("#app"); WebAssemblyHostBuilder.RootComponents.Add<App>("#app");
WebAssemblyHostBuilder.RootComponents.Add<HeadOutlet>("head::after"); WebAssemblyHostBuilder.RootComponents.Add<HeadOutlet>("head::after");

View File

@@ -7,7 +7,7 @@ namespace Moonlight.Client.Startup;
public partial class Startup public partial class Startup
{ {
private Task SetupLogging() private Task SetupLoggingAsync()
{ {
var loggerFactory = new LoggerFactory(); var loggerFactory = new LoggerFactory();
@@ -18,7 +18,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task RegisterLogging() private Task RegisterLoggingAsync()
{ {
WebAssemblyHostBuilder.Logging.ClearProviders(); WebAssemblyHostBuilder.Logging.ClearProviders();
WebAssemblyHostBuilder.Logging.AddAnsiConsole(); WebAssemblyHostBuilder.Logging.AddAnsiConsole();

View File

@@ -6,7 +6,7 @@ namespace Moonlight.Client.Startup;
public partial class Startup public partial class Startup
{ {
private Task PrintVersion() private Task PrintVersionAsync()
{ {
// Fancy start console output... yes very fancy :> // Fancy start console output... yes very fancy :>
Console.Write("Running "); Console.Write("Running ");
@@ -27,7 +27,7 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task LoadConfiguration() private async Task LoadConfigurationAsync()
{ {
try try
{ {

View File

@@ -10,7 +10,7 @@ public partial class Startup
private IPluginStartup[] PluginStartups; private IPluginStartup[] PluginStartups;
private IServiceProvider PluginLoadServiceProvider; private IServiceProvider PluginLoadServiceProvider;
private Task InitializePlugins() private Task InitializePluginsAsync()
{ {
// Define minimal service collection // Define minimal service collection
var startupSc = new ServiceCollection(); var startupSc = new ServiceCollection();
@@ -38,13 +38,13 @@ public partial class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task HookPluginBuild() private async Task HookPluginBuildAsync()
{ {
foreach (var pluginAppStartup in PluginStartups) foreach (var pluginAppStartup in PluginStartups)
{ {
try try
{ {
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebAssemblyHostBuilder); await pluginAppStartup.BuildApplicationAsync(PluginLoadServiceProvider, WebAssemblyHostBuilder);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -57,13 +57,13 @@ public partial class Startup
} }
} }
private async Task HookPluginConfigure() private async Task HookPluginConfigureAsync()
{ {
foreach (var pluginAppStartup in PluginStartups) foreach (var pluginAppStartup in PluginStartups)
{ {
try try
{ {
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebAssemblyHost); await pluginAppStartup.ConfigureApplicationAsync(PluginLoadServiceProvider, WebAssemblyHost);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -16,33 +16,33 @@ public partial class Startup
public FrontendConfiguration Configuration { get; private set; } public FrontendConfiguration Configuration { get; private set; }
public Task Initialize(IPluginStartup[]? plugins = null) public Task InitializeAsync(IPluginStartup[]? plugins = null)
{ {
PluginStartups = plugins ?? []; PluginStartups = plugins ?? [];
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task AddMoonlight(WebAssemblyHostBuilder builder) public async Task AddMoonlightAsync(WebAssemblyHostBuilder builder)
{ {
WebAssemblyHostBuilder = builder; WebAssemblyHostBuilder = builder;
await PrintVersion(); await PrintVersionAsync();
await SetupLogging(); await SetupLoggingAsync();
await LoadConfiguration(); await LoadConfigurationAsync();
await InitializePlugins(); await InitializePluginsAsync();
await RegisterLogging(); await RegisterLoggingAsync();
await RegisterBase(); await RegisterBaseAsync();
await RegisterAuthentication(); await RegisterAuthenticationAsync();
await HookPluginBuild(); await HookPluginBuildAsync();
} }
public async Task AddMoonlight(WebAssemblyHost assemblyHost) public async Task AddMoonlightAsync(WebAssemblyHost assemblyHost)
{ {
WebAssemblyHost = assemblyHost; WebAssemblyHost = assemblyHost;
await HookPluginConfigure(); await HookPluginConfigureAsync();
} }
} }

View File

@@ -1,518 +0,0 @@
!bg-base-100
!border-base-content/40
!border-none
!flex
!font-medium
!font-semibold
!h-2.5
!justify-between
!me-1.5
!ms-auto
!px-2.5
!rounded-full
!text-sm
!w-2.5
*:[grid-area:1/1]
*:first:rounded-tl-lg
*:last:rounded-tr-lg
-left-4
-ml-4
-translate-x-full
-translate-y-1/2
absolute
accordion
accordion-bordered
accordion-toggle
active
active-tab:bg-primary
active-tab:text-base-content
advance-select-menu
advance-select-option
advance-select-tag
advance-select-toggle
alert
alert-error
alert-outline
alert-soft
align-bottom
align-middle
animate-bounce
animate-ping
aria-[current='page']:text-bg-soft-primary
avatar
avatar-away-bottom
avatar-away-top
avatar-busy-bottom
avatar-busy-top
avatar-offline-bottom
avatar-offline-top
avatar-online-bottom
avatar-online-top
avatar-placeholder
badge
badge-error
badge-info
badge-outline
badge-primary
badge-soft
badge-success
bg-background
bg-background/60
bg-base-100
bg-base-150
bg-base-200
bg-base-200/50
bg-base-300
bg-base-300/45
bg-base-300/50
bg-base-300/60
bg-error
bg-info
bg-primary
bg-primary/5
bg-success
bg-transparent
bg-warning
block
blur
border
border-0
border-2
border-b
border-base-content
border-base-content/20
border-base-content/25
border-base-content/40
border-base-content/5
border-dashed
border-t
border-transparent
bottom-0
bottom-full
break-words
btn
btn-accent
btn-active
btn-circle
btn-disabled
btn-error
btn-info
btn-outline
btn-primary
btn-secondary
btn-sm
btn-soft
btn-square
btn-success
btn-text
btn-warning
card
card-alert
card-body
card-border
card-footer
card-header
card-title
carousel
carousel-body
carousel-next
carousel-prev
carousel-slide
chat
chat-avatar
chat-bubble
chat-footer
chat-header
chat-receiver
chat-sender
checkbox
checkbox-primary
checkbox-xs
col-span-1
collapse
combo-box-selected:block
combo-box-selected:dropdown-active
complete
container
contents
cursor-default
cursor-not-allowed
cursor-pointer
diff
disabled
divide-base-150/60
divide-y
drop-shadow
dropdown
dropdown-disabled
dropdown-item
dropdown-menu
dropdown-open:opacity-100
dropdown-open:rotate-180
dropdown-toggle
duration-300
duration-500
ease-in-out
ease-linear
end-3
file-upload-complete:progress-success
fill-black
filter
filter-reset
fixed
flex
flex-1
flex-col
flex-grow
flex-nowrap
flex-row
flex-shrink-0
flex-wrap
focus-visible:outline-none
focus-within:border-primary
focus:border-primary
focus:outline-1
focus:outline-none
focus:outline-primary
focus:ring-0
font-bold
font-inter
font-medium
font-normal
font-semibold
gap-0.5
gap-1
gap-1.5
gap-2
gap-3
gap-4
gap-5
gap-6
gap-x-1
gap-x-2
gap-x-3
gap-y-1
gap-y-3
grid
grid-cols-1
grid-flow-col
grow
grow-0
h-12
h-2
h-32
h-64
h-8
h-auto
h-full
h-screen
helper-text
hidden
hover:bg-primary/5
hover:bg-transparent
hover:text-base-content
hover:text-base-content/60
hover:text-primary
image-full
inline
inline-block
inline-flex
inline-grid
input
input-floating
input-floating-label
input-lg
input-md
input-sm
input-xl
inset-0
inset-y-0
inset-y-2
invisible
is-invalid
is-valid
isolate
italic
items-center
items-end
items-start
join
join-item
justify-between
justify-center
justify-end
justify-start
justify-stretch
label-text
leading-3
leading-3.5
leading-6
left-0
lg:bg-base-100/20
lg:flex
lg:gap-y-0
lg:grid-cols-2
lg:hidden
lg:justify-end
lg:justify-start
lg:min-w-0
lg:p-10
lg:pb-5
lg:pl-64
lg:pr-3.5
lg:pt-5
lg:ring-1
lg:ring-base-content/10
lg:rounded-lg
lg:shadow-xs
list-disc
list-inside
loading
loading-lg
loading-sm
loading-spinner
loading-xl
loading-xs
lowercase
m-10
mask
max-h-52
max-lg:flex-col
max-lg:hidden
max-w-7xl
max-w-80
max-w-full
max-w-lg
max-w-sm
max-w-xl
mb-0.5
mb-1
mb-2
mb-3
mb-4
mb-5
md:table-cell
md:text-3xl
me-1
me-1.5
me-2
me-2.5
me-5
menu
menu-active
menu-disabled
menu-dropdown
menu-dropdown-show
menu-focus
menu-horizontal
menu-title
min-h-0
min-h-svh
min-w-0
min-w-28
min-w-48
min-w-60
min-w-[100px]
ml-3
ml-4
modal
modal-content
modal-dialog
modal-middle
modal-title
mr-4
ms-1
ms-2
ms-3
mt-1
mt-1.5
mt-10
mt-12
mt-2
mt-2.5
mt-3
mt-4
mt-5
mt-8
mx-1
mx-auto
my-3
my-auto
opacity-0
opacity-100
open
origin-top-left
outline
outline-0
overflow-hidden
overflow-x-auto
overflow-y-auto
overlay-open:duration-50
overlay-open:opacity-100
p-0.5
p-1
p-2
p-3
p-4
p-5
p-6
p-8
pin-input
pin-input-underline
placeholder-base-content/60
pointer-events-auto
pointer-events-none
progress
progress-bar
progress-indeterminate
progress-primary
pt-0
pt-0.5
pt-3
px-1.5
px-2
px-2.5
px-3
px-4
px-5
py-0.5
py-1.5
py-2
py-2.5
py-6
radio
range
relative
resize
ring-0
ring-1
ring-white/10
rounded-box
rounded-field
rounded-full
rounded-lg
rounded-md
rounded-t-lg
row-active
row-hover
rtl:!mr-0
select
select-disabled:opacity-40
select-disabled:pointer-events-none
select-floating
select-floating-label
selected
selected:select-active
shadow-base-300/20
shadow-lg
shadow-xs
shrink-0
size-10
size-4
size-5
size-8
skeleton
skeleton-animated
sm:auto-cols-max
sm:flex
sm:items-center
sm:items-end
sm:justify-between
sm:justify-end
sm:max-w-2xl
sm:max-w-3xl
sm:max-w-4xl
sm:max-w-5xl
sm:max-w-6xl
sm:max-w-7xl
sm:max-w-lg
sm:max-w-md
sm:max-w-xl
sm:mb-0
sm:mt-5
sm:mt-6
sm:p-6
sm:py-2
sm:text-sm/5
space-x-1
space-y-1
space-y-4
sr-only
static
status
status-error
sticky
switch
tab
tab-active
table
table-pin-cols
table-pin-rows
tabs
tabs-bordered
tabs-lg
tabs-lifted
tabs-md
tabs-sm
tabs-xl
tabs-xs
text-2xl
text-4xl
text-accent
text-base
text-base-content
text-base-content/40
text-base-content/50
text-base-content/60
text-base-content/70
text-base-content/80
text-base/6
text-center
text-error
text-error-content
text-gray-400
text-info
text-info-content
text-left
text-lg
text-primary
text-primary-content
text-sm
text-sm/5
text-success
text-success-content
text-warning
text-warning-content
text-xl
text-xs
text-xs/5
textarea
textarea-floating
textarea-floating-label
theme-controller
tooltip
tooltip-content
top-0
top-1/2
top-full
transform
transition
transition-all
transition-opacity
translate-x-0
truncate
underline
uppercase
validate
w-0
w-0.5
w-12
w-4
w-56
w-64
w-fit
w-full
whitespace-nowrap
z-10
z-40
z-50

View File

@@ -21,6 +21,8 @@
-ml-4 -ml-4
-translate-x-full -translate-x-full
-translate-y-1/2 -translate-y-1/2
[animation-duration:0.8s]
[animation-timing-function:ease]
absolute absolute
accordion accordion
accordion-bordered accordion-bordered
@@ -42,8 +44,10 @@ align-bottom
align-middle align-middle
animate-bounce animate-bounce
animate-ping animate-ping
animate-spin
aria-[current='page']:text-bg-soft-primary aria-[current='page']:text-bg-soft-primary
avatar avatar
avatar-placeholder
badge badge
badge-error badge-error
badge-info badge-info
@@ -73,18 +77,33 @@ block
blur blur
border border
border-0 border-0
border-1
border-2 border-2
border-3
border-b border-b
border-b-2
border-b-base-content/20
border-b-primary
border-base-content border-base-content
border-base-content/20 border-base-content/20
border-base-content/25 border-base-content/25
border-base-content/40 border-base-content/40
border-base-content/5 border-base-content/5
border-dashed border-dashed
border-dotted
border-error/30
border-info/30
border-l-transparent
border-r-transparent
border-solid
border-success/30
border-t border-t
border-t-transparent
border-transparent border-transparent
border-warning/30
bottom-0 bottom-0
bottom-full bottom-full
breadcrumbs
break-words break-words
btn btn
btn-accent btn-accent
@@ -93,7 +112,6 @@ btn-circle
btn-disabled btn-disabled
btn-error btn-error
btn-info btn-info
btn-outline
btn-primary btn-primary
btn-secondary btn-secondary
btn-sm btn-sm
@@ -153,6 +171,7 @@ duration-500
ease-in-out ease-in-out
ease-linear ease-linear
end-3 end-3
error-message
file-upload-complete:progress-success file-upload-complete:progress-success
fill-base-content fill-base-content
fill-black fill-black
@@ -180,6 +199,8 @@ font-inter
font-medium font-medium
font-normal font-normal
font-semibold font-semibold
footer
footer-center
gap-0.5 gap-0.5
gap-1 gap-1
gap-1.5 gap-1.5
@@ -200,7 +221,7 @@ grid-cols-4
grid-flow-col grid-flow-col
grow grow
grow-0 grow-0
h-12 h-10
h-2 h-2
h-3 h-3
h-32 h-32
@@ -213,10 +234,13 @@ helper-text
hidden hidden
hover:bg-primary/5 hover:bg-primary/5
hover:bg-transparent hover:bg-transparent
hover:cursor-pointer
hover:text-base-content hover:text-base-content
hover:text-base-content/60 hover:text-base-content/60
hover:text-primary hover:text-primary
image-full image-full
indicator
indicator-item
inline inline
inline-block inline-block
inline-flex inline-flex
@@ -245,11 +269,9 @@ justify-between
justify-center justify-center
justify-end justify-end
justify-start justify-start
justify-stretch
label-text label-text
leading-3 leading-3
leading-3.5 leading-3.5
leading-6
leading-none leading-none
left-0 left-0
lg:bg-base-100/20 lg:bg-base-100/20
@@ -276,7 +298,6 @@ list-disc
list-inside list-inside
list-none list-none
loading loading
loading-lg
loading-sm loading-sm
loading-spinner loading-spinner
loading-xl loading-xl
@@ -287,7 +308,11 @@ mask
max-h-52 max-h-52
max-lg:flex-col max-lg:flex-col
max-lg:hidden max-lg:hidden
max-md:flex-wrap
max-md:justify-center
max-sm:hidden
max-w-7xl max-w-7xl
max-w-8
max-w-80 max-w-80
max-w-full max-w-full
max-w-lg max-w-lg
@@ -323,6 +348,7 @@ min-w-28
min-w-48 min-w-48
min-w-60 min-w-60
min-w-[100px] min-w-[100px]
min-w-full
min-w-sm min-w-sm
ml-3 ml-3
ml-4 ml-4
@@ -330,10 +356,10 @@ modal
modal-content modal-content
modal-dialog modal-dialog
modal-middle modal-middle
modal-title
mr-4 mr-4
ms-0.5 ms-0.5
ms-1 ms-1
ms-1.5
ms-2 ms-2
ms-3 ms-3
ms-auto ms-auto
@@ -351,10 +377,12 @@ mt-8
mx-1 mx-1
mx-auto mx-auto
my-3 my-3
my-5
my-auto my-auto
object-cover object-cover
opacity-0 opacity-0
opacity-100 opacity-100
opacity-75
open open
origin-top-left origin-top-left
outline outline
@@ -368,11 +396,13 @@ p-0.5
p-1 p-1
p-1.5 p-1.5
p-2 p-2
p-2.5
p-3 p-3
p-4 p-4
p-5 p-5
p-6 p-6
p-8 p-8
pb-1
pin-input pin-input
placeholder-base-content/60 placeholder-base-content/60
pointer-events-auto pointer-events-auto
@@ -383,6 +413,7 @@ progress-indeterminate
progress-primary progress-primary
pt-0 pt-0
pt-0.5 pt-0.5
pt-2
pt-3 pt-3
px-1.5 px-1.5
px-2 px-2
@@ -418,6 +449,7 @@ select-disabled:opacity-40
select-disabled:pointer-events-none select-disabled:pointer-events-none
select-floating select-floating
select-floating-label select-floating-label
select-sm
selected selected
selected:select-active selected:select-active
shadow-base-300/20 shadow-base-300/20
@@ -426,6 +458,7 @@ shadow-md
shadow-xs shadow-xs
shrink-0 shrink-0
size-10 size-10
size-12
size-4 size-4
size-5 size-5
size-8 size-8
@@ -448,18 +481,20 @@ sm:max-w-md
sm:max-w-xl sm:max-w-xl
sm:mb-0 sm:mb-0
sm:mt-5 sm:mt-5
sm:mt-6
sm:p-6 sm:p-6
sm:py-2 sm:py-2
sm:text-sm/5 sm:text-sm/5
space-x-1 space-x-1
space-x-2.5
space-y-1 space-y-1
space-y-4 space-y-4
sr-only sr-only
stack
static static
status status
status-error status-error
sticky sticky
success-message
switch switch
tab tab
tab-active tab-active
@@ -495,6 +530,7 @@ text-left
text-lg text-lg
text-primary text-primary
text-primary-content text-primary-content
text-right
text-sm text-sm
text-sm/5 text-sm/5
text-success text-success
@@ -525,6 +561,7 @@ validate
w-0 w-0
w-0.5 w-0.5
w-12 w-12
w-13
w-4 w-4
w-56 w-56
w-64 w-64

View File

@@ -9,7 +9,9 @@
!me-1.5 !me-1.5
!ms-auto !ms-auto
!px-2.5 !px-2.5
!py-0.5
!rounded-full !rounded-full
!rounded-xs
!text-sm !text-sm
!w-2.5 !w-2.5
*:[grid-area:1/1] *:[grid-area:1/1]
@@ -19,13 +21,17 @@
-ml-4 -ml-4
-translate-x-full -translate-x-full
-translate-y-1/2 -translate-y-1/2
[animation-duration:0.8s]
[animation-timing-function:ease]
absolute absolute
accordion accordion
accordion-bordered accordion-bordered
accordion-toggle accordion-toggle
active active
active-tab:bg-primary active-tab:bg-primary
active-tab:hover:text-primary-content
active-tab:text-base-content active-tab:text-base-content
active-tab:text-primary-content
advance-select-menu advance-select-menu
advance-select-option advance-select-option
advance-select-tag advance-select-tag
@@ -39,6 +45,7 @@ align-bottom
align-middle align-middle
animate-bounce animate-bounce
animate-ping animate-ping
animate-spin
aria-[current='page']:text-bg-soft-primary aria-[current='page']:text-bg-soft-primary
avatar avatar
avatar-away-bottom avatar-away-bottom
@@ -62,6 +69,7 @@ bg-background/60
bg-base-100 bg-base-100
bg-base-150 bg-base-150
bg-base-200 bg-base-200
bg-base-200!
bg-base-200/50 bg-base-200/50
bg-base-300 bg-base-300
bg-base-300/45 bg-base-300/45
@@ -85,9 +93,14 @@ block
blur blur
border border
border-0 border-0
border-1
border-2 border-2
border-3
border-accent border-accent
border-b border-b
border-b-2
border-b-base-content/20
border-b-primary
border-base-content border-base-content
border-base-content/20 border-base-content/20
border-base-content/25 border-base-content/25
@@ -96,15 +109,26 @@ border-base-content/5
border-base-content/60 border-base-content/60
border-base-content/70 border-base-content/70
border-dashed border-dashed
border-dotted
border-e-2 border-e-2
border-error/30
border-info/30
border-l-4 border-l-4
border-l-transparent
border-primary border-primary
border-r-transparent
border-solid
border-success border-success
border-success/30
border-t border-t
border-t-2 border-t-2
border-t-transparent
border-transparent border-transparent
border-warning/30
bottom-0 bottom-0
bottom-full bottom-full
breadcrumbs
breadcrumbs-separator
break-words break-words
btn btn
btn-accent btn-accent
@@ -164,8 +188,10 @@ diff
disabled disabled
divide-base-150/60 divide-base-150/60
divide-y divide-y
divider
drop-shadow drop-shadow
dropdown dropdown
dropdown-active
dropdown-disabled dropdown-disabled
dropdown-item dropdown-item
dropdown-menu dropdown-menu
@@ -179,7 +205,9 @@ ease-linear
end-3 end-3
error-message error-message
file-upload-complete:progress-success file-upload-complete:progress-success
fill-base-content
fill-black fill-black
fill-gray-200
filter filter
filter-reset filter-reset
fixed fixed
@@ -210,6 +238,8 @@ font-inter
font-medium font-medium
font-normal font-normal
font-semibold font-semibold
footer
footer-center
from-violet-400 from-violet-400
gap-0.5 gap-0.5
gap-1 gap-1
@@ -226,6 +256,7 @@ gap-x-6
gap-y-1 gap-y-1
gap-y-1.5 gap-y-1.5
gap-y-2 gap-y-2
gap-y-2.5
gap-y-3 gap-y-3
gap-y-8 gap-y-8
grid grid
@@ -233,10 +264,12 @@ grid-cols-1
grid-cols-12 grid-cols-12
grid-cols-2 grid-cols-2
grid-cols-3 grid-cols-3
grid-cols-4
grid-flow-col grid-flow-col
grow grow
grow-0 grow-0
h-0 h-0
h-10
h-12 h-12
h-14 h-14
h-2 h-2
@@ -253,11 +286,14 @@ hidden
hover:bg-indigo-500 hover:bg-indigo-500
hover:bg-primary/5 hover:bg-primary/5
hover:bg-transparent hover:bg-transparent
hover:cursor-pointer
hover:text-base-content hover:text-base-content
hover:text-base-content/60 hover:text-base-content/60
hover:text-indigo-500 hover:text-indigo-500
hover:text-primary hover:text-primary
image-full image-full
indicator
indicator-item
inline inline
inline-block inline-block
inline-flex inline-flex
@@ -294,6 +330,7 @@ leading-3.5
leading-6 leading-6
leading-9 leading-9
leading-[1.1] leading-[1.1]
leading-none
left-0 left-0
lg:bg-base-100/20 lg:bg-base-100/20
lg:flex lg:flex
@@ -320,6 +357,7 @@ link-hover
link-primary link-primary
list-disc list-disc
list-inside list-inside
list-none
loading loading
loading-lg loading-lg
loading-sm loading-sm
@@ -332,7 +370,11 @@ mask
max-h-52 max-h-52
max-lg:flex-col max-lg:flex-col
max-lg:hidden max-lg:hidden
max-md:flex-wrap
max-md:justify-center
max-sm:hidden
max-w-7xl max-w-7xl
max-w-8
max-w-80 max-w-80
max-w-full max-w-full
max-w-lg max-w-lg
@@ -376,6 +418,7 @@ min-w-28
min-w-48 min-w-48
min-w-60 min-w-60
min-w-[100px] min-w-[100px]
min-w-full
min-w-sm min-w-sm
mix-blend-exclusion mix-blend-exclusion
ml-3 ml-3
@@ -387,10 +430,13 @@ modal-middle
modal-title modal-title
mr-2 mr-2
mr-4 mr-4
ms-0.5
ms-1 ms-1
ms-1.5
ms-2 ms-2
ms-2.5 ms-2.5
ms-3 ms-3
ms-auto
mt-1 mt-1
mt-1.5 mt-1.5
mt-10 mt-10
@@ -398,6 +444,7 @@ mt-12
mt-2 mt-2
mt-2.5 mt-2.5
mt-3 mt-3
mt-3.5
mt-4 mt-4
mt-5 mt-5
mt-6 mt-6
@@ -407,9 +454,12 @@ mx-auto
my-2.5 my-2.5
my-3 my-3
my-5 my-5
my-8
my-auto my-auto
object-cover
opacity-0 opacity-0
opacity-100 opacity-100
opacity-75
open open
origin-top-left origin-top-left
outline outline
@@ -430,6 +480,7 @@ p-4
p-5 p-5
p-6 p-6
p-8 p-8
pb-1
pe-1.5 pe-1.5
pin-input pin-input
pin-input-underline pin-input-underline
@@ -444,6 +495,7 @@ progress-primary
pt-0 pt-0
pt-0.5 pt-0.5
pt-1.5 pt-1.5
pt-2
pt-3 pt-3
px-1.5 px-1.5
px-2 px-2
@@ -459,10 +511,12 @@ py-12
py-2 py-2
py-2.5 py-2.5
py-6 py-6
radial-progress
radio radio
range range
relative relative
resize resize
ring
ring-0 ring-0
ring-1 ring-1
ring-gray-700 ring-gray-700
@@ -543,10 +597,12 @@ sm:text-sm/5
sm:w-1/2 sm:w-1/2
sm:w-full sm:w-full
space-x-1 space-x-1
space-x-2.5
space-y-1 space-y-1
space-y-4 space-y-4
space-y-6 space-y-6
sr-only sr-only
stack
stat stat
stat-actions stat-actions
stat-desc stat-desc
@@ -602,6 +658,7 @@ text-left
text-lg text-lg
text-primary text-primary
text-primary-content text-primary-content
text-right
text-slate-100 text-slate-100
text-sm text-sm
text-sm/5 text-sm/5
@@ -645,6 +702,7 @@ via-sky-400
w-0 w-0
w-0.5 w-0.5
w-12 w-12
w-13
w-32 w-32
w-4 w-4
w-56 w-56

View File

@@ -1,7 +1,7 @@
<label for="@Id" class="btn btn-square border-0 ring-0 outline-0" style="background-color: @Value"> <label for="@Id" class="btn btn-square border-0 ring-0 outline-0" style="background-color: @Value">
<i class="text-lg text-base-content @Icon"></i> <i class="text-lg text-base-content @Icon"></i>
</label> </label>
<input value="@Value" @oninput="Update" id="@Id" type="color" class="h-0 w-0 opacity-0"/> <input value="@Value" @oninput="UpdateAsync" id="@Id" type="color" class="h-0 w-0 opacity-0"/>
@code @code
{ {
@@ -37,7 +37,7 @@
Id = $"color-selector-{GetHashCode()}"; Id = $"color-selector-{GetHashCode()}";
} }
private async Task Update(ChangeEventArgs args) private async Task UpdateAsync(ChangeEventArgs args)
{ {
Value = args.Value?.ToString() ?? "#FFFFFF"; Value = args.Value?.ToString() ?? "#FFFFFF";
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);

View File

@@ -17,7 +17,7 @@
for all SignalR Hubs to be synced. for all SignalR Hubs to be synced.
</p> </p>
<div class="mt-5"> <div class="mt-5">
<LazyLoader Load="Load"> <LazyLoader Load="LoadAsync">
<WButton OnClick="OnClick" CssClasses="btn btn-primary"> <WButton OnClick="OnClick" CssClasses="btn btn-primary">
Send broadcast Send broadcast
</WButton> </WButton>
@@ -30,9 +30,9 @@
{ {
private HubConnection? Connection; private HubConnection? Connection;
private async Task Load(LazyLoader lazyLoader) private async Task LoadAsync(LazyLoader lazyLoader)
{ {
await lazyLoader.UpdateText("Connecting to SignalR endpoint"); await lazyLoader.UpdateTextAsync("Connecting to SignalR endpoint");
Connection = new HubConnectionBuilder() Connection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/api/admin/system/diagnose/ws")) .WithUrl(Navigation.ToAbsoluteUri("/api/admin/system/diagnose/ws"))
@@ -41,7 +41,7 @@
Connection.On( Connection.On(
"Pong", "Pong",
async () => await ToastService.Success("Received broadcast") async () => await ToastService.SuccessAsync("Received broadcast")
); );
await Connection.StartAsync(); await Connection.StartAsync();

View File

@@ -137,7 +137,7 @@
} }
else else
{ {
<input @onclick="_ => UpdateRadiusBox(possibleValue)" type="radio" name="radius-box" class="radio hidden"/> <input @onclick="_ => UpdateRadiusBoxAsync(possibleValue)" type="radio" name="radius-box" class="radio hidden"/>
} }
<span class="label-text w-full"> <span class="label-text w-full">
<div class="pe-1.5 pt-1.5" aria-hidden="true"> <div class="pe-1.5 pt-1.5" aria-hidden="true">
@@ -164,7 +164,7 @@
} }
else else
{ {
<input @onclick="_ => UpdateRadiusField(possibleValue)" type="radio" name="radius-field" class="radio hidden"/> <input @onclick="_ => UpdateRadiusFieldAsync(possibleValue)" type="radio" name="radius-field" class="radio hidden"/>
} }
<span class="label-text w-full"> <span class="label-text w-full">
<div class="pe-1.5 pt-1.5" aria-hidden="true"> <div class="pe-1.5 pt-1.5" aria-hidden="true">
@@ -191,7 +191,7 @@
} }
else else
{ {
<input @onclick="_ => UpdateRadiusSelector(possibleValue)" type="radio" name="radius-selector" class="radio hidden"/> <input @onclick="_ => UpdateRadiusSelectorAsync(possibleValue)" type="radio" name="radius-selector" class="radio hidden"/>
} }
<span class="label-text w-full"> <span class="label-text w-full">
<div class="pe-1.5 pt-1.5" aria-hidden="true"> <div class="pe-1.5 pt-1.5" aria-hidden="true">
@@ -302,19 +302,19 @@
set => Theme.Noise = value ? 1 : 0; set => Theme.Noise = value ? 1 : 0;
} }
private async Task UpdateRadiusBox(float value) private async Task UpdateRadiusBoxAsync(float value)
{ {
Theme.RadiusBox = value; Theme.RadiusBox = value;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task UpdateRadiusField(float value) private async Task UpdateRadiusFieldAsync(float value)
{ {
Theme.RadiusField = value; Theme.RadiusField = value;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task UpdateRadiusSelector(float value) private async Task UpdateRadiusSelectorAsync(float value)
{ {
Theme.RadiusSelector = value; Theme.RadiusSelector = value;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);

View File

@@ -42,7 +42,7 @@
public event Func<Task> OnStateChanged; public event Func<Task> OnStateChanged;
public bool ShowMobileNavigation { get; private set; } = false; public bool ShowMobileNavigation { get; private set; } = false;
public async Task ToggleMobileNavigation() public async Task ToggleMobileNavigationAsync()
{ {
ShowMobileNavigation = !ShowMobileNavigation; ShowMobileNavigation = !ShowMobileNavigation;
await OnStateChanged(); await OnStateChanged();

View File

@@ -5,7 +5,7 @@
<header class="flex items-center px-4 lg:hidden border-b border-base-content/5"> <header class="flex items-center px-4 lg:hidden border-b border-base-content/5">
<div class="py-2.5"> <div class="py-2.5">
<span class="relative"> <span class="relative">
<button @onclick="Layout.ToggleMobileNavigation" aria-label="Open navigation" <button @onclick="Layout.ToggleMobileNavigationAsync" aria-label="Open navigation"
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 sm:text-sm/5 text-base-content" class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 sm:text-sm/5 text-base-content"
type="button"> type="button">
<i class="icon-menu text-xl"></i> <i class="icon-menu text-xl"></i>

View File

@@ -95,7 +95,7 @@
</div> </div>
</div> </div>
</div> </div>
<a href="#" @onclick:preventDefault @onclick="Logout" class="flex items-center"> <a href="#" @onclick:preventDefault @onclick="LogoutAsync" class="flex items-center">
<i class="icon-log-out text-lg"></i> <i class="icon-log-out text-lg"></i>
</a> </a>
</div> </div>
@@ -121,7 +121,7 @@
<div class="truncate">Moonlight v2.1</div> <div class="truncate">Moonlight v2.1</div>
</div> </div>
<button @onclick="Layout.ToggleMobileNavigation" aria-label="Close navigation" type="button" <button @onclick="Layout.ToggleMobileNavigationAsync" aria-label="Close navigation" type="button"
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 text-base-content"> class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 text-base-content">
<i class="icon-x text-lg"></i> <i class="icon-x text-lg"></i>
</button> </button>
@@ -180,7 +180,7 @@
<div class="flex flex-col gap-0.5"> <div class="flex flex-col gap-0.5">
<div class="relative"> <div class="relative">
<a class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 sm:py-2 sm:text-sm/5 text-base-content" <a class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 sm:py-2 sm:text-sm/5 text-base-content"
href="#" @onclick:preventDefault @onclick="Logout"> href="#" @onclick:preventDefault @onclick="LogoutAsync">
<i class="icon-log-out"></i> <i class="icon-log-out"></i>
<span class="truncate">Logout</span> <span class="truncate">Logout</span>
</a> </a>
@@ -252,13 +252,13 @@
if (!Layout.ShowMobileNavigation) if (!Layout.ShowMobileNavigation)
return; return;
await Layout.ToggleMobileNavigation(); await Layout.ToggleMobileNavigationAsync();
}; };
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task Logout() private Task LogoutAsync()
{ {
Navigation.NavigateTo("/api/auth/logout", true); Navigation.NavigateTo("/api/auth/logout", true);
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -7,7 +7,7 @@
<div class="flex h-screen justify-center items-center"> <div class="flex h-screen justify-center items-center">
<div class="sm:min-w-md"> <div class="sm:min-w-md">
<div class="bg-base-100 shadow-base-300/20 z-1 w-full space-y-6 rounded-xl p-6 shadow-md lg:p-8"> <div class="bg-base-100 shadow-base-300/20 z-1 w-full space-y-6 rounded-xl p-6 shadow-md lg:p-8">
<LazyLoader EnableDefaultSpacing="false" Load="Load"> <LazyLoader EnableDefaultSpacing="false" Load="LoadAsync">
@if (ShowSelection) @if (ShowSelection)
{ {
<div class="flex justify-center items-center gap-3"> <div class="flex justify-center items-center gap-3">
@@ -34,7 +34,7 @@
if (config == null) // Ignore all schemes which have no ui configured if (config == null) // Ignore all schemes which have no ui configured
continue; continue;
<button @onclick="() => Start(scheme)" class="btn btn-text w-full" <button @onclick="() => StartAsync(scheme)" class="btn btn-text w-full"
style="background-color: @(config.Color)"> style="background-color: @(config.Color)">
<img src="@config.IconUrl" <img src="@config.IconUrl"
alt="scheme icon" alt="scheme icon"
@@ -71,7 +71,7 @@
}; };
} }
private async Task Load(LazyLoader arg) private async Task LoadAsync(LazyLoader arg)
{ {
AuthSchemes = await ApiClient.GetJson<AuthSchemeResponse[]>( AuthSchemes = await ApiClient.GetJson<AuthSchemeResponse[]>(
"api/auth" "api/auth"
@@ -82,12 +82,12 @@
// showing the selection screen // showing the selection screen
if (AuthSchemes.Length == 1) if (AuthSchemes.Length == 1)
await Start(AuthSchemes[0]); await StartAsync(AuthSchemes[0]);
else else
ShowSelection = true; ShowSelection = true;
} }
private Task Start(AuthSchemeResponse scheme) private Task StartAsync(AuthSchemeResponse scheme)
{ {
Navigation.NavigateTo($"/api/auth/{scheme.Identifier}", true); Navigation.NavigateTo($"/api/auth/{scheme.Identifier}", true);
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -16,7 +16,7 @@
<i class="icon-chevron-left"></i> <i class="icon-chevron-left"></i>
Back Back
</a> </a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary"> <WButton OnClick="Form.SubmitAsync" CssClasses="btn btn-primary">
<i class="icon-check"></i> <i class="icon-check"></i>
Create Create
</WButton> </WButton>
@@ -66,17 +66,17 @@
var response = await ApiClient.PostJson<CreateApiKeyResponse>("api/admin/apikeys", Request); var response = await ApiClient.PostJson<CreateApiKeyResponse>("api/admin/apikeys", Request);
await DownloadService.Download( await DownloadService.DownloadAsync(
$"moonlight-key-{response.Id}.txt", $"moonlight-key-{response.Id}.txt",
response.Secret response.Secret
); );
await AlertService.Success( await AlertService.SuccessAsync(
"API Key successfully created", "API Key successfully created",
"The API Key has been downloaded. Dont lose it, you cant view it anymore" "The API Key has been downloaded. Dont lose it, you cant view it anymore"
); );
await ToastService.Success("Successfully created api key"); await ToastService.SuccessAsync("Successfully created api key");
Navigation.NavigateTo("/admin/api"); Navigation.NavigateTo("/admin/api");
} }
} }

View File

@@ -3,7 +3,9 @@
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonCore.Models @using MoonCore.Models
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys @using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using MoonCore.Blazor.FlyonUi.DataTables @using MoonCore.Blazor.FlyonUi.Grid
@using MoonCore.Blazor.FlyonUi.Grid.Columns
@using MoonCore.Blazor.FlyonUi.Grid.ToolbarItems
@inject HttpApiClient ApiClient @inject HttpApiClient ApiClient
@inject AlertService AlertService @inject AlertService AlertService
@@ -47,58 +49,82 @@
</div> </div>
</div> </div>
<div class="mb-5 flex justify-end"> <DataGrid @ref="Grid"
<a href="/admin/api/create" class="btn btn-primary"> TGridItem="ApiKeyResponse"
Create ItemsProvider="ItemsProviderAsync"
</a> EnableFiltering="true"
</div> EnablePagination="true">
<PropertyColumn Field="x => x.Id" Sortable="true" />
<PropertyColumn Field="x => x.Description" />
<TemplateColumn Sortable="true" Title="Expires At">
<td>
@Formatter.FormatDate(context.ExpiresAt.UtcDateTime)
</td>
</TemplateColumn>
<TemplateColumn Sortable="true" Title="Created At">
<td>
@Formatter.FormatDate(context.CreatedAt.UtcDateTime)
</td>
</TemplateColumn>
<TemplateColumn>
<td>
<div class="flex justify-end">
<a href="/admin/api/@(context.Id)" class="text-primary mr-2 sm:mr-3">
<i class="icon-pencil text-base"></i>
</a>
<DataTable @ref="Table" TItem="ApiKeyResponse"> <a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault
<Configuration> class="text-error">
<Pagination TItem="ApiKeyResponse" ItemSource="LoadData" /> <i class="icon-trash text-base"></i>
</a>
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.Id)" Name="Id"/> </div>
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.Description)" Name="Description"/> </td>
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.ExpiresAt)" Name="Expires at"> </TemplateColumn>
<ColumnTemplate>
@(Formatter.FormatDate(context.ExpiresAt.UtcDateTime)) <TemplateToolbarItem>
</ColumnTemplate> <a href="/admin/api/create" class="btn btn-primary ms-1.5">
</DataTableColumn> Create
<DataTableColumn TItem="ApiKeyResponse"> </a>
<ColumnTemplate> </TemplateToolbarItem>
<div class="flex justify-end"> </DataGrid>
<a href="/admin/api/@(context.Id)" class="text-primary mr-2 sm:mr-3">
<i class="icon-pencil text-base"></i>
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
class="text-error">
<i class="icon-trash text-base"></i>
</a>
</div>
</ColumnTemplate>
</DataTableColumn>
</Configuration>
</DataTable>
@code @code
{ {
private DataTable<ApiKeyResponse> Table; private DataGrid<ApiKeyResponse> Grid;
private async Task<IPagedData<ApiKeyResponse>> LoadData(PaginationOptions options) private async Task<DataGridItemResult<ApiKeyResponse>> ItemsProviderAsync(DataGridItemRequest request)
=> await ApiClient.GetJson<PagedData<ApiKeyResponse>>($"api/admin/apikeys?page={options.Page}&pageSize={options.PerPage}");
private async Task Delete(ApiKeyResponse apiKeyResponse)
{ {
await AlertService.ConfirmDanger( var query = $"?startIndex={request.StartIndex}&count={request.Count}";
"API Key deletion",
if (!string.IsNullOrEmpty(request.SortColumn))
{
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
}
if (!string.IsNullOrEmpty(request.Filter))
query += $"&filter={request.Filter}";
var data = await ApiClient.GetJson<CountedData<ApiKeyResponse>>($"api/admin/apikeys{query}");
return new()
{
Items = data.Items,
TotalCount = data.TotalCount
};
}
private async Task DeleteAsync(ApiKeyResponse apiKeyResponse)
{
await AlertService.ConfirmDangerAsync(
"API Key Deletion",
$"Do you really want to delete the api key '{apiKeyResponse.Description}'", $"Do you really want to delete the api key '{apiKeyResponse.Description}'",
async () => async () =>
{ {
await ApiClient.Delete($"api/admin/apikeys/{apiKeyResponse.Id}"); await ApiClient.Delete($"api/admin/apikeys/{apiKeyResponse.Id}");
await ToastService.Success("Successfully deleted api key"); await ToastService.SuccessAsync("Successfully deleted api key");
await Table.Refresh(); await Grid.RefreshAsync();
} }
); );
} }

View File

@@ -7,13 +7,13 @@
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject ToastService ToastService @inject ToastService ToastService
<LazyLoader Load="Load"> <LazyLoader Load="LoadAsync">
<PageHeader Title="Update API Key"> <PageHeader Title="Update API Key">
<a href="/admin/api" class="btn btn-secondary"> <a href="/admin/api" class="btn btn-secondary">
<i class="icon-chevron-left"></i> <i class="icon-chevron-left"></i>
Back Back
</a> </a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary"> <WButton OnClick="Form.SubmitAsync" CssClasses="btn btn-primary">
<i class="icon-check"></i> <i class="icon-check"></i>
Update Update
</WButton> </WButton>
@@ -40,7 +40,7 @@
private HandleForm Form; private HandleForm Form;
private UpdateApiKeyRequest Request; private UpdateApiKeyRequest Request;
private async Task Load(LazyLoader _) private async Task LoadAsync(LazyLoader _)
{ {
var detail = await ApiClient.GetJson<ApiKeyResponse>($"api/admin/apikeys/{Id}"); var detail = await ApiClient.GetJson<ApiKeyResponse>($"api/admin/apikeys/{Id}");
@@ -54,7 +54,7 @@
{ {
await ApiClient.Patch($"api/admin/apikeys/{Id}", Request); await ApiClient.Patch($"api/admin/apikeys/{Id}", Request);
await ToastService.Success("Successfully updated api key"); await ToastService.SuccessAsync("Successfully updated api key");
Navigation.NavigateTo("/admin/api"); Navigation.NavigateTo("/admin/api");
} }
} }

View File

@@ -26,17 +26,17 @@
includes your installed theme and plugins. For more information, have a look at <a class="text-primary" href="https://help.moonlightpanel.xyz">our docs</a> includes your installed theme and plugins. For more information, have a look at <a class="text-primary" href="https://help.moonlightpanel.xyz">our docs</a>
</p> </p>
<WButton OnClick="GenerateFrontend" CssClasses="btn btn-primary mt-5">Generate frontend.zip</WButton> <WButton OnClick="GenerateFrontendAsync" CssClasses="btn btn-primary mt-5">Generate frontend.zip</WButton>
</div> </div>
</div> </div>
</div> </div>
@code @code
{ {
private async Task GenerateFrontend(WButton _) private async Task GenerateFrontendAsync(WButton _)
{ {
var stream = await ApiClient.GetStream("api/admin/system/advanced/frontend"); var stream = await ApiClient.GetStream("api/admin/system/advanced/frontend");
await DownloadService.Download("frontend.zip", stream); await DownloadService.DownloadAsync("frontend.zip", stream);
} }
} }

View File

@@ -2,7 +2,9 @@
@using System.Text.Json @using System.Text.Json
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.FlyonUi.DataTables @using MoonCore.Blazor.FlyonUi.Grid
@using MoonCore.Blazor.FlyonUi.Grid.Columns
@using MoonCore.Blazor.FlyonUi.Grid.ToolbarItems
@using MoonCore.Blazor.FlyonUi.Helpers @using MoonCore.Blazor.FlyonUi.Helpers
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonCore.Models @using MoonCore.Models
@@ -16,6 +18,7 @@
@inject ThemeService ThemeService @inject ThemeService ThemeService
@inject AlertService AlertService @inject AlertService AlertService
@inject ToastService ToastService @inject ToastService ToastService
@inject HttpApiClient ApiClient
@inject DownloadService DownloadService @inject DownloadService DownloadService
@inject ILogger<Index> Logger @inject ILogger<Index> Logger
@@ -25,72 +28,72 @@
Themes Themes
</PageSeparator> </PageSeparator>
<div class="mt-5 flex justify-end"> <div class="my-8">
<div> <DataGrid TGridItem="ThemeResponse"
<label for="import-theme" class="btn btn-info me-1"> ItemsProvider="ItemsProviderAsync"
<i class="icon-file-up"></i> EnableFiltering="true"
Import EnablePagination="true">
</label>
<InputFile OnChange="Import" id="import-theme" class="hidden" multiple /> <PropertyColumn Field="x => x.Id" Sortable="true" />
<a href="/admin/system/customisation/themes/create" class="btn btn-primary">Create</a> <TemplateColumn Title="Name" Sortable="true">
</div> <td>
</div> <div class="flex items-center">
@context.Name
<div class="my-2.5"> @if (context.IsEnabled)
<DataTable @ref="Table" TItem="ThemeResponse"> {
<Configuration> <i class="icon-check text-success ms-2"></i>
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Id)" Name="Id"/> }
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Name)" Name="Name"> </div>
<ColumnTemplate> </td>
<div class="flex items-center"> </TemplateColumn>
@context.Name <PropertyColumn Field="x => x.Version" Sortable="true" />
<PropertyColumn Field="x => x.Author" />
@if (context.IsEnabled)
{ <TemplateColumn>
<i class="icon-check text-success ms-2"></i> <td>
} <div class="flex justify-end">
</div> @if (!string.IsNullOrEmpty(context.DonateUrl))
</ColumnTemplate> {
</DataTableColumn> <a href="@context.DonateUrl" target="_blank" class="flex items-center mr-2 sm:mr-3">
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Author)" Name="Author"/> <i class="text-accent icon-heart me-1"></i>
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Version)" Name="Version"/> <span class="text-accent">Donate</span>
<DataTableColumn TItem="ThemeResponse">
<ColumnTemplate>
<div class="flex justify-end">
@if (!string.IsNullOrEmpty(context.DonateUrl))
{
<a href="@context.DonateUrl" target="_blank" class="flex items-center mr-2 sm:mr-3">
<i class="text-accent icon-heart me-1"></i>
<span class="text-accent">Donate</span>
</a>
}
@if (!string.IsNullOrEmpty(context.UpdateUrl))
{
<a href="#" class="flex items-center mr-2 sm:mr-3">
<i class="text-info icon-refresh-cw me-1"></i>
<span class="text-info">Update</span>
</a>
}
<a @onclick="() => Export(context)" @onclick:preventDefault href="#" class="flex items-center mr-2 sm:mr-3">
<i class="text-success icon-download me-1"></i>
<span class="text-success">Export</span>
</a>
<a href="/admin/system/customisation/themes/@(context.Id)" class="mr-2 sm:mr-3">
<i class="icon-pencil text-primary"></i>
</a> </a>
}
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault> @if (!string.IsNullOrEmpty(context.UpdateUrl))
<i class="icon-trash text-error"></i> {
<a href="#" class="flex items-center mr-2 sm:mr-3">
<i class="text-info icon-refresh-cw me-1"></i>
<span class="text-info">Update</span>
</a> </a>
</div> }
</ColumnTemplate>
</DataTableColumn> <a @onclick="() => ExportAsync(context)" @onclick:preventDefault href="#" class="flex items-center mr-2 sm:mr-3">
<Pagination TItem="ThemeResponse" PageSize="10" ItemSource="LoadItems"/> <i class="text-success icon-download me-1"></i>
</Configuration> <span class="text-success">Export</span>
</DataTable> </a>
<a href="/admin/system/customisation/themes/@(context.Id)" class="mr-2 sm:mr-3">
<i class="icon-pencil text-primary"></i>
</a>
<a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault>
<i class="icon-trash text-error"></i>
</a>
</div>
</td>
</TemplateColumn>
<TemplateToolbarItem>
<label for="import-theme" class="btn btn-info me-1 ms-1.5">
<i class="icon-file-up"></i>
Import
</label>
<InputFile OnChange="ImportAsync" id="import-theme" class="hidden" multiple />
<a href="/admin/system/customisation/themes/create" class="btn btn-primary">Create</a>
</TemplateToolbarItem>
</DataGrid>
</div> </div>
<PageSeparator Icon="icon-images"> <PageSeparator Icon="icon-images">
@@ -100,12 +103,31 @@
@code @code
{ {
private DataTable<ThemeResponse> Table; private DataGrid<ThemeResponse> Grid;
private async Task<IPagedData<ThemeResponse>> LoadItems(PaginationOptions options) private async Task<DataGridItemResult<ThemeResponse>> ItemsProviderAsync(DataGridItemRequest request)
=> await ThemeService.Get(options.Page, options.PerPage); {
var query = $"?startIndex={request.StartIndex}&count={request.Count}";
if (!string.IsNullOrEmpty(request.SortColumn))
{
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
}
if (!string.IsNullOrEmpty(request.Filter))
query += $"&filter={request.Filter}";
var data = await ApiClient.GetJson<CountedData<ThemeResponse>>($"api/admin/system/customisation/themes{query}");
return new()
{
Items = data.Items,
TotalCount = data.TotalCount
};
}
private async Task Import(InputFileChangeEventArgs eventArgs) private async Task ImportAsync(InputFileChangeEventArgs eventArgs)
{ {
if(eventArgs.FileCount < 1) if(eventArgs.FileCount < 1)
return; return;
@@ -120,13 +142,13 @@
{ {
if (!file.Name.EndsWith(".json")) if (!file.Name.EndsWith(".json"))
{ {
await ToastService.Error($"Unable to import {file.Name}", "Only .json files are supported"); await ToastService.ErrorAsync($"Unable to import {file.Name}", "Only .json files are supported");
continue; continue;
} }
if (file.Size > maxFileSize) if (file.Size > maxFileSize)
{ {
await ToastService.Error($"Unable to import {file.Name}", "Exceeded the maximum file limit of 1MB"); await ToastService.ErrorAsync($"Unable to import {file.Name}", "Exceeded the maximum file limit of 1MB");
continue; continue;
} }
@@ -137,11 +159,11 @@
if (themeTransfer == null) if (themeTransfer == null)
{ {
await ToastService.Error($"Unable to import {file.Name}", "Failed to deserialize the content"); await ToastService.ErrorAsync($"Unable to import {file.Name}", "Failed to deserialize the content");
continue; continue;
} }
var theme = await ThemeService.Create(new CreateThemeRequest() var theme = await ThemeService.CreateAsync(new CreateThemeRequest()
{ {
Name = themeTransfer.Name, Name = themeTransfer.Name,
Author = themeTransfer.Author, Author = themeTransfer.Author,
@@ -151,9 +173,9 @@
Version = themeTransfer.Version Version = themeTransfer.Version
}); });
await ToastService.Success("Successfully imported theme", theme.Name); await ToastService.SuccessAsync("Successfully imported theme", theme.Name);
await Table.Refresh(); await Grid.RefreshAsync();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -162,7 +184,7 @@
} }
} }
private async Task Export(ThemeResponse theme) private async Task ExportAsync(ThemeResponse theme)
{ {
var transfer = new ThemeTransferModel() var transfer = new ThemeTransferModel()
{ {
@@ -181,20 +203,20 @@
var fileName = $"{transfer.Name.Replace(" ", string.Empty).Trim()}.json"; var fileName = $"{transfer.Name.Replace(" ", string.Empty).Trim()}.json";
await DownloadService.Download(fileName, json); await DownloadService.DownloadAsync(fileName, json);
} }
private async Task Delete(ThemeResponse response) private async Task DeleteAsync(ThemeResponse response)
{ {
await AlertService.ConfirmDanger( await AlertService.ConfirmDangerAsync(
"Theme deletion", "Theme deletion",
$"Do you really want to delete the theme: {response.Name}", $"Do you really want to delete the theme: {response.Name}",
async () => async () =>
{ {
await ThemeService.Delete(response.Id); await ThemeService.DeleteAsync(response.Id);
await ToastService.Success("Successfully deleted theme"); await ToastService.SuccessAsync("Successfully deleted theme");
await Table.Refresh(); await Grid.RefreshAsync();
} }
); );
} }

View File

@@ -14,7 +14,7 @@
<i class="icon-chevron-left"></i> <i class="icon-chevron-left"></i>
Back Back
</a> </a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary"> <WButton OnClick="Form.SubmitAsync" CssClasses="btn btn-primary">
<i class="icon-check"></i> <i class="icon-check"></i>
Create Create
</WButton> </WButton>
@@ -58,8 +58,8 @@
private async Task OnValidSubmit() private async Task OnValidSubmit()
{ {
await ThemeService.Create(Request); await ThemeService.CreateAsync(Request);
await ToastService.Success("Successfully created theme"); await ToastService.SuccessAsync("Successfully created theme");
NavigationManager.NavigateTo("/admin/system/customisation"); NavigationManager.NavigateTo("/admin/system/customisation");
} }

View File

@@ -9,13 +9,13 @@
@inject ToastService ToastService @inject ToastService ToastService
@inject NavigationManager Navigation @inject NavigationManager Navigation
<LazyLoader Load="Load"> <LazyLoader Load="LoadAsync">
<PageHeader Title="@($"Update {Request.Name}")"> <PageHeader Title="@($"Update {Request.Name}")">
<a href="/admin/system/customisation" class="btn btn-secondary"> <a href="/admin/system/customisation" class="btn btn-secondary">
<i class="icon-chevron-left"></i> <i class="icon-chevron-left"></i>
Back Back
</a> </a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary"> <WButton OnClick="Form.SubmitAsync" CssClasses="btn btn-primary">
<i class="icon-check"></i> <i class="icon-check"></i>
Update Update
</WButton> </WButton>
@@ -80,9 +80,9 @@
private HandleForm Form; private HandleForm Form;
private async Task Load(LazyLoader _) private async Task LoadAsync(LazyLoader _)
{ {
Response = await ThemeService.Get(Id); Response = await ThemeService.GetAsync(Id);
Request = new() Request = new()
{ {
@@ -98,9 +98,9 @@
private async Task OnValidSubmit() private async Task OnValidSubmit()
{ {
await ThemeService.Update(Id, Request); await ThemeService.UpdateAsync(Id, Request);
await ToastService.Success("Successfully updated theme"); await ToastService.SuccessAsync("Successfully updated theme");
Navigation.NavigateTo("/admin/system/customisation"); Navigation.NavigateTo("/admin/system/customisation");
} }
} }

View File

@@ -28,11 +28,11 @@
If you only want to export specific parts of the diagnose report, click on "Advanced" and select the desired providers If you only want to export specific parts of the diagnose report, click on "Advanced" and select the desired providers
</p> </p>
<WButton OnClick="GenerateDiagnose" CssClasses="btn btn-primary my-5">Generate report</WButton> <WButton OnClick="GenerateDiagnoseAsync" CssClasses="btn btn-primary my-5">Generate report</WButton>
<div class="text-sm"> <div class="text-sm">
<a class="text-primary cursor-pointer flex items-center" @onclick:preventDefault <a class="text-primary cursor-pointer flex items-center" @onclick:preventDefault
@onclick="ToggleDropDown"> @onclick="ToggleDropDownAsync">
<span class="me-1.5">Advanced</span> <span class="me-1.5">Advanced</span>
@if (DropdownOpen) @if (DropdownOpen)
{ {
@@ -45,7 +45,7 @@
</a> </a>
<div class="@(DropdownOpen ? "" : "hidden")"> <div class="@(DropdownOpen ? "" : "hidden")">
<LazyLoader Load="Load"> <LazyLoader Load="LoadAsync">
<div class="mb-2 py-2 border-b border-base-content/70 flex items-center gap-3"> <div class="mb-2 py-2 border-b border-base-content/70 flex items-center gap-3">
<input id="selectall_checkbox" @bind="SelectAll" type="checkbox" class="checkbox checkbox-primary checkbox-xs"> <input id="selectall_checkbox" @bind="SelectAll" type="checkbox" class="checkbox checkbox-primary checkbox-xs">
<label for="selectall_checkbox">Select all</label> <label for="selectall_checkbox">Select all</label>
@@ -86,7 +86,7 @@
} }
} }
private async Task Load(LazyLoader arg) private async Task LoadAsync(LazyLoader arg)
{ {
var providers = await ApiClient.GetJson<DiagnoseProvideResponse[]>( var providers = await ApiClient.GetJson<DiagnoseProvideResponse[]>(
"api/admin/system/diagnose/providers" "api/admin/system/diagnose/providers"
@@ -96,7 +96,7 @@
.ToDictionary(x => x, _ => true); .ToDictionary(x => x, _ => true);
} }
private async Task GenerateDiagnose(WButton _) private async Task GenerateDiagnoseAsync(WButton button)
{ {
var request = new GenerateDiagnoseRequest(); var request = new GenerateDiagnoseRequest();
@@ -111,11 +111,11 @@
var stream = await ApiClient.PostStream("api/admin/system/diagnose", request); var stream = await ApiClient.PostStream("api/admin/system/diagnose", request);
await DownloadService.Download("diagnose.zip", stream); await DownloadService.DownloadAsync("diagnose.zip", stream);
} }
private async Task ToggleDropDown() private async Task ToggleDropDownAsync()
{ {
DropdownOpen = !DropdownOpen; DropdownOpen = !DropdownOpen;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);

View File

@@ -19,7 +19,7 @@
</HelperMessage> </HelperMessage>
</div> </div>
<LazyLoader Load="Load"> <LazyLoader Load="LoadAsync">
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-5"> <div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-5">
<StatCard Title="Servers" Text="@Stats.Servers.ToString()" Icon="icon-server"/> <StatCard Title="Servers" Text="@Stats.Servers.ToString()" Icon="icon-server"/>
<StatCard Title="Recurring" Text="@Stats.Recurring.ToString()" Icon="icon-calendar-sync"/> <StatCard Title="Recurring" Text="@Stats.Recurring.ToString()" Icon="icon-calendar-sync"/>
@@ -39,7 +39,7 @@
{ {
private HangfireStatsResponse Stats; private HangfireStatsResponse Stats;
private async Task Load(LazyLoader _) private async Task LoadAsync(LazyLoader _)
{ {
Stats = await ApiClient.GetJson<HangfireStatsResponse>( Stats = await ApiClient.GetJson<HangfireStatsResponse>(
"api/admin/system/hangfire/stats" "api/admin/system/hangfire/stats"

View File

@@ -13,7 +13,7 @@
<NavTabs Index="0" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks" /> <NavTabs Index="0" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks" />
</div> </div>
<LazyLoader Load="LoadOverview"> <LazyLoader Load="LoadOverviewAsync">
<div class="gap-5 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4"> <div class="gap-5 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4">
<StatCard Title="CPU Usage" Text="@(OverviewData.CpuUsage + "%")" Icon="icon-cpu"/> <StatCard Title="CPU Usage" Text="@(OverviewData.CpuUsage + "%")" Icon="icon-cpu"/>
<StatCard Title="Memory Usage" Text="@(Formatter.FormatSize(OverviewData.MemoryUsage))" Icon="icon-memory-stick"/> <StatCard Title="Memory Usage" Text="@(Formatter.FormatSize(OverviewData.MemoryUsage))" Icon="icon-memory-stick"/>
@@ -28,7 +28,7 @@
<div class="flex"> <div class="flex">
<div class="card card-body"> <div class="card card-body">
<div class="flex justify-center"> <div class="flex justify-center">
<WButton OnClick="Restart" CssClasses="btn btn-error w-full"> <WButton OnClick="RestartAsync" CssClasses="btn btn-error w-full">
<i class="icon-repeat me-2"></i> <i class="icon-repeat me-2"></i>
Restart/Shutdown Restart/Shutdown
</WButton> </WButton>
@@ -41,12 +41,12 @@
{ {
private SystemOverviewResponse OverviewData; private SystemOverviewResponse OverviewData;
private async Task LoadOverview(LazyLoader arg) private async Task LoadOverviewAsync(LazyLoader arg)
{ {
OverviewData = await ApiClient.GetJson<SystemOverviewResponse>("api/admin/system"); OverviewData = await ApiClient.GetJson<SystemOverviewResponse>("api/admin/system");
} }
private async Task Restart(WButton _) private async Task RestartAsync(WButton _)
{ {
await ApiClient.Post("api/admin/system/shutdown"); await ApiClient.Post("api/admin/system/shutdown");
} }

View File

@@ -12,7 +12,7 @@
<i class="icon-chevron-left"></i> <i class="icon-chevron-left"></i>
Back Back
</a> </a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary"> <WButton OnClick="Form.SubmitAsync" CssClasses="btn btn-primary">
<i class="icon-check"></i> <i class="icon-check"></i>
Create Create
</WButton> </WButton>
@@ -67,7 +67,7 @@
await ApiClient.Post("api/admin/users", Request); await ApiClient.Post("api/admin/users", Request);
await ToastService.Success("Successfully created user"); await ToastService.SuccessAsync("Successfully created user");
Navigation.NavigateTo("/admin/users"); Navigation.NavigateTo("/admin/users");
} }
} }

View File

@@ -3,13 +3,14 @@
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonCore.Models @using MoonCore.Models
@using Moonlight.Shared.Http.Responses.Admin.Users @using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonCore.Blazor.FlyonUi.DataTables @using MoonCore.Blazor.FlyonUi.Grid
@using MoonCore.Blazor.FlyonUi.Grid.Columns
@inject HttpApiClient ApiClient @inject HttpApiClient ApiClient
@inject AlertService AlertService @inject AlertService AlertService
@inject ToastService ToastService @inject ToastService ToastService
<div class="mb-5"> <div class="mb-8">
<PageHeader Title="Users"> <PageHeader Title="Users">
<a href="/admin/users/create" class="btn btn-primary"> <a href="/admin/users/create" class="btn btn-primary">
Create Create
@@ -17,47 +18,67 @@
</PageHeader> </PageHeader>
</div> </div>
<DataTable @ref="Table" TItem="UserResponse"> <DataGrid @ref="Grid"
<Configuration> TGridItem="UserResponse"
<Pagination TItem="UserResponse" ItemSource="LoadData" /> ItemsProvider="ItemsProviderAsync"
EnableFiltering="true"
<DataTableColumn TItem="UserResponse" Field="@(x => x.Id)" Name="Id"/> EnablePagination="true">
<DataTableColumn TItem="UserResponse" Field="@(x => x.Username)" Name="Username"/> <PropertyColumn Field="x => x.Id" Sortable="true" />
<DataTableColumn TItem="UserResponse" Field="@(x => x.Email)" Name="Email"/> <PropertyColumn Field="x => x.Username" Sortable="true" />
<DataTableColumn TItem="UserResponse"> <PropertyColumn Field="x => x.Email" Sortable="true" />
<ColumnTemplate>
<div class="flex justify-end"> <TemplateColumn>
<a href="/admin/users/@(context.Id)" class="mr-2 sm:mr-3"> <td>
<i class="icon-pencil text-primary"></i> <div class="flex justify-end">
</a> <a href="/admin/users/@(context.Id)" class="mr-2 sm:mr-3">
<i class="icon-pencil text-primary"></i>
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault> <a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault>
<i class="icon-trash text-error"></i> <i class="icon-trash text-error"></i>
</a> </a>
</div> </div>
</ColumnTemplate> </td>
</DataTableColumn> </TemplateColumn>
</Configuration> </DataGrid>
</DataTable>
@code @code
{ {
private DataTable<UserResponse> Table; private DataGrid<UserResponse> Grid;
private async Task<IPagedData<UserResponse>> LoadData(PaginationOptions options) private async Task<DataGridItemResult<UserResponse>> ItemsProviderAsync(DataGridItemRequest request)
=> await ApiClient.GetJson<PagedData<UserResponse>>($"api/admin/users?page={options.Page}&pageSize={options.PerPage}");
private async Task Delete(UserResponse response)
{ {
await AlertService.ConfirmDanger( var query = $"?startIndex={request.StartIndex}&count={request.Count}";
if (!string.IsNullOrEmpty(request.SortColumn))
{
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
}
if (!string.IsNullOrEmpty(request.Filter))
query += $"&filter={request.Filter}";
var data = await ApiClient.GetJson<CountedData<UserResponse>>($"api/admin/users{query}");
return new()
{
Items = data.Items,
TotalCount = data.TotalCount
};
}
private async Task DeleteAsync(UserResponse response)
{
await AlertService.ConfirmDangerAsync(
"User deletion", "User deletion",
$"Do you really want to delete the user '{response.Username}'", $"Do you really want to delete the user '{response.Username}'",
async () => async () =>
{ {
await ApiClient.Delete($"api/admin/users/{response.Id}"); await ApiClient.Delete($"api/admin/users/{response.Id}");
await ToastService.Success("Successfully deleted user"); await ToastService.SuccessAsync("Successfully deleted user");
await Table.Refresh(); await Grid.RefreshAsync();
} }
); );
} }

View File

@@ -8,13 +8,13 @@
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject ToastService ToastService @inject ToastService ToastService
<LazyLoader Load="Load"> <LazyLoader Load="LoadAsync">
<PageHeader Title="Update User"> <PageHeader Title="Update User">
<a href="/admin/users" class="btn btn-secondary"> <a href="/admin/users" class="btn btn-secondary">
<i class="icon-chevron-left"></i> <i class="icon-chevron-left"></i>
Back Back
</a> </a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary"> <WButton OnClick="Form.SubmitAsync" CssClasses="btn btn-primary">
<i class="icon-check"></i> <i class="icon-check"></i>
Update Update
</WButton> </WButton>
@@ -62,7 +62,7 @@
private List<string> Permissions = []; private List<string> Permissions = [];
private async Task Load(LazyLoader _) private async Task LoadAsync(LazyLoader _)
{ {
var detail = await ApiClient.GetJson<UserResponse>($"api/admin/users/{Id}"); var detail = await ApiClient.GetJson<UserResponse>($"api/admin/users/{Id}");
@@ -82,7 +82,7 @@
await ApiClient.Patch($"api/admin/users/{Id}", Request); await ApiClient.Patch($"api/admin/users/{Id}", Request);
await ToastService.Success("Successfully updated user"); await ToastService.SuccessAsync("Successfully updated user");
Navigation.NavigateTo("/admin/users"); Navigation.NavigateTo("/admin/users");
} }
} }

View File

@@ -6,4 +6,5 @@ public class ApiKeyResponse
public string Description { get; set; } public string Description { get; set; }
public string[] Permissions { get; set; } = []; public string[] Permissions { get; set; } = [];
public DateTimeOffset ExpiresAt { get; set; } public DateTimeOffset ExpiresAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
} }

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