Compare commits
30 Commits
v2.1_Repla
...
v2_ChangeA
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cc35300f1 | |||
| 2f21806bea | |||
| c5d75a8710 | |||
| f3dd37f649 | |||
| b035dd6b76 | |||
| 34c4bb8cb7 | |||
| de5c9f4ea1 | |||
| 9ab69ffef5 | |||
| d2ef59d171 | |||
| 8e2b333f47 | |||
| 594fb3073f | |||
| 3e87d5c140 | |||
| 86bec7f2ee | |||
| 8e242dc8da | |||
| efca9cf5d8 | |||
| 8573fffaa2 | |||
| 51aeb67ad6 | |||
| 5e371edf2b | |||
| d46ad72cb6 | |||
| a6ae2aacfb | |||
| dc862e4b3c | |||
| e56c5edfb4 | |||
| 70b310adef | |||
| 6748288f3c | |||
| 2c5d45e9c2 | |||
| c02c13bf90 | |||
| 902ca114c1 | |||
| 17cd039c9b | |||
| 26f955fce2 | |||
| 3cc48fb8f7 |
3
.github/workflows/publish-dev-packages.yml
vendored
3
.github/workflows/publish-dev-packages.yml
vendored
@@ -3,6 +3,9 @@ name: Build and Publish NuGet Package
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ v2_ChangeArchitecture,v2.1 ]
|
branches: [ v2_ChangeArchitecture,v2.1 ]
|
||||||
|
paths:
|
||||||
|
- '**.csproj'
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
||||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.8" />
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="Plugins.props" />
|
<Import Project="Plugins.props" />
|
||||||
|
|||||||
@@ -1,28 +1,25 @@
|
|||||||
using Moonlight.ApiServer.Runtime;
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Runtime;
|
||||||
using Moonlight.ApiServer.Startup;
|
using Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
var pluginLoader = new PluginLoader();
|
var pluginLoader = new PluginLoader();
|
||||||
pluginLoader.Initialize();
|
pluginLoader.Initialize();
|
||||||
/*
|
|
||||||
await startup.Run(args, pluginLoader.Instances);
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
var cs = new Startup();
|
|
||||||
|
|
||||||
await cs.Initialize(args, pluginLoader.Instances);
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
await cs.AddMoonlight(builder);
|
builder.AddMoonlight(pluginLoader.Instances);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
await cs.AddMoonlight(app);
|
app.UseMoonlight(pluginLoader.Instances);
|
||||||
|
|
||||||
|
// Add frontend
|
||||||
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
builder.Configuration.Bind(configuration);
|
||||||
|
|
||||||
// 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
|
||||||
if (cs.Configuration.Frontend.EnableHosting)
|
if (configuration.Frontend.EnableHosting)
|
||||||
{
|
{
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
app.UseWebAssemblyDebugging();
|
app.UseWebAssemblyDebugging();
|
||||||
@@ -31,5 +28,7 @@ if (cs.Configuration.Frontend.EnableHosting)
|
|||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.MapMoonlight(pluginLoader.Instances);
|
||||||
|
|
||||||
|
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
@@ -1,24 +1,37 @@
|
|||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Implementations.LocalAuth;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Configuration;
|
namespace Moonlight.ApiServer.Configuration;
|
||||||
|
|
||||||
public record AppConfiguration
|
public record AppConfiguration
|
||||||
{
|
{
|
||||||
[YamlMember(Description = "The public url your instance should be accessible through")]
|
[YamlMember(Description = "Moonlight configuration\n\n\nThe public url your instance should be accessible through")]
|
||||||
public string PublicUrl { get; set; } = "http://localhost:5165";
|
public string PublicUrl { get; set; } = "http://localhost:5165";
|
||||||
|
|
||||||
[YamlMember(Description = "The credentials of the postgres which moonlight should use")]
|
[YamlMember(Description = "\nThe credentials of the postgres which moonlight should use")]
|
||||||
public DatabaseConfig Database { get; set; } = new();
|
public DatabaseConfig Database { get; set; } = new();
|
||||||
|
|
||||||
[YamlMember(Description = "Settings regarding authentication")]
|
[YamlMember(Description = "\nSettings regarding authentication")]
|
||||||
public AuthenticationConfig Authentication { get; set; } = new();
|
public AuthenticationConfig Authentication { get; set; } = new();
|
||||||
|
|
||||||
[YamlMember(Description = "These options are only meant for development purposes")]
|
[YamlMember(Description = "\nThese options are only meant for development purposes")]
|
||||||
public DevelopmentConfig Development { get; set; } = new();
|
public DevelopmentConfig Development { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember(Description = "\nSettings for hosting the frontend")]
|
||||||
public FrontendData Frontend { get; set; } = new();
|
public FrontendData Frontend { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember(Description = "\nSettings for the internal web server moonlight is running in")]
|
||||||
public KestrelConfig Kestrel { get; set; } = new();
|
public KestrelConfig Kestrel { get; set; } = new();
|
||||||
public MetricsData Metrics { get; set; } = new();
|
|
||||||
|
[YamlMember(Description = "\nSettings for the internal file manager for moonlights storage access")]
|
||||||
|
public FilesData Files { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember(Description = "\nSettings for open telemetry")]
|
||||||
|
public OpenTelemetryData OpenTelemetry { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember(Description = "\nConfiguration for the realtime communication solution SignalR")]
|
||||||
|
public SignalRData SignalR { get; set; } = new();
|
||||||
|
|
||||||
public static AppConfiguration CreateEmpty()
|
public static AppConfiguration CreateEmpty()
|
||||||
{
|
{
|
||||||
@@ -29,10 +42,29 @@ public record AppConfiguration
|
|||||||
Kestrel = new()
|
Kestrel = new()
|
||||||
{
|
{
|
||||||
AllowedOrigins = []
|
AllowedOrigins = []
|
||||||
|
},
|
||||||
|
Authentication = new()
|
||||||
|
{
|
||||||
|
EnabledSchemes = []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record SignalRData
|
||||||
|
{
|
||||||
|
[YamlMember(Description =
|
||||||
|
"\nWhether to use redis (or any other redis compatible solution) to scale out SignalR hubs. This is required when using multiple api server replicas")]
|
||||||
|
public bool UseRedis { get; set; } = false;
|
||||||
|
|
||||||
|
public string RedisConnectionString { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FilesData
|
||||||
|
{
|
||||||
|
[YamlMember(Description = "The maximum file size limit a combine operation is allowed to process")]
|
||||||
|
public double CombineLimit { get; set; } = ByteConverter.FromGigaBytes(5).MegaBytes;
|
||||||
|
}
|
||||||
|
|
||||||
public record FrontendData
|
public record FrontendData
|
||||||
{
|
{
|
||||||
[YamlMember(Description = "Enable the hosting of the frontend. Disable this if you only want to run the api server")]
|
[YamlMember(Description = "Enable the hosting of the frontend. Disable this if you only want to run the api server")]
|
||||||
@@ -54,26 +86,21 @@ public record AppConfiguration
|
|||||||
{
|
{
|
||||||
[YamlMember(Description = "The secret token to use for creating jwts and encrypting things. This needs to be at least 32 characters long")]
|
[YamlMember(Description = "The secret token to use for creating jwts and encrypting things. This needs to be at least 32 characters long")]
|
||||||
public string Secret { get; set; } = Formatter.GenerateString(32);
|
public string Secret { get; set; } = Formatter.GenerateString(32);
|
||||||
|
|
||||||
[YamlMember(Description = "The lifespan of generated user tokens in hours")]
|
|
||||||
public int TokenDuration { get; set; } = 24 * 10;
|
|
||||||
|
|
||||||
[YamlMember(Description = "This enables the use of the local oauth2 provider, so moonlight will use itself as an oauth2 provider")]
|
[YamlMember(Description = "Settings for the user sessions")]
|
||||||
public bool EnableLocalOAuth2 { get; set; } = true;
|
public SessionsConfig Sessions { get; set; } = new();
|
||||||
public OAuth2Data OAuth2 { get; set; } = new();
|
|
||||||
|
|
||||||
public record OAuth2Data
|
[YamlMember(Description = "This specifies if the first registered/synced user will become an admin automatically")]
|
||||||
{
|
public bool FirstUserAdmin { get; set; } = true;
|
||||||
public string Secret { get; set; } = Formatter.GenerateString(32);
|
|
||||||
public string ClientId { get; set; } = Formatter.GenerateString(8);
|
|
||||||
public string ClientSecret { get; set; } = Formatter.GenerateString(32);
|
|
||||||
public string? AuthorizationEndpoint { get; set; }
|
|
||||||
public string? AccessEndpoint { get; set; }
|
|
||||||
public string? AuthorizationRedirect { get; set; }
|
|
||||||
|
|
||||||
[YamlMember(Description = "This specifies if the first registered user will become an admin automatically. This only works when using local oauth2")]
|
[YamlMember(Description = "This specifies the authentication schemes the frontend should be able to challenge")]
|
||||||
public bool FirstUserAdmin { get; set; } = true;
|
public string[] EnabledSchemes { get; set; } = [LocalAuthConstants.AuthenticationScheme];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record SessionsConfig
|
||||||
|
{
|
||||||
|
public string CookieName { get; set; } = "session";
|
||||||
|
public int ExpiresIn { get; set; } = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record DevelopmentConfig
|
public record DevelopmentConfig
|
||||||
@@ -91,12 +118,37 @@ public record AppConfiguration
|
|||||||
public string[] AllowedOrigins { get; set; } = ["*"];
|
public string[] AllowedOrigins { get; set; } = ["*"];
|
||||||
}
|
}
|
||||||
|
|
||||||
public record MetricsData
|
public record OpenTelemetryData
|
||||||
{
|
{
|
||||||
[YamlMember(Description = "This enables the collecting of metrics and allows access to the /metrics endpoint")]
|
[YamlMember(Description = "This enables open telemetry for moonlight")]
|
||||||
public bool Enable { get; set; } = false;
|
public bool Enable { get; set; } = false;
|
||||||
|
|
||||||
|
public OpenTelemetryMetricsData Metrics { get; set; } = new();
|
||||||
|
public OpenTelemetryTracesData Traces { get; set; } = new();
|
||||||
|
public OpenTelemetryLogsData Logs { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OpenTelemetryMetricsData
|
||||||
|
{
|
||||||
|
[YamlMember(Description = "This enables the exporting of metrics")]
|
||||||
|
public bool Enable { get; set; } = true;
|
||||||
|
|
||||||
|
[YamlMember(Description = "Enables the /metrics exporter for prometheus")]
|
||||||
|
public bool EnablePrometheus { get; set; } = false;
|
||||||
|
|
||||||
[YamlMember(Description = "The interval in which metrics are created, specified in seconds")]
|
[YamlMember(Description = "The interval in which metrics are created, specified in seconds")]
|
||||||
public int Interval { get; set; } = 15;
|
public int Interval { get; set; } = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record OpenTelemetryTracesData
|
||||||
|
{
|
||||||
|
[YamlMember(Description = "This enables the exporting of traces")]
|
||||||
|
public bool Enable { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OpenTelemetryLogsData
|
||||||
|
{
|
||||||
|
[YamlMember(Description = "This enables the exporting of logs")]
|
||||||
|
public bool Enable { get; set; } = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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: "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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: "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
||||||
|
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Moonlight.ApiServer.Extensions;
|
|||||||
|
|
||||||
public static class ZipArchiveExtensions
|
public static class ZipArchiveExtensions
|
||||||
{
|
{
|
||||||
public static async Task AddBinary(this ZipArchive archive, string name, byte[] bytes)
|
public static async Task AddBinaryAsync(this ZipArchive archive, string name, byte[] bytes)
|
||||||
{
|
{
|
||||||
var entry = archive.CreateEntry(name);
|
var entry = archive.CreateEntry(name);
|
||||||
await using var dataStream = entry.Open();
|
await using var dataStream = entry.Open();
|
||||||
@@ -14,13 +14,13 @@ public static class ZipArchiveExtensions
|
|||||||
await dataStream.FlushAsync();
|
await dataStream.FlushAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task AddText(this ZipArchive archive, string name, string content)
|
public static async Task AddTextAsync(this ZipArchive archive, string name, string content)
|
||||||
{
|
{
|
||||||
var data = Encoding.UTF8.GetBytes(content);
|
var data = Encoding.UTF8.GetBytes(content);
|
||||||
await archive.AddBinary(name, data);
|
await archive.AddBinaryAsync(name, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task AddFile(this ZipArchive archive, string name, string path)
|
public static async Task AddFileAsync(this ZipArchive archive, string name, string path)
|
||||||
{
|
{
|
||||||
var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
|
||||||
|
|||||||
25
Moonlight.ApiServer/Helpers/FilePathHelper.cs
Normal file
25
Moonlight.ApiServer/Helpers/FilePathHelper.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace Moonlight.ApiServer.Helpers;
|
||||||
|
|
||||||
|
public class FilePathHelper
|
||||||
|
{
|
||||||
|
public static string SanitizePath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
// Normalize separators
|
||||||
|
path = path.Replace('\\', '/');
|
||||||
|
|
||||||
|
// Remove ".." and "."
|
||||||
|
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Where(part => part != ".." && part != ".");
|
||||||
|
|
||||||
|
var sanitized = string.Join("/", parts);
|
||||||
|
|
||||||
|
// Ensure it does not start with a slash
|
||||||
|
if (sanitized.StartsWith('/'))
|
||||||
|
sanitized = sanitized.TrimStart('/');
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +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.Common;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
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;
|
||||||
@@ -27,72 +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(
|
public async Task<ActionResult<CountedData<ApiKeyResponse>>> GetAsync(
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] int startIndex,
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
[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(page * pageSize)
|
? query.OrderByDescending(x => x.Id)
|
||||||
.Take(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 = page,
|
Items = items,
|
||||||
Items = mappedApiKey,
|
TotalCount = totalCount
|
||||||
PageSize = pageSize,
|
|
||||||
TotalItems = count,
|
|
||||||
TotalPages = count == 0 ? 0 : (count - 1) / 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
|
||||||
{
|
{
|
||||||
@@ -106,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
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.Common;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Models;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
using Moonlight.ApiServer.Mappers;
|
using Moonlight.ApiServer.Mappers;
|
||||||
using Moonlight.Shared.Http.Requests.Admin.Sys.Theme;
|
using Moonlight.Shared.Http.Requests.Admin.Sys.Theme;
|
||||||
@@ -25,68 +23,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(
|
public async Task<ActionResult<CountedData<ThemeResponse>>> GetAsync(
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] int startIndex,
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
[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(page * pageSize)
|
query = orderBy switch
|
||||||
.Take(pageSize)
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
var mappedItems = items
|
|
||||||
.Select(ThemeMapper.ToResponse)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<ThemeResponse>()
|
|
||||||
{
|
{
|
||||||
CurrentPage = page,
|
nameof(Theme.Id) => orderByDir == "desc"
|
||||||
Items = mappedItems,
|
? query.OrderByDescending(x => x.Id)
|
||||||
PageSize = pageSize,
|
: query.OrderBy(x => x.Id),
|
||||||
TotalItems = count,
|
|
||||||
TotalPages = count == 0 ? 0 : (count - 1) / 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
|
||||||
@@ -100,29 +126,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
using Moonlight.Shared.Http.Requests.Admin.Sys;
|
using Moonlight.Shared.Http.Requests.Admin.Sys;
|
||||||
@@ -21,21 +20,16 @@ public class DiagnoseController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task Diagnose([FromBody] GenerateDiagnoseRequest request)
|
public async Task<ActionResult> DiagnoseAsync([FromBody] GenerateDiagnoseRequest request)
|
||||||
{
|
{
|
||||||
var stream = await DiagnoseService.GenerateDiagnose(request.Providers);
|
var stream = await DiagnoseService.GenerateDiagnoseAsync(request.Providers);
|
||||||
|
|
||||||
await Results.Stream(
|
return File(stream, "application/zip", "diagnose.zip");
|
||||||
stream,
|
|
||||||
contentType: "application/zip",
|
|
||||||
fileDownloadName: "diagnose.zip"
|
|
||||||
)
|
|
||||||
.ExecuteAsync(HttpContext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("providers")]
|
[HttpGet("providers")]
|
||||||
public async Task<DiagnoseProvideResponse[]> GetProviders()
|
public async Task<ActionResult<DiagnoseProvideResponse[]>> GetProvidersAsync()
|
||||||
{
|
{
|
||||||
return await DiagnoseService.GetProviders();
|
return await DiagnoseService.GetProvidersAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Helpers;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/system/files")]
|
||||||
|
[Authorize(Policy = "permissions:admin.system.files")]
|
||||||
|
public class CombineController : Controller
|
||||||
|
{
|
||||||
|
private readonly AppConfiguration Configuration;
|
||||||
|
|
||||||
|
private const string BaseDirectory = "storage";
|
||||||
|
|
||||||
|
public CombineController(AppConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("combine")]
|
||||||
|
public async Task<IResult> CombineAsync([FromBody] CombineRequest request)
|
||||||
|
{
|
||||||
|
// Validate file lenght
|
||||||
|
if (request.Files.Length < 2)
|
||||||
|
return Results.Problem("At least two files are required", statusCode: 400);
|
||||||
|
|
||||||
|
// Resolve the physical paths
|
||||||
|
var destination = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Destination));
|
||||||
|
|
||||||
|
var files = request.Files
|
||||||
|
.Select(path => Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(path)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Validate max file size
|
||||||
|
long combinedSize = 0;
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var fi = new FileInfo(file);
|
||||||
|
combinedSize += fi.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ByteConverter.FromBytes(combinedSize).MegaBytes > Configuration.Files.CombineLimit)
|
||||||
|
{
|
||||||
|
return Results.Problem("The combine operation exceeds the maximum file size", statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine files
|
||||||
|
|
||||||
|
await using var destinationFs = System.IO.File.Open(
|
||||||
|
destination,
|
||||||
|
FileMode.Create,
|
||||||
|
FileAccess.ReadWrite,
|
||||||
|
FileShare.Read
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
await using var fs = System.IO.File.Open(
|
||||||
|
file,
|
||||||
|
FileMode.Open,
|
||||||
|
FileAccess.ReadWrite
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.CopyToAsync(destinationFs);
|
||||||
|
await destinationFs.FlushAsync();
|
||||||
|
|
||||||
|
fs.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await destinationFs.FlushAsync();
|
||||||
|
destinationFs.Close();
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using System.Text;
|
||||||
|
using ICSharpCode.SharpZipLib.GZip;
|
||||||
|
using ICSharpCode.SharpZipLib.Tar;
|
||||||
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Helpers;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/system/files")]
|
||||||
|
[Authorize(Policy = "permissions:admin.system.files")]
|
||||||
|
public class CompressController : Controller
|
||||||
|
{
|
||||||
|
private const string BaseDirectory = "storage";
|
||||||
|
|
||||||
|
[HttpPost("compress")]
|
||||||
|
public async Task<IResult> CompressAsync([FromBody] CompressRequest request)
|
||||||
|
{
|
||||||
|
// Validate item length
|
||||||
|
if (request.Items.Length == 0)
|
||||||
|
{
|
||||||
|
return Results.Problem(
|
||||||
|
"At least one item is required",
|
||||||
|
statusCode: 400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build paths
|
||||||
|
var destinationPath = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Destination));
|
||||||
|
var rootPath = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Root));
|
||||||
|
|
||||||
|
// Resolve the relative to the root item paths to absolute paths
|
||||||
|
var itemsPaths = request.Items.Select(item =>
|
||||||
|
Path.Combine(
|
||||||
|
BaseDirectory,
|
||||||
|
FilePathHelper.SanitizePath(
|
||||||
|
UnixPath.Combine(request.Root, item)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute request
|
||||||
|
switch (request.Format)
|
||||||
|
{
|
||||||
|
case "tar.gz":
|
||||||
|
await CompressTarGzAsync(destinationPath, itemsPaths, rootPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "zip":
|
||||||
|
await CompressZipAsync(destinationPath, itemsPaths, rootPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Results.Problem("Unsupported archive format specified", statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#region Tar Gz
|
||||||
|
|
||||||
|
private async Task CompressTarGzAsync(string destination, IEnumerable<string> items, string root)
|
||||||
|
{
|
||||||
|
await using var outStream = System.IO.File.Create(destination);
|
||||||
|
await using var gzoStream = new GZipOutputStream(outStream);
|
||||||
|
await using var tarStream = new TarOutputStream(gzoStream, Encoding.UTF8);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
await CompressItemToTarGzAsync(tarStream, item, root);
|
||||||
|
|
||||||
|
await tarStream.FlushAsync();
|
||||||
|
await gzoStream.FlushAsync();
|
||||||
|
await outStream.FlushAsync();
|
||||||
|
|
||||||
|
tarStream.Close();
|
||||||
|
gzoStream.Close();
|
||||||
|
outStream.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CompressItemToTarGzAsync(TarOutputStream tarOutputStream, string item, string root)
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(item))
|
||||||
|
{
|
||||||
|
// Open file stream
|
||||||
|
var fs = System.IO.File.Open(item, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
var entry = TarEntry.CreateTarEntry(
|
||||||
|
Formatter
|
||||||
|
.ReplaceStart(item, root, "")
|
||||||
|
.TrimStart('/')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set size
|
||||||
|
entry.Size = fs.Length;
|
||||||
|
|
||||||
|
// Write entry
|
||||||
|
await tarOutputStream.PutNextEntryAsync(entry, CancellationToken.None);
|
||||||
|
|
||||||
|
// Copy file content to tar stream
|
||||||
|
await fs.CopyToAsync(tarOutputStream);
|
||||||
|
fs.Close();
|
||||||
|
|
||||||
|
// Close the entry
|
||||||
|
tarOutputStream.CloseEntry();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(item))
|
||||||
|
{
|
||||||
|
foreach (var fsEntry in Directory.EnumerateFileSystemEntries(item))
|
||||||
|
await CompressItemToTarGzAsync(tarOutputStream, fsEntry, root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ZIP
|
||||||
|
|
||||||
|
private async Task CompressZipAsync(string destination, IEnumerable<string> items, string root)
|
||||||
|
{
|
||||||
|
await using var outStream = System.IO.File.Create(destination);
|
||||||
|
await using var zipOutputStream = new ZipOutputStream(outStream);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
await AddItemToZipAsync(zipOutputStream, item, root);
|
||||||
|
|
||||||
|
await zipOutputStream.FlushAsync();
|
||||||
|
await outStream.FlushAsync();
|
||||||
|
|
||||||
|
zipOutputStream.Close();
|
||||||
|
outStream.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddItemToZipAsync(ZipOutputStream outputStream, string item, string root)
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(item))
|
||||||
|
{
|
||||||
|
// Open file stream
|
||||||
|
var fs = System.IO.File.Open(item, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
var entry = new ZipEntry(
|
||||||
|
Formatter
|
||||||
|
.ReplaceStart(item, root, "")
|
||||||
|
.TrimStart('/')
|
||||||
|
);
|
||||||
|
|
||||||
|
entry.Size = fs.Length;
|
||||||
|
|
||||||
|
// Write entry
|
||||||
|
await outputStream.PutNextEntryAsync(entry, CancellationToken.None);
|
||||||
|
|
||||||
|
// Copy file content to tar stream
|
||||||
|
await fs.CopyToAsync(outputStream);
|
||||||
|
fs.Close();
|
||||||
|
|
||||||
|
// Close the entry
|
||||||
|
outputStream.CloseEntry();
|
||||||
|
|
||||||
|
// Flush caches
|
||||||
|
await outputStream.FlushAsync();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(item))
|
||||||
|
{
|
||||||
|
foreach (var subItem in Directory.EnumerateFileSystemEntries(item))
|
||||||
|
await AddItemToZipAsync(outputStream, subItem, root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Text;
|
||||||
|
using ICSharpCode.SharpZipLib.GZip;
|
||||||
|
using ICSharpCode.SharpZipLib.Tar;
|
||||||
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Moonlight.ApiServer.Helpers;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/system/files")]
|
||||||
|
[Authorize(Policy = "permissions:admin.system.files")]
|
||||||
|
public class DecompressController : Controller
|
||||||
|
{
|
||||||
|
private const string BaseDirectory = "storage";
|
||||||
|
|
||||||
|
[HttpPost("decompress")]
|
||||||
|
public async Task DecompressAsync([FromBody] DecompressRequest request)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Path));
|
||||||
|
var destination = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(request.Destination));
|
||||||
|
|
||||||
|
switch (request.Format)
|
||||||
|
{
|
||||||
|
case "tar.gz":
|
||||||
|
await DecompressTarGzAsync(path, destination);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "zip":
|
||||||
|
await DecompressZipAsync(path, destination);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Tar Gz
|
||||||
|
|
||||||
|
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 gzipInputStream = new GZipInputStream(fs);
|
||||||
|
await using var tarInputStream = new TarInputStream(gzipInputStream, Encoding.UTF8);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var entry = await tarInputStream.GetNextEntryAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
if (entry == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var safeFilePath = FilePathHelper.SanitizePath(entry.Name);
|
||||||
|
var fileDestination = Path.Combine(destination, safeFilePath);
|
||||||
|
var parentFolder = Path.GetDirectoryName(fileDestination);
|
||||||
|
|
||||||
|
// Ensure parent directory exists, if it's not the base directory
|
||||||
|
if (parentFolder != null && parentFolder != BaseDirectory)
|
||||||
|
Directory.CreateDirectory(parentFolder);
|
||||||
|
|
||||||
|
await using var fileDestinationFs =
|
||||||
|
System.IO.File.Open(fileDestination, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
await tarInputStream.CopyToAsync(fileDestinationFs, CancellationToken.None);
|
||||||
|
|
||||||
|
await fileDestinationFs.FlushAsync();
|
||||||
|
fileDestinationFs.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
tarInputStream.Close();
|
||||||
|
gzipInputStream.Close();
|
||||||
|
fs.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Zip
|
||||||
|
|
||||||
|
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 zipInputStream = new ZipInputStream(fs);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var entry = zipInputStream.GetNextEntry();
|
||||||
|
|
||||||
|
if (entry == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (entry.IsDirectory)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var safeFilePath = FilePathHelper.SanitizePath(entry.Name);
|
||||||
|
var fileDestination = Path.Combine(destination, safeFilePath);
|
||||||
|
var parentFolder = Path.GetDirectoryName(fileDestination);
|
||||||
|
|
||||||
|
// Ensure parent directory exists, if it's not the base directory
|
||||||
|
if (parentFolder != null && parentFolder != BaseDirectory)
|
||||||
|
Directory.CreateDirectory(parentFolder);
|
||||||
|
|
||||||
|
await using var fileDestinationFs =
|
||||||
|
System.IO.File.Open(fileDestination, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
await zipInputStream.CopyToAsync(fileDestinationFs, CancellationToken.None);
|
||||||
|
|
||||||
|
await fileDestinationFs.FlushAsync();
|
||||||
|
fileDestinationFs.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
zipInputStream.Close();
|
||||||
|
fs.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Helpers;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/system/files/downloadUrl")]
|
||||||
|
[Authorize(Policy = "permissions:admin.system.files")]
|
||||||
|
public class DownloadUrlController : Controller
|
||||||
|
{
|
||||||
|
private readonly AppConfiguration Configuration;
|
||||||
|
|
||||||
|
private const string BaseDirectory = "storage";
|
||||||
|
|
||||||
|
public DownloadUrlController(AppConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task GetAsync([FromQuery] string path)
|
||||||
|
{
|
||||||
|
var physicalPath = Path.Combine(BaseDirectory, FilePathHelper.SanitizePath(path));
|
||||||
|
var name = Path.GetFileName(physicalPath);
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(physicalPath))
|
||||||
|
{
|
||||||
|
await using var fs = System.IO.File.Open(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
|
||||||
|
await Results.File(fs, fileDownloadName: name).ExecuteAsync(HttpContext);
|
||||||
|
}
|
||||||
|
else if(Directory.Exists(physicalPath))
|
||||||
|
{
|
||||||
|
// Without the base directory we would have the full path to the target folder
|
||||||
|
// inside the zip
|
||||||
|
|
||||||
|
var baseDirectory = Path.Combine(
|
||||||
|
BaseDirectory,
|
||||||
|
FilePathHelper.SanitizePath(Path.GetDirectoryName(path) ?? "")
|
||||||
|
);
|
||||||
|
|
||||||
|
Response.StatusCode = 200;
|
||||||
|
Response.ContentType = "application/zip";
|
||||||
|
Response.Headers["Content-Disposition"] = $"attachment; filename=\"{name}.zip\"";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var zipStream = new ZipOutputStream(Response.Body);
|
||||||
|
zipStream.IsStreamOwner = false;
|
||||||
|
|
||||||
|
await StreamFolderAsZipAsync(zipStream, physicalPath, baseDirectory, HttpContext.RequestAborted);
|
||||||
|
}
|
||||||
|
catch (ZipException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StreamFolderAsZipAsync(
|
||||||
|
ZipOutputStream zipStream,
|
||||||
|
string path, string rootPath,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
foreach (var file in Directory.EnumerateFiles(path))
|
||||||
|
{
|
||||||
|
if (HttpContext.RequestAborted.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var fi = new FileInfo(file);
|
||||||
|
|
||||||
|
var filePath = Formatter.ReplaceStart(file, rootPath, "");
|
||||||
|
|
||||||
|
await zipStream.PutNextEntryAsync(new ZipEntry(filePath)
|
||||||
|
{
|
||||||
|
Size = fi.Length,
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
await using var fs = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
await fs.CopyToAsync(zipStream, cancellationToken);
|
||||||
|
await fs.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
fs.Close();
|
||||||
|
|
||||||
|
await zipStream.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var directory in Directory.EnumerateDirectories(path))
|
||||||
|
{
|
||||||
|
if (HttpContext.RequestAborted.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await StreamFolderAsZipAsync(zipStream, directory, rootPath, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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
|
||||||
|
[HttpPost]
|
||||||
|
public Task<DownloadUrlResponse> PostAsync([FromQuery] string path)
|
||||||
|
{
|
||||||
|
var safePath = FilePathHelper.SanitizePath(path);
|
||||||
|
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(physicalPath) || Directory.Exists(physicalPath))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new DownloadUrlResponse()
|
||||||
|
{
|
||||||
|
Url = $"{Configuration.PublicUrl}/api/admin/system/files/downloadUrl?path={path}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HttpApiException("No such file or directory found", 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
using Moonlight.ApiServer.Helpers;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys.Files;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/system/files")]
|
||||||
|
[Authorize(Policy = "permissions:admin.system.files")]
|
||||||
|
public class FilesController : Controller
|
||||||
|
{
|
||||||
|
private const string BaseDirectory = "storage";
|
||||||
|
|
||||||
|
[HttpPost("touch")]
|
||||||
|
public async Task CreateFileAsync([FromQuery] string path)
|
||||||
|
{
|
||||||
|
var safePath = FilePathHelper.SanitizePath(path);
|
||||||
|
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(physicalPath))
|
||||||
|
throw new HttpApiException("A file already exists at that path", 400);
|
||||||
|
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
throw new HttpApiException("A folder already exists at that path", 400);
|
||||||
|
|
||||||
|
await using var fs = System.IO.File.Create(physicalPath);
|
||||||
|
fs.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("mkdir")]
|
||||||
|
public Task CreateFolderAsync([FromQuery] string path)
|
||||||
|
{
|
||||||
|
var safePath = FilePathHelper.SanitizePath(path);
|
||||||
|
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
||||||
|
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
throw new HttpApiException("A folder already exists at that path", 400);
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(physicalPath))
|
||||||
|
throw new HttpApiException("A file already exists at that path", 400);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(physicalPath);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("list")]
|
||||||
|
public Task<FileSystemEntryResponse[]> ListAsync([FromQuery] string path)
|
||||||
|
{
|
||||||
|
var safePath = FilePathHelper.SanitizePath(path);
|
||||||
|
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
||||||
|
|
||||||
|
var entries = new List<FileSystemEntryResponse>();
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(physicalPath);
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var fi = new FileInfo(file);
|
||||||
|
|
||||||
|
entries.Add(new FileSystemEntryResponse()
|
||||||
|
{
|
||||||
|
Name = fi.Name,
|
||||||
|
Size = fi.Length,
|
||||||
|
CreatedAt = fi.CreationTimeUtc,
|
||||||
|
IsFolder = false,
|
||||||
|
UpdatedAt = fi.LastWriteTimeUtc
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var directories = Directory.GetDirectories(physicalPath);
|
||||||
|
|
||||||
|
foreach (var directory in directories)
|
||||||
|
{
|
||||||
|
var di = new DirectoryInfo(directory);
|
||||||
|
|
||||||
|
entries.Add(new FileSystemEntryResponse()
|
||||||
|
{
|
||||||
|
Name = di.Name,
|
||||||
|
Size = 0,
|
||||||
|
CreatedAt = di.CreationTimeUtc,
|
||||||
|
UpdatedAt = di.LastWriteTimeUtc,
|
||||||
|
IsFolder = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(
|
||||||
|
entries.ToArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("move")]
|
||||||
|
public Task MoveAsync([FromQuery] string oldPath, [FromQuery] string newPath)
|
||||||
|
{
|
||||||
|
var oldSafePath = FilePathHelper.SanitizePath(oldPath);
|
||||||
|
var newSafePath = FilePathHelper.SanitizePath(newPath);
|
||||||
|
|
||||||
|
var oldPhysicalDirPath = Path.Combine(BaseDirectory, oldSafePath);
|
||||||
|
|
||||||
|
if (Directory.Exists(oldPhysicalDirPath))
|
||||||
|
{
|
||||||
|
var newPhysicalDirPath = Path.Combine(BaseDirectory, newSafePath);
|
||||||
|
|
||||||
|
Directory.Move(
|
||||||
|
oldPhysicalDirPath,
|
||||||
|
newPhysicalDirPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var oldPhysicalFilePath = Path.Combine(BaseDirectory, oldSafePath);
|
||||||
|
var newPhysicalFilePath = Path.Combine(BaseDirectory, newSafePath);
|
||||||
|
|
||||||
|
System.IO.File.Move(
|
||||||
|
oldPhysicalFilePath,
|
||||||
|
newPhysicalFilePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("delete")]
|
||||||
|
public Task DeleteAsync([FromQuery] string path)
|
||||||
|
{
|
||||||
|
var safePath = FilePathHelper.SanitizePath(path);
|
||||||
|
var physicalDirPath = Path.Combine(BaseDirectory, safePath);
|
||||||
|
|
||||||
|
if (Directory.Exists(physicalDirPath))
|
||||||
|
Directory.Delete(physicalDirPath, true);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var physicalFilePath = Path.Combine(BaseDirectory, safePath);
|
||||||
|
|
||||||
|
System.IO.File.Delete(physicalFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("upload")]
|
||||||
|
public async Task<IResult> UploadAsync([FromQuery] string path)
|
||||||
|
{
|
||||||
|
if (Request.Form.Files.Count != 1)
|
||||||
|
return Results.Problem("Only one file is allowed in the request", statusCode: 400);
|
||||||
|
|
||||||
|
var file = Request.Form.Files[0];
|
||||||
|
|
||||||
|
var safePath = FilePathHelper.SanitizePath(path);
|
||||||
|
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
||||||
|
|
||||||
|
// Create directory which the new file should be put into
|
||||||
|
var baseDirectory = Path.GetDirectoryName(physicalPath);
|
||||||
|
|
||||||
|
if(!string.IsNullOrEmpty(baseDirectory))
|
||||||
|
Directory.CreateDirectory(baseDirectory);
|
||||||
|
|
||||||
|
// Create file from provided form
|
||||||
|
await using var dataStream = file.OpenReadStream();
|
||||||
|
|
||||||
|
await using var targetStream = System.IO.File.Open(
|
||||||
|
physicalPath,
|
||||||
|
FileMode.Create,
|
||||||
|
FileAccess.ReadWrite,
|
||||||
|
FileShare.Read
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy the content to the newly created file
|
||||||
|
await dataStream.CopyToAsync(targetStream);
|
||||||
|
await targetStream.FlushAsync();
|
||||||
|
|
||||||
|
// Close both streams
|
||||||
|
targetStream.Close();
|
||||||
|
dataStream.Close();
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("download")]
|
||||||
|
public async Task DownloadAsync([FromQuery] string path)
|
||||||
|
{
|
||||||
|
var safePath = FilePathHelper.SanitizePath(path);
|
||||||
|
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
||||||
|
|
||||||
|
await using var fs = System.IO.File.Open(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
await fs.CopyToAsync(Response.Body);
|
||||||
|
|
||||||
|
fs.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,475 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using ICSharpCode.SharpZipLib.GZip;
|
|
||||||
using ICSharpCode.SharpZipLib.Tar;
|
|
||||||
using ICSharpCode.SharpZipLib.Zip;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/system/files")]
|
|
||||||
[Authorize(Policy = "permissions:admin.system.files")]
|
|
||||||
public class FilesController : Controller
|
|
||||||
{
|
|
||||||
private readonly string BaseDirectory = "storage";
|
|
||||||
private readonly long MaxChunkSize = ByteConverter.FromMegaBytes(20).Bytes;
|
|
||||||
|
|
||||||
[HttpPost("touch")]
|
|
||||||
public async Task CreateFile([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = SanitizePath(path);
|
|
||||||
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
if (System.IO.File.Exists(physicalPath))
|
|
||||||
throw new HttpApiException("A file already exists at that path", 400);
|
|
||||||
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
throw new HttpApiException("A folder already exists at that path", 400);
|
|
||||||
|
|
||||||
await using var fs = System.IO.File.Create(physicalPath);
|
|
||||||
fs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("mkdir")]
|
|
||||||
public Task CreateFolder([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = SanitizePath(path);
|
|
||||||
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
throw new HttpApiException("A folder already exists at that path", 400);
|
|
||||||
|
|
||||||
if (System.IO.File.Exists(physicalPath))
|
|
||||||
throw new HttpApiException("A file already exists at that path", 400);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(physicalPath);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("list")]
|
|
||||||
public Task<FileSystemEntryResponse[]> List([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = SanitizePath(path);
|
|
||||||
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
var entries = new List<FileSystemEntryResponse>();
|
|
||||||
|
|
||||||
var files = Directory.GetFiles(physicalPath);
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
var fi = new FileInfo(file);
|
|
||||||
|
|
||||||
entries.Add(new FileSystemEntryResponse()
|
|
||||||
{
|
|
||||||
Name = fi.Name,
|
|
||||||
Size = fi.Length,
|
|
||||||
CreatedAt = fi.CreationTimeUtc,
|
|
||||||
IsFolder = false,
|
|
||||||
UpdatedAt = fi.LastWriteTimeUtc
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var directories = Directory.GetDirectories(physicalPath);
|
|
||||||
|
|
||||||
foreach (var directory in directories)
|
|
||||||
{
|
|
||||||
var di = new DirectoryInfo(directory);
|
|
||||||
|
|
||||||
entries.Add(new FileSystemEntryResponse()
|
|
||||||
{
|
|
||||||
Name = di.Name,
|
|
||||||
Size = 0,
|
|
||||||
CreatedAt = di.CreationTimeUtc,
|
|
||||||
UpdatedAt = di.LastWriteTimeUtc,
|
|
||||||
IsFolder = true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(
|
|
||||||
entries.ToArray()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("upload")]
|
|
||||||
public async Task Upload([FromQuery] string path, [FromQuery] long chunkSize, [FromQuery] long totalSize, [FromQuery] int chunkId)
|
|
||||||
{
|
|
||||||
if (Request.Form.Files.Count != 1)
|
|
||||||
throw new HttpApiException("You need to provide exactly one file", 400);
|
|
||||||
|
|
||||||
var file = Request.Form.Files[0];
|
|
||||||
|
|
||||||
if (file.Length > chunkSize)
|
|
||||||
throw new HttpApiException("The provided data exceeds the chunk size limit", 400);
|
|
||||||
|
|
||||||
var chunks = totalSize / chunkSize;
|
|
||||||
chunks += totalSize % chunkSize > 0 ? 1 : 0;
|
|
||||||
|
|
||||||
if (chunkId > chunks)
|
|
||||||
throw new HttpApiException("Invalid chunk id: Out of bounds", 400);
|
|
||||||
|
|
||||||
var positionToSkipTo = chunkSize * chunkId;
|
|
||||||
|
|
||||||
var safePath = SanitizePath(path);
|
|
||||||
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
var baseDir = Path.GetDirectoryName(physicalPath);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(baseDir))
|
|
||||||
Directory.CreateDirectory(baseDir);
|
|
||||||
|
|
||||||
await using var fs = System.IO.File.Open(physicalPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
|
||||||
|
|
||||||
// This resizes the file to the correct size so we can handle the chunk if it didnt exist
|
|
||||||
|
|
||||||
if (fs.Length != totalSize)
|
|
||||||
fs.SetLength(totalSize);
|
|
||||||
|
|
||||||
fs.Position = positionToSkipTo;
|
|
||||||
|
|
||||||
var dataStream = file.OpenReadStream();
|
|
||||||
|
|
||||||
await dataStream.CopyToAsync(fs);
|
|
||||||
await fs.FlushAsync();
|
|
||||||
|
|
||||||
fs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("move")]
|
|
||||||
public Task Move([FromQuery] string oldPath, [FromQuery] string newPath)
|
|
||||||
{
|
|
||||||
var oldSafePath = SanitizePath(oldPath);
|
|
||||||
var newSafePath = SanitizePath(newPath);
|
|
||||||
|
|
||||||
var oldPhysicalDirPath = Path.Combine(BaseDirectory, oldSafePath);
|
|
||||||
|
|
||||||
if (Directory.Exists(oldPhysicalDirPath))
|
|
||||||
{
|
|
||||||
var newPhysicalDirPath = Path.Combine(BaseDirectory, newSafePath);
|
|
||||||
|
|
||||||
Directory.Move(
|
|
||||||
oldPhysicalDirPath,
|
|
||||||
newPhysicalDirPath
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var oldPhysicalFilePath = Path.Combine(BaseDirectory, oldSafePath);
|
|
||||||
var newPhysicalFilePath = Path.Combine(BaseDirectory, newSafePath);
|
|
||||||
|
|
||||||
System.IO.File.Move(
|
|
||||||
oldPhysicalFilePath,
|
|
||||||
newPhysicalFilePath
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("delete")]
|
|
||||||
public Task Delete([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = SanitizePath(path);
|
|
||||||
var physicalDirPath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
if (Directory.Exists(physicalDirPath))
|
|
||||||
Directory.Delete(physicalDirPath, true);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var physicalFilePath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
System.IO.File.Delete(physicalFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("download")]
|
|
||||||
public async Task Download([FromQuery] string path)
|
|
||||||
{
|
|
||||||
var safePath = SanitizePath(path);
|
|
||||||
var physicalPath = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
await using var fs = System.IO.File.Open(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
await fs.CopyToAsync(Response.Body);
|
|
||||||
|
|
||||||
fs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("compress")]
|
|
||||||
public async Task Compress([FromBody] CompressRequest request)
|
|
||||||
{
|
|
||||||
if (request.Type == "tar.gz")
|
|
||||||
await CompressTarGz(request.Path, request.ItemsToCompress);
|
|
||||||
else if (request.Type == "zip")
|
|
||||||
await CompressZip(request.Path, request.ItemsToCompress);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Tar Gz
|
|
||||||
|
|
||||||
private async Task CompressTarGz(string path, string[] itemsToCompress)
|
|
||||||
{
|
|
||||||
var safePath = SanitizePath(path);
|
|
||||||
var destination = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
await using var outStream = System.IO.File.Create(destination);
|
|
||||||
await using var gzoStream = new GZipOutputStream(outStream);
|
|
||||||
await using var tarStream = new TarOutputStream(gzoStream, Encoding.UTF8);
|
|
||||||
|
|
||||||
foreach (var itemName in itemsToCompress)
|
|
||||||
{
|
|
||||||
var safeFilePath = SanitizePath(itemName);
|
|
||||||
var filePath = Path.Combine(BaseDirectory, safeFilePath);
|
|
||||||
|
|
||||||
var fi = new FileInfo(filePath);
|
|
||||||
|
|
||||||
if (fi.Exists)
|
|
||||||
await AddFileToTarGz(tarStream, filePath);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var safeDirePath = SanitizePath(itemName);
|
|
||||||
var dirPath = Path.Combine(BaseDirectory, safeDirePath);
|
|
||||||
|
|
||||||
await AddDirectoryToTarGz(tarStream, dirPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await tarStream.FlushAsync();
|
|
||||||
await gzoStream.FlushAsync();
|
|
||||||
await outStream.FlushAsync();
|
|
||||||
|
|
||||||
tarStream.Close();
|
|
||||||
gzoStream.Close();
|
|
||||||
outStream.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddDirectoryToTarGz(TarOutputStream tarOutputStream, string root)
|
|
||||||
{
|
|
||||||
foreach (var file in Directory.GetFiles(root))
|
|
||||||
await AddFileToTarGz(tarOutputStream, file);
|
|
||||||
|
|
||||||
foreach (var directory in Directory.GetDirectories(root))
|
|
||||||
await AddDirectoryToTarGz(tarOutputStream, directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddFileToTarGz(TarOutputStream tarOutputStream, string file)
|
|
||||||
{
|
|
||||||
// Open file stream
|
|
||||||
var fs = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
|
|
||||||
// Meta
|
|
||||||
var entry = TarEntry.CreateTarEntry(file);
|
|
||||||
|
|
||||||
// Fix path
|
|
||||||
entry.Name = Formatter
|
|
||||||
.ReplaceStart(entry.Name, BaseDirectory, "")
|
|
||||||
.TrimStart('/');
|
|
||||||
|
|
||||||
entry.Size = fs.Length;
|
|
||||||
|
|
||||||
// Write entry
|
|
||||||
await tarOutputStream.PutNextEntryAsync(entry, CancellationToken.None);
|
|
||||||
|
|
||||||
// Copy file content to tar stream
|
|
||||||
await fs.CopyToAsync(tarOutputStream);
|
|
||||||
fs.Close();
|
|
||||||
|
|
||||||
// Close the entry
|
|
||||||
tarOutputStream.CloseEntry();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ZIP
|
|
||||||
|
|
||||||
private async Task CompressZip(string path, string[] itemsToCompress)
|
|
||||||
{
|
|
||||||
var safePath = SanitizePath(path);
|
|
||||||
var destination = Path.Combine(BaseDirectory, safePath);
|
|
||||||
|
|
||||||
await using var outStream = System.IO.File.Create(destination);
|
|
||||||
await using var zipOutputStream = new ZipOutputStream(outStream);
|
|
||||||
|
|
||||||
foreach (var itemName in itemsToCompress)
|
|
||||||
{
|
|
||||||
var safeFilePath = SanitizePath(itemName);
|
|
||||||
var filePath = Path.Combine(BaseDirectory, safeFilePath);
|
|
||||||
|
|
||||||
var fi = new FileInfo(filePath);
|
|
||||||
|
|
||||||
if (fi.Exists)
|
|
||||||
await AddFileToZip(zipOutputStream, filePath);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var safeDirePath = SanitizePath(itemName);
|
|
||||||
var dirPath = Path.Combine(BaseDirectory, safeDirePath);
|
|
||||||
|
|
||||||
await AddDirectoryToZip(zipOutputStream, dirPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await zipOutputStream.FlushAsync();
|
|
||||||
await outStream.FlushAsync();
|
|
||||||
|
|
||||||
zipOutputStream.Close();
|
|
||||||
outStream.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddFileToZip(ZipOutputStream zipOutputStream, string path)
|
|
||||||
{
|
|
||||||
// Open file stream
|
|
||||||
var fs = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
|
|
||||||
// Fix path
|
|
||||||
var name = Formatter
|
|
||||||
.ReplaceStart(path, BaseDirectory, "")
|
|
||||||
.TrimStart('/');
|
|
||||||
|
|
||||||
// Meta
|
|
||||||
var entry = new ZipEntry(name);
|
|
||||||
|
|
||||||
entry.Size = fs.Length;
|
|
||||||
|
|
||||||
// Write entry
|
|
||||||
await zipOutputStream.PutNextEntryAsync(entry, CancellationToken.None);
|
|
||||||
|
|
||||||
// Copy file content to tar stream
|
|
||||||
await fs.CopyToAsync(zipOutputStream);
|
|
||||||
fs.Close();
|
|
||||||
|
|
||||||
// Close the entry
|
|
||||||
zipOutputStream.CloseEntry();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddDirectoryToZip(ZipOutputStream zipOutputStream, string root)
|
|
||||||
{
|
|
||||||
foreach (var file in Directory.GetFiles(root))
|
|
||||||
await AddFileToZip(zipOutputStream, file);
|
|
||||||
|
|
||||||
foreach (var directory in Directory.GetDirectories(root))
|
|
||||||
await AddDirectoryToZip(zipOutputStream, directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
[HttpPost("decompress")]
|
|
||||||
public async Task Decompress([FromBody] DecompressRequest request)
|
|
||||||
{
|
|
||||||
if (request.Type == "tar.gz")
|
|
||||||
await DecompressTarGz(request.Path, request.Destination);
|
|
||||||
else if (request.Type == "zip")
|
|
||||||
await DecompressZip(request.Path, request.Destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Tar Gz
|
|
||||||
|
|
||||||
private async Task DecompressTarGz(string path, string destination)
|
|
||||||
{
|
|
||||||
var safeDestination = SanitizePath(destination);
|
|
||||||
|
|
||||||
var safeArchivePath = SanitizePath(path);
|
|
||||||
var archivePath = Path.Combine(BaseDirectory, safeArchivePath);
|
|
||||||
|
|
||||||
await using var fs = System.IO.File.Open(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
await using var gzipInputStream = new GZipInputStream(fs);
|
|
||||||
await using var tarInputStream = new TarInputStream(gzipInputStream, Encoding.UTF8);
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var entry = await tarInputStream.GetNextEntryAsync(CancellationToken.None);
|
|
||||||
|
|
||||||
if (entry == null)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var safeFilePath = SanitizePath(entry.Name);
|
|
||||||
var fileDestination = Path.Combine(BaseDirectory, safeDestination, safeFilePath);
|
|
||||||
var parentFolder = Path.GetDirectoryName(fileDestination);
|
|
||||||
|
|
||||||
// Ensure parent directory exists, if it's not the base directory
|
|
||||||
if (parentFolder != null && parentFolder != BaseDirectory)
|
|
||||||
Directory.CreateDirectory(parentFolder);
|
|
||||||
|
|
||||||
await using var fileDestinationFs =
|
|
||||||
System.IO.File.Open(fileDestination, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
|
||||||
await tarInputStream.CopyToAsync(fileDestinationFs, CancellationToken.None);
|
|
||||||
|
|
||||||
await fileDestinationFs.FlushAsync();
|
|
||||||
fileDestinationFs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
tarInputStream.Close();
|
|
||||||
gzipInputStream.Close();
|
|
||||||
fs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Zip
|
|
||||||
|
|
||||||
private async Task DecompressZip(string path, string destination)
|
|
||||||
{
|
|
||||||
var safeDestination = SanitizePath(destination);
|
|
||||||
|
|
||||||
var safeArchivePath = SanitizePath(path);
|
|
||||||
var archivePath = Path.Combine(BaseDirectory, safeArchivePath);
|
|
||||||
|
|
||||||
await using var fs = System.IO.File.Open(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
await using var zipInputStream = new ZipInputStream(fs);
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var entry = zipInputStream.GetNextEntry();
|
|
||||||
|
|
||||||
if (entry == null)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (entry.IsDirectory)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var safeFilePath = SanitizePath(entry.Name);
|
|
||||||
var fileDestination = Path.Combine(BaseDirectory, safeDestination, safeFilePath);
|
|
||||||
var parentFolder = Path.GetDirectoryName(fileDestination);
|
|
||||||
|
|
||||||
// Ensure parent directory exists, if it's not the base directory
|
|
||||||
if (parentFolder != null && parentFolder != BaseDirectory)
|
|
||||||
Directory.CreateDirectory(parentFolder);
|
|
||||||
|
|
||||||
await using var fileDestinationFs =
|
|
||||||
System.IO.File.Open(fileDestination, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
|
||||||
await zipInputStream.CopyToAsync(fileDestinationFs, CancellationToken.None);
|
|
||||||
|
|
||||||
await fileDestinationFs.FlushAsync();
|
|
||||||
fileDestinationFs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
zipInputStream.Close();
|
|
||||||
fs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private string SanitizePath(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
// Normalize separators
|
|
||||||
path = path.Replace('\\', '/');
|
|
||||||
|
|
||||||
// Remove ".." and "."
|
|
||||||
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Where(part => part != ".." && part != ".");
|
|
||||||
|
|
||||||
var sanitized = string.Join("/", parts);
|
|
||||||
|
|
||||||
// Ensure it does not start with a slash
|
|
||||||
if (sanitized.StartsWith('/'))
|
|
||||||
sanitized = sanitized.TrimStart('/');
|
|
||||||
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
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 Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Common;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extended.Helpers;
|
using MoonCore.Extended.Helpers;
|
||||||
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;
|
||||||
|
|
||||||
@@ -27,63 +26,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(
|
public async Task<ActionResult<CountedData<UserResponse>>> GetAsync(
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] int startIndex,
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
[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(page * pageSize)
|
? query.OrderByDescending(x => x.Id)
|
||||||
.Take(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 = page,
|
Items = items,
|
||||||
Items = mappedUsers,
|
TotalCount = totalCount
|
||||||
PageSize = pageSize,
|
|
||||||
TotalItems = count,
|
|
||||||
TotalPages = count == 0 ? 0 : (count - 1) / 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();
|
||||||
@@ -91,10 +105,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);
|
||||||
|
|
||||||
@@ -106,27 +120,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();
|
||||||
@@ -134,10 +142,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))
|
||||||
@@ -155,38 +163,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.Security.Claims;
|
||||||
using System.Security.Claims;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.ApiServer.Interfaces;
|
using Moonlight.ApiServer.Interfaces;
|
||||||
using Moonlight.Shared.Http.Requests.Auth;
|
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
using Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
||||||
@@ -19,93 +13,116 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
|||||||
[Route("api/auth")]
|
[Route("api/auth")]
|
||||||
public class AuthController : Controller
|
public class AuthController : Controller
|
||||||
{
|
{
|
||||||
|
private readonly IAuthenticationSchemeProvider SchemeProvider;
|
||||||
|
private readonly IEnumerable<IAuthCheckExtension> Extensions;
|
||||||
private readonly AppConfiguration Configuration;
|
private readonly AppConfiguration Configuration;
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
|
||||||
private readonly IOAuth2Provider OAuth2Provider;
|
|
||||||
|
|
||||||
public AuthController(
|
public AuthController(
|
||||||
AppConfiguration configuration,
|
IAuthenticationSchemeProvider schemeProvider,
|
||||||
DatabaseRepository<User> userRepository,
|
IEnumerable<IAuthCheckExtension> extensions,
|
||||||
IOAuth2Provider oAuth2Provider
|
AppConfiguration configuration
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
UserRepository = userRepository;
|
SchemeProvider = schemeProvider;
|
||||||
OAuth2Provider = oAuth2Provider;
|
Extensions = extensions;
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[HttpGet]
|
||||||
[HttpGet("start")]
|
public async Task<AuthSchemeResponse[]> GetSchemesAsync()
|
||||||
public async Task<LoginStartResponse> Start()
|
|
||||||
{
|
{
|
||||||
var url = await OAuth2Provider.Start();
|
var schemes = await SchemeProvider.GetAllSchemesAsync();
|
||||||
|
|
||||||
return new LoginStartResponse()
|
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
|
||||||
{
|
|
||||||
Url = url
|
return schemes
|
||||||
};
|
.Where(x => allowedSchemes.Contains(x.Name))
|
||||||
|
.Select(scheme => new AuthSchemeResponse()
|
||||||
|
{
|
||||||
|
DisplayName = scheme.DisplayName ?? scheme.Name,
|
||||||
|
Identifier = scheme.Name
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[HttpGet("{identifier:alpha}")]
|
||||||
[HttpPost("complete")]
|
public async Task StartSchemeAsync([FromRoute] string identifier)
|
||||||
public async Task<LoginCompleteResponse> Complete([FromBody] LoginCompleteRequest request)
|
|
||||||
{
|
{
|
||||||
var user = await OAuth2Provider.Complete(request.Code);
|
// Validate identifier against our enable list
|
||||||
|
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
|
||||||
|
|
||||||
if (user == null)
|
if (!allowedSchemes.Contains(identifier))
|
||||||
throw new HttpApiException("Unable to load user data", 500);
|
|
||||||
|
|
||||||
// Generate token
|
|
||||||
var securityTokenDescriptor = new SecurityTokenDescriptor()
|
|
||||||
{
|
{
|
||||||
Expires = DateTime.Now.AddHours(Configuration.Authentication.TokenDuration),
|
await Results
|
||||||
IssuedAt = DateTime.Now,
|
.Problem(
|
||||||
NotBefore = DateTime.Now.AddMinutes(-1),
|
"Invalid scheme identifier provided",
|
||||||
Claims = new Dictionary<string, object>()
|
statusCode: 404
|
||||||
|
)
|
||||||
|
.ExecuteAsync(HttpContext);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can check if it even exists
|
||||||
|
var scheme = await SchemeProvider.GetSchemeAsync(identifier);
|
||||||
|
|
||||||
|
if (scheme == null)
|
||||||
|
{
|
||||||
|
await Results
|
||||||
|
.Problem(
|
||||||
|
"Invalid scheme identifier provided",
|
||||||
|
statusCode: 404
|
||||||
|
)
|
||||||
|
.ExecuteAsync(HttpContext);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything fine, challenge the frontend
|
||||||
|
await HttpContext.ChallengeAsync(
|
||||||
|
scheme.Name,
|
||||||
|
new AuthenticationProperties()
|
||||||
{
|
{
|
||||||
{
|
RedirectUri = "/"
|
||||||
"userId",
|
}
|
||||||
user.Id
|
);
|
||||||
},
|
|
||||||
{
|
|
||||||
"permissions",
|
|
||||||
string.Join(";", user.Permissions)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SigningCredentials = new SigningCredentials(
|
|
||||||
new SymmetricSecurityKey(
|
|
||||||
Encoding.UTF8.GetBytes(Configuration.Authentication.Secret)
|
|
||||||
),
|
|
||||||
SecurityAlgorithms.HmacSha256
|
|
||||||
),
|
|
||||||
Issuer = Configuration.PublicUrl,
|
|
||||||
Audience = Configuration.PublicUrl
|
|
||||||
};
|
|
||||||
|
|
||||||
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
|
||||||
var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
|
|
||||||
|
|
||||||
var jwt = jwtSecurityTokenHandler.WriteToken(securityToken);
|
|
||||||
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
AccessToken = jwt
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpGet("check")]
|
[HttpGet("check")]
|
||||||
public async Task<CheckResponse> Check()
|
public async Task<AuthClaimResponse[]> CheckAsync()
|
||||||
{
|
{
|
||||||
var userIdStr = User.FindFirstValue("userId")!;
|
var username = User.FindFirstValue(ClaimTypes.Name)!;
|
||||||
var userId = int.Parse(userIdStr);
|
var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
|
||||||
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
|
var email = User.FindFirstValue(ClaimTypes.Email)!;
|
||||||
|
var userId = User.FindFirstValue("UserId")!;
|
||||||
|
var permissions = User.FindFirstValue("Permissions")!;
|
||||||
|
|
||||||
return new()
|
// Create basic set of claims used by the frontend
|
||||||
|
var claims = new List<AuthClaimResponse>()
|
||||||
{
|
{
|
||||||
Email = user.Email,
|
new(ClaimTypes.Name, username),
|
||||||
Username = user.Username,
|
new(ClaimTypes.NameIdentifier, id),
|
||||||
Permissions = user.Permissions
|
new(ClaimTypes.Email, email),
|
||||||
|
new("UserId", userId),
|
||||||
|
new("Permissions", permissions)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Enrich the frontend claims by extensions (used by plugins)
|
||||||
|
foreach (var extension in Extensions)
|
||||||
|
{
|
||||||
|
claims.AddRange(
|
||||||
|
await extension.GetFrontendClaimsAsync(User)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("logout")]
|
||||||
|
public async Task LogoutAsync()
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync();
|
||||||
|
await Results.Redirect("/").ExecuteAsync(HttpContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
@using Moonlight.ApiServer.Database.Entities
|
@using Moonlight.ApiServer.Database.Entities
|
||||||
@using Moonlight.Shared.Misc
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="bg-background text-base-content font-inter">
|
<html lang="en" class="bg-base-200 text-base-content font-inter">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
@@ -23,48 +22,45 @@
|
|||||||
{
|
{
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--mooncore-color-background: @(Theme.Content.ColorBackground);
|
--color-base-100: @(Theme.Content.ColorBase100);
|
||||||
--mooncore-color-base-100: @(Theme.Content.ColorBase100);
|
--color-base-200: @(Theme.Content.ColorBase200);
|
||||||
--mooncore-color-base-150: @(Theme.Content.ColorBase150);
|
--color-base-300: @(Theme.Content.ColorBase300);
|
||||||
--mooncore-color-base-200: @(Theme.Content.ColorBase200);
|
--color-base-content: @(Theme.Content.ColorBaseContent);
|
||||||
--mooncore-color-base-250: @(Theme.Content.ColorBase250);
|
|
||||||
--mooncore-color-base-300: @(Theme.Content.ColorBase300);
|
|
||||||
--mooncore-color-base-content: @(Theme.Content.ColorBaseContent);
|
|
||||||
|
|
||||||
--mooncore-color-primary: @(Theme.Content.ColorPrimary);
|
--color-primary: @(Theme.Content.ColorPrimary);
|
||||||
--mooncore-color-primary-content: @(Theme.Content.ColorPrimaryContent);
|
--color-primary-content: @(Theme.Content.ColorPrimaryContent);
|
||||||
|
|
||||||
--mooncore-color-secondary: @(Theme.Content.ColorSecondary);
|
--color-secondary: @(Theme.Content.ColorSecondary);
|
||||||
--mooncore-color-secondary-content: @(Theme.Content.ColorSecondaryContent);
|
--color-secondary-content: @(Theme.Content.ColorSecondaryContent);
|
||||||
|
|
||||||
--mooncore-color-accent: @(Theme.Content.ColorAccent);
|
--color-accent: @(Theme.Content.ColorAccent);
|
||||||
--mooncore-color-accent-content: @(Theme.Content.ColorAccentContent);
|
--color-accent-content: @(Theme.Content.ColorAccentContent);
|
||||||
|
|
||||||
--mooncore-color-neutral: @(Theme.Content.ColorNeutral);
|
--color-neutral: @(Theme.Content.ColorNeutral);
|
||||||
--mooncore-color-neutral-content: @(Theme.Content.ColorNeutralContent);
|
--color-neutral-content: @(Theme.Content.ColorNeutralContent);
|
||||||
|
|
||||||
--mooncore-color-info: @(Theme.Content.ColorInfo);
|
--color-info: @(Theme.Content.ColorInfo);
|
||||||
--mooncore-color-info-content: @(Theme.Content.ColorInfoContent);
|
--color-info-content: @(Theme.Content.ColorInfoContent);
|
||||||
|
|
||||||
--mooncore-color-success: @(Theme.Content.ColorSuccess);
|
--color-success: @(Theme.Content.ColorSuccess);
|
||||||
--mooncore-color-success-content: @(Theme.Content.ColorSuccessContent);
|
--color-success-content: @(Theme.Content.ColorSuccessContent);
|
||||||
|
|
||||||
--mooncore-color-warning: @(Theme.Content.ColorWarning);
|
--color-warning: @(Theme.Content.ColorWarning);
|
||||||
--mooncore-color-warning-content: @(Theme.Content.ColorWarningContent);
|
--color-warning-content: @(Theme.Content.ColorWarningContent);
|
||||||
|
|
||||||
--mooncore-color-error: @(Theme.Content.ColorError);
|
--color-error: @(Theme.Content.ColorError);
|
||||||
--mooncore-color-error-content: @(Theme.Content.ColorErrorContent);
|
--color-error-content: @(Theme.Content.ColorErrorContent);
|
||||||
|
|
||||||
--mooncore-radius-selector: @(Theme.Content.RadiusSelector)rem;
|
--radius-selector: @(Theme.Content.RadiusSelector)rem;
|
||||||
--mooncore-radius-field: @(Theme.Content.RadiusField)rem;
|
--radius-field: @(Theme.Content.RadiusField)rem;
|
||||||
--mooncore-radius-box: @(Theme.Content.RadiusBox)rem;
|
--radius-box: @(Theme.Content.RadiusBox)rem;
|
||||||
|
|
||||||
--mooncore-size-selector: @(Theme.Content.SizeSelector)rem;
|
--size-selector: @(Theme.Content.SizeSelector)rem;
|
||||||
--mooncore-size-field: @(Theme.Content.SizeField)rem;
|
--size-field: @(Theme.Content.SizeField)rem;
|
||||||
|
|
||||||
--mooncore-border: @(Theme.Content.Border)px;
|
--border: @(Theme.Content.Border)px;
|
||||||
--mooncore-depth: @(Theme.Content.Depth);
|
--depth: @(Theme.Content.Depth);
|
||||||
--mooncore-noise: @(Theme.Content.Noise);
|
--noise: @(Theme.Content.Noise);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Helpers;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
using Moonlight.ApiServer.Implementations.LocalAuth;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Controllers.LocalAuth;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/localAuth")]
|
||||||
|
public class LocalAuthController : Controller
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<User> UserRepository;
|
||||||
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
private readonly IAuthenticationService AuthenticationService;
|
||||||
|
private readonly IOptionsMonitor<LocalAuthOptions> Options;
|
||||||
|
private readonly ILogger<LocalAuthController> Logger;
|
||||||
|
private readonly AppConfiguration Configuration;
|
||||||
|
|
||||||
|
public LocalAuthController(
|
||||||
|
DatabaseRepository<User> userRepository,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IAuthenticationService authenticationService,
|
||||||
|
IOptionsMonitor<LocalAuthOptions> options,
|
||||||
|
ILogger<LocalAuthController> logger,
|
||||||
|
AppConfiguration configuration
|
||||||
|
)
|
||||||
|
{
|
||||||
|
UserRepository = userRepository;
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
AuthenticationService = authenticationService;
|
||||||
|
Options = options;
|
||||||
|
Logger = logger;
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[HttpGet("login")]
|
||||||
|
public async Task<ActionResult> LoginAsync()
|
||||||
|
{
|
||||||
|
var html = await ComponentHelper.RenderToHtmlAsync<Login>(ServiceProvider);
|
||||||
|
|
||||||
|
return Content(html, "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("register")]
|
||||||
|
public async Task<ActionResult> RegisterAsync()
|
||||||
|
{
|
||||||
|
var html = await ComponentHelper.RenderToHtmlAsync<Register>(ServiceProvider);
|
||||||
|
|
||||||
|
return Content(html, "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<ActionResult> LoginAsync([FromForm] string email, [FromForm] string password)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Perform login
|
||||||
|
var user = await InternalLoginAsync(email, password);
|
||||||
|
|
||||||
|
// Login user
|
||||||
|
var options = Options.Get(LocalAuthConstants.AuthenticationScheme);
|
||||||
|
|
||||||
|
await AuthenticationService.SignInAsync(HttpContext, options.SignInScheme, new ClaimsPrincipal(
|
||||||
|
new ClaimsIdentity(
|
||||||
|
[
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||||
|
new Claim(ClaimTypes.Name, user.Username)
|
||||||
|
],
|
||||||
|
LocalAuthConstants.AuthenticationScheme
|
||||||
|
)
|
||||||
|
), new AuthenticationProperties());
|
||||||
|
|
||||||
|
// Redirect back to wasm app
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
string errorMessage;
|
||||||
|
|
||||||
|
if (e is AggregateException aggregateException)
|
||||||
|
errorMessage = aggregateException.Message;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = "An internal error occured";
|
||||||
|
Logger.LogError(e, "An unhandled error occured while logging in user");
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = await ComponentHelper.RenderToHtmlAsync<Login>(ServiceProvider,
|
||||||
|
parameters => { parameters["ErrorMessage"] = errorMessage; });
|
||||||
|
|
||||||
|
return Content(html, "text/html");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<ActionResult> RegisterAsync([FromForm] string email, [FromForm] string password, [FromForm] string username)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Perform register
|
||||||
|
var user = await InternalRegisterAsync(username, email, password);
|
||||||
|
|
||||||
|
// Login user
|
||||||
|
var options = Options.Get(LocalAuthConstants.AuthenticationScheme);
|
||||||
|
|
||||||
|
await AuthenticationService.SignInAsync(HttpContext, options.SignInScheme, new ClaimsPrincipal(
|
||||||
|
new ClaimsIdentity(
|
||||||
|
[
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||||
|
new Claim(ClaimTypes.Name, user.Username)
|
||||||
|
],
|
||||||
|
LocalAuthConstants.AuthenticationScheme
|
||||||
|
)
|
||||||
|
), new AuthenticationProperties());
|
||||||
|
|
||||||
|
// Redirect back to wasm app
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
string errorMessage;
|
||||||
|
|
||||||
|
if (e is AggregateException aggregateException)
|
||||||
|
errorMessage = aggregateException.Message;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = "An internal error occured";
|
||||||
|
Logger.LogError(e, "An unhandled error occured while logging in user");
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = await ComponentHelper.RenderToHtmlAsync<Register>(ServiceProvider,
|
||||||
|
parameters => { parameters["ErrorMessage"] = errorMessage; });
|
||||||
|
|
||||||
|
return Content(html, "text/html");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<User> InternalRegisterAsync(string username, string email, string password)
|
||||||
|
{
|
||||||
|
email = email.ToLower();
|
||||||
|
username = username.ToLower();
|
||||||
|
|
||||||
|
if (await UserRepository.Get().AnyAsync(x => x.Username == username))
|
||||||
|
throw new AggregateException("A account with that username already exists");
|
||||||
|
|
||||||
|
if (await UserRepository.Get().AnyAsync(x => x.Email == email))
|
||||||
|
throw new AggregateException("A account with that email already exists");
|
||||||
|
|
||||||
|
string[] permissions = [];
|
||||||
|
|
||||||
|
if (Configuration.Authentication.FirstUserAdmin)
|
||||||
|
{
|
||||||
|
var count = await UserRepository
|
||||||
|
.Get()
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
permissions = ["*"];
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = new User()
|
||||||
|
{
|
||||||
|
Username = username,
|
||||||
|
Email = email,
|
||||||
|
Password = HashHelper.Hash(password),
|
||||||
|
Permissions = permissions
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalUser = await UserRepository.AddAsync(user);
|
||||||
|
|
||||||
|
return finalUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<User> InternalLoginAsync(string email, string password)
|
||||||
|
{
|
||||||
|
email = email.ToLower();
|
||||||
|
|
||||||
|
var user = await UserRepository
|
||||||
|
.Get()
|
||||||
|
.FirstOrDefaultAsync(x => x.Email == email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
throw new AggregateException("Invalid combination of email and password");
|
||||||
|
|
||||||
|
if (!HashHelper.Verify(password, user.Password))
|
||||||
|
throw new AggregateException("Invalid combination of email and password");
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<html lang="en" class="h-full bg-background">
|
<html lang="en" class="h-full bg-base-200">
|
||||||
<head>
|
<head>
|
||||||
<title>Login into your account</title>
|
<title>Login into your account</title>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
<div class="flex h-auto min-h-screen items-center justify-center overflow-x-hidden py-10">
|
<div class="flex h-auto min-h-screen items-center justify-center overflow-x-hidden py-10">
|
||||||
<div class="relative flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
<div class="relative flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div class="bg-base-100 shadow-base-300/20 z-1 w-full space-y-6 rounded-xl p-6 shadow-md sm:min-w-md lg:p-8">
|
||||||
class="bg-base-100 shadow-base-300/20 z-1 w-full space-y-6 rounded-xl p-6 shadow-md sm:min-w-md lg:p-8">
|
|
||||||
<div class="flex justify-center items-center gap-3">
|
<div class="flex justify-center items-center gap-3">
|
||||||
<img src="/_content/Moonlight.Client/svg/logo.svg" class="size-12" alt="brand-logo"/>
|
<img src="/_content/Moonlight.Client/svg/logo.svg" class="size-12" alt="brand-logo"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,8 +41,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<p class="text-base-content/80 mb-4 text-center">
|
<p class="text-base-content/80 mb-4 text-center">
|
||||||
No account?
|
No account?
|
||||||
<a href="?client_id=@(ClientId)&redirect_uri=@(RedirectUri)&response_type=@(ResponseType)&view=register"
|
<a href="/api/localAuth/register" class="link link-animated link-primary font-normal">Create an account</a>
|
||||||
class="link link-animated link-primary font-normal">Create an account</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,8 +53,5 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public string ClientId { get; set; }
|
|
||||||
[Parameter] public string RedirectUri { get; set; }
|
|
||||||
[Parameter] public string ResponseType { get; set; }
|
|
||||||
[Parameter] public string? ErrorMessage { get; set; }
|
[Parameter] public string? ErrorMessage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<html lang="en" class="h-full bg-background">
|
<html lang="en" class="h-full bg-base-200">
|
||||||
<head>
|
<head>
|
||||||
<title>Register a new account</title>
|
<title>Register a new account</title>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
<div class="flex h-auto min-h-screen items-center justify-center overflow-x-hidden py-10">
|
<div class="flex h-auto min-h-screen items-center justify-center overflow-x-hidden py-10">
|
||||||
<div class="relative flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
<div class="relative flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div class="bg-base-100 shadow-base-300/20 z-1 w-full space-y-6 rounded-xl p-6 shadow-md sm:min-w-md lg:p-8">
|
||||||
class="bg-base-100 shadow-base-300/20 z-1 w-full space-y-6 rounded-xl p-6 shadow-md sm:min-w-md lg:p-8">
|
|
||||||
<div class="flex justify-center items-center gap-3">
|
<div class="flex justify-center items-center gap-3">
|
||||||
<img src="/_content/Moonlight.Client/svg/logo.svg" class="size-12" alt="brand-logo"/>
|
<img src="/_content/Moonlight.Client/svg/logo.svg" class="size-12" alt="brand-logo"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,8 +46,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<p class="text-base-content/80 mb-4 text-center">
|
<p class="text-base-content/80 mb-4 text-center">
|
||||||
Already registered?
|
Already registered?
|
||||||
<a href="?client_id=@(ClientId)&redirect_uri=@(RedirectUri)&response_type=@(ResponseType)&view=login"
|
<a href="/api/localAuth/login" class="link link-animated link-primary font-normal">Login into your account</a>
|
||||||
class="link link-animated link-primary font-normal">Login into your account</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,8 +57,5 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public string ClientId { get; set; }
|
|
||||||
[Parameter] public string RedirectUri { get; set; }
|
|
||||||
[Parameter] public string ResponseType { get; set; }
|
|
||||||
[Parameter] public string? ErrorMessage { get; set; }
|
[Parameter] public string? ErrorMessage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using MoonCore.Extended.Helpers;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.Shared.Http.Responses.OAuth2;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.OAuth2;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("oauth2")]
|
|
||||||
public partial class OAuth2Controller : Controller
|
|
||||||
{
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
|
||||||
|
|
||||||
private readonly string ExpectedRedirectUri;
|
|
||||||
|
|
||||||
public OAuth2Controller(AppConfiguration configuration, DatabaseRepository<User> userRepository)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
UserRepository = userRepository;
|
|
||||||
|
|
||||||
ExpectedRedirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect)
|
|
||||||
? Configuration.PublicUrl
|
|
||||||
: Configuration.Authentication.OAuth2.AuthorizationRedirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpGet("authorize")]
|
|
||||||
public async Task Authorize(
|
|
||||||
[FromQuery(Name = "client_id")] string clientId,
|
|
||||||
[FromQuery(Name = "redirect_uri")] string redirectUri,
|
|
||||||
[FromQuery(Name = "response_type")] string responseType,
|
|
||||||
[FromQuery(Name = "view")] string view = "login"
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (!Configuration.Authentication.EnableLocalOAuth2)
|
|
||||||
throw new HttpApiException("Local OAuth2 has been disabled", 403);
|
|
||||||
|
|
||||||
if (Configuration.Authentication.OAuth2.ClientId != clientId ||
|
|
||||||
redirectUri != ExpectedRedirectUri ||
|
|
||||||
responseType != "code")
|
|
||||||
{
|
|
||||||
throw new HttpApiException("Invalid oauth2 request", 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
string html;
|
|
||||||
|
|
||||||
if (view == "register")
|
|
||||||
{
|
|
||||||
html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
|
|
||||||
{
|
|
||||||
parameters.Add("ClientId", clientId);
|
|
||||||
parameters.Add("RedirectUri", redirectUri);
|
|
||||||
parameters.Add("ResponseType", responseType);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
|
|
||||||
{
|
|
||||||
parameters.Add("ClientId", clientId);
|
|
||||||
parameters.Add("RedirectUri", redirectUri);
|
|
||||||
parameters.Add("ResponseType", responseType);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await Results
|
|
||||||
.Text(html, "text/html", Encoding.UTF8)
|
|
||||||
.ExecuteAsync(HttpContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpPost("authorize")]
|
|
||||||
public async Task AuthorizePost(
|
|
||||||
[FromQuery(Name = "client_id")] string clientId,
|
|
||||||
[FromQuery(Name = "redirect_uri")] string redirectUri,
|
|
||||||
[FromQuery(Name = "response_type")] string responseType,
|
|
||||||
[FromForm(Name = "email")] [EmailAddress(ErrorMessage = "You need to provide a valid email address")] string email,
|
|
||||||
[FromForm(Name = "password")] string password,
|
|
||||||
[FromForm(Name = "username")] string username = "",
|
|
||||||
[FromQuery(Name = "view")] string view = "login"
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (!Configuration.Authentication.EnableLocalOAuth2)
|
|
||||||
throw new HttpApiException("Local OAuth2 has been disabled", 403);
|
|
||||||
|
|
||||||
if (Configuration.Authentication.OAuth2.ClientId != clientId ||
|
|
||||||
redirectUri != ExpectedRedirectUri ||
|
|
||||||
responseType != "code")
|
|
||||||
{
|
|
||||||
throw new HttpApiException("Invalid oauth2 request", 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (view == "register" && string.IsNullOrEmpty(username))
|
|
||||||
throw new HttpApiException("You need to provide a username", 400);
|
|
||||||
|
|
||||||
string? errorMessage = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (view == "register")
|
|
||||||
{
|
|
||||||
var user = await Register(username, email, password);
|
|
||||||
var code = await GenerateCode(user);
|
|
||||||
|
|
||||||
Response.Redirect($"{redirectUri}?code={code}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var user = await Login(email, password);
|
|
||||||
var code = await GenerateCode(user);
|
|
||||||
|
|
||||||
Response.Redirect($"{redirectUri}?code={code}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpApiException e)
|
|
||||||
{
|
|
||||||
errorMessage = e.Title;
|
|
||||||
|
|
||||||
string html;
|
|
||||||
|
|
||||||
if (view == "register")
|
|
||||||
{
|
|
||||||
html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
|
|
||||||
{
|
|
||||||
parameters.Add("ClientId", clientId);
|
|
||||||
parameters.Add("RedirectUri", redirectUri);
|
|
||||||
parameters.Add("ResponseType", responseType);
|
|
||||||
parameters.Add("ErrorMessage", errorMessage!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
|
|
||||||
{
|
|
||||||
parameters.Add("ClientId", clientId);
|
|
||||||
parameters.Add("RedirectUri", redirectUri);
|
|
||||||
parameters.Add("ResponseType", responseType);
|
|
||||||
parameters.Add("ErrorMessage", errorMessage!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await Results
|
|
||||||
.Text(html, "text/html", Encoding.UTF8)
|
|
||||||
.ExecuteAsync(HttpContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpPost("handle")]
|
|
||||||
public async Task<OAuth2HandleResponse> Handle(
|
|
||||||
[FromForm(Name = "grant_type")] string grantType,
|
|
||||||
[FromForm(Name = "code")] string code,
|
|
||||||
[FromForm(Name = "redirect_uri")] string redirectUri,
|
|
||||||
[FromForm(Name = "client_id")] string clientId
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (!Configuration.Authentication.EnableLocalOAuth2)
|
|
||||||
throw new HttpApiException("Local OAuth2 has been disabled", 403);
|
|
||||||
|
|
||||||
// Check header
|
|
||||||
if (!Request.Headers.ContainsKey("Authorization"))
|
|
||||||
throw new HttpApiException("You are missing the Authorization header", 400);
|
|
||||||
|
|
||||||
var authorizationHeaderValue = Request.Headers["Authorization"].FirstOrDefault() ?? "";
|
|
||||||
|
|
||||||
if (authorizationHeaderValue != $"Basic {Configuration.Authentication.OAuth2.ClientSecret}")
|
|
||||||
throw new HttpApiException("Invalid Authorization header value", 400);
|
|
||||||
|
|
||||||
// Check form
|
|
||||||
if (grantType != "authorization_code")
|
|
||||||
throw new HttpApiException("Invalid grant type provided", 400);
|
|
||||||
|
|
||||||
if (clientId != Configuration.Authentication.OAuth2.ClientId)
|
|
||||||
throw new HttpApiException("Invalid client id provided", 400);
|
|
||||||
|
|
||||||
if (redirectUri != ExpectedRedirectUri)
|
|
||||||
throw new HttpApiException("Invalid redirect uri provided", 400);
|
|
||||||
|
|
||||||
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
|
||||||
|
|
||||||
ClaimsPrincipal? codeData;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
codeData = jwtSecurityTokenHandler.ValidateToken(code, new TokenValidationParameters()
|
|
||||||
{
|
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
|
||||||
Configuration.Authentication.OAuth2.Secret
|
|
||||||
)),
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
ValidateLifetime = true,
|
|
||||||
ClockSkew = TimeSpan.Zero,
|
|
||||||
ValidateAudience = false,
|
|
||||||
ValidateIssuer = false
|
|
||||||
}, out _);
|
|
||||||
}
|
|
||||||
catch (SecurityTokenException)
|
|
||||||
{
|
|
||||||
throw new HttpApiException("Invalid code provided", 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codeData == null)
|
|
||||||
throw new HttpApiException("Invalid code provided", 400);
|
|
||||||
|
|
||||||
var userIdClaim = codeData.Claims.FirstOrDefault(x => x.Type == "id");
|
|
||||||
|
|
||||||
if (userIdClaim == null)
|
|
||||||
throw new HttpApiException("Malformed code provided", 400);
|
|
||||||
|
|
||||||
if (!int.TryParse(userIdClaim.Value, out var userId))
|
|
||||||
throw new HttpApiException("Malformed code provided", 400);
|
|
||||||
|
|
||||||
var user = UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefault(x => x.Id == userId);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw new HttpApiException("Malformed code provided", 400);
|
|
||||||
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
UserId = user.Id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<string> GenerateCode(User user)
|
|
||||||
{
|
|
||||||
var securityTokenDescriptor = new SecurityTokenDescriptor()
|
|
||||||
{
|
|
||||||
Expires = DateTime.Now.AddMinutes(1),
|
|
||||||
IssuedAt = DateTime.Now,
|
|
||||||
NotBefore = DateTime.Now.AddMinutes(-1),
|
|
||||||
Claims = new Dictionary<string, object>()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
"id",
|
|
||||||
user.Id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SigningCredentials = new SigningCredentials(
|
|
||||||
new SymmetricSecurityKey(
|
|
||||||
Encoding.UTF8.GetBytes(Configuration.Authentication.OAuth2.Secret)
|
|
||||||
),
|
|
||||||
SecurityAlgorithms.HmacSha256
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
|
||||||
var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
|
|
||||||
|
|
||||||
return Task.FromResult(
|
|
||||||
jwtSecurityTokenHandler.WriteToken(securityToken)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<User> Register(string username, string email, string password)
|
|
||||||
{
|
|
||||||
if (await UserRepository.Get().AnyAsync(x => x.Username == username))
|
|
||||||
throw new HttpApiException("A account with that username already exists", 400);
|
|
||||||
|
|
||||||
if (await UserRepository.Get().AnyAsync(x => x.Email == email))
|
|
||||||
throw new HttpApiException("A account with that email already exists", 400);
|
|
||||||
|
|
||||||
if (!UsernameRegex().IsMatch(username))
|
|
||||||
throw new HttpApiException("The username is only allowed to be contained out of small characters and numbers", 400);
|
|
||||||
|
|
||||||
var user = new User()
|
|
||||||
{
|
|
||||||
Username = username,
|
|
||||||
Email = email,
|
|
||||||
Password = HashHelper.Hash(password),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Configuration.Authentication.OAuth2.FirstUserAdmin)
|
|
||||||
{
|
|
||||||
var userCount = await UserRepository.Get().CountAsync();
|
|
||||||
|
|
||||||
if (userCount == 0)
|
|
||||||
user.Permissions = ["*"];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return await UserRepository.Add(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<User> Login(string email, string password)
|
|
||||||
{
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Email == email);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw new HttpApiException("Invalid combination of email and password", 400);
|
|
||||||
|
|
||||||
if (!HashHelper.Verify(password, user.Password))
|
|
||||||
throw new HttpApiException("Invalid combination of email and password", 400);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex("^[a-z][a-z0-9]*$")]
|
|
||||||
private static partial Regex UsernameRegex();
|
|
||||||
}
|
|
||||||
@@ -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 =>
|
||||||
{
|
{
|
||||||
|
|||||||
14
Moonlight.ApiServer/Http/Hubs/DiagnoseHub.cs
Normal file
14
Moonlight.ApiServer/Http/Hubs/DiagnoseHub.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Hubs;
|
||||||
|
|
||||||
|
[Authorize(Policy = "permissions:admin.system.diagnose")]
|
||||||
|
public class DiagnoseHub : Hub
|
||||||
|
{
|
||||||
|
[HubMethodName("Ping")]
|
||||||
|
public async Task PingAsync()
|
||||||
|
{
|
||||||
|
await Clients.All.SendAsync("Pong");
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Moonlight.ApiServer/IAssemblyMarker.cs
Normal file
3
Moonlight.ApiServer/IAssemblyMarker.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Moonlight.ApiServer;
|
||||||
|
|
||||||
|
public interface IAssemblyMarker;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Text.Json;
|
using MoonCore.Yaml;
|
||||||
using Moonlight.ApiServer.Configuration;
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Extensions;
|
using Moonlight.ApiServer.Extensions;
|
||||||
using Moonlight.ApiServer.Interfaces;
|
using Moonlight.ApiServer.Interfaces;
|
||||||
@@ -8,11 +9,11 @@ namespace Moonlight.ApiServer.Implementations.Diagnose;
|
|||||||
|
|
||||||
public class CoreConfigDiagnoseProvider : IDiagnoseProvider
|
public class CoreConfigDiagnoseProvider : IDiagnoseProvider
|
||||||
{
|
{
|
||||||
private readonly AppConfiguration Config;
|
private readonly AppConfiguration Configuration;
|
||||||
|
|
||||||
public CoreConfigDiagnoseProvider(AppConfiguration config)
|
public CoreConfigDiagnoseProvider(AppConfiguration configuration)
|
||||||
{
|
{
|
||||||
Config = config;
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CheckForNullOrEmpty(string? content)
|
private string CheckForNullOrEmpty(string? content)
|
||||||
@@ -22,36 +23,25 @@ public class CoreConfigDiagnoseProvider : IDiagnoseProvider
|
|||||||
: "ISNOTEMPTY";
|
: "ISNOTEMPTY";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ModifyZipArchive(ZipArchive archive)
|
public async Task ModifyZipArchiveAsync(ZipArchive archive)
|
||||||
{
|
{
|
||||||
var json = JsonSerializer.Serialize(Config);
|
try
|
||||||
var config = JsonSerializer.Deserialize<AppConfiguration>(json);
|
|
||||||
|
|
||||||
if (config == null)
|
|
||||||
{
|
{
|
||||||
await archive.AddText("core/config.txt", "Could not fetch config.");
|
var configString = YamlSerializer.Serialize(Configuration);
|
||||||
return;
|
var configuration = YamlSerializer.Deserialize<AppConfiguration>(configString);
|
||||||
|
|
||||||
|
configuration.Database.Password = CheckForNullOrEmpty(configuration.Database.Password);
|
||||||
|
configuration.Authentication.Secret = CheckForNullOrEmpty(configuration.Authentication.Secret);
|
||||||
|
configuration.SignalR.RedisConnectionString = CheckForNullOrEmpty(configuration.SignalR.RedisConnectionString);
|
||||||
|
|
||||||
|
await archive.AddTextAsync(
|
||||||
|
"core/config.txt",
|
||||||
|
YamlSerializer.Serialize(configuration)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
await archive.AddTextAsync("core/config.txt", $"Unable to load config: {e.ToStringDemystified()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Database.Password = CheckForNullOrEmpty(config.Database.Password);
|
|
||||||
|
|
||||||
config.Authentication.OAuth2.ClientSecret = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientSecret);
|
|
||||||
|
|
||||||
config.Authentication.OAuth2.Secret = CheckForNullOrEmpty(config.Authentication.OAuth2.Secret);
|
|
||||||
|
|
||||||
config.Authentication.Secret = CheckForNullOrEmpty(config.Authentication.Secret);
|
|
||||||
|
|
||||||
config.Authentication.OAuth2.ClientId = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientId);
|
|
||||||
|
|
||||||
await archive.AddText(
|
|
||||||
"core/config.txt",
|
|
||||||
JsonSerializer.Serialize(
|
|
||||||
config,
|
|
||||||
new JsonSerializerOptions()
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,17 +6,16 @@ namespace Moonlight.ApiServer.Implementations.Diagnose;
|
|||||||
|
|
||||||
public class LogsDiagnoseProvider : IDiagnoseProvider
|
public class LogsDiagnoseProvider : IDiagnoseProvider
|
||||||
{
|
{
|
||||||
public async Task ModifyZipArchive(ZipArchive archive)
|
public async Task ModifyZipArchiveAsync(ZipArchive archive)
|
||||||
{
|
{
|
||||||
var path = Path.Combine("storage", "logs", "latest.log");
|
var path = Path.Combine("storage", "logs", "moonlight.log");
|
||||||
|
|
||||||
if (!File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
await archive.AddText("logs.txt", "Logs file latest.log has not been found");
|
var logsContent = await File.ReadAllTextAsync(path);
|
||||||
return;
|
await archive.AddTextAsync("logs.txt", logsContent);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var logsContent = await File.ReadAllTextAsync(path);
|
await archive.AddTextAsync("logs.txt", "Logs file moonlight.log has not been found");
|
||||||
await archive.AddText("logs.txt", logsContent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.ApiServer.Implementations.LocalAuth;
|
||||||
|
|
||||||
|
public static class LocalAuthConstants
|
||||||
|
{
|
||||||
|
public const string AuthenticationScheme = "LocalAuth";
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Implementations.LocalAuth;
|
||||||
|
|
||||||
|
public class LocalAuthHandler : AuthenticationHandler<LocalAuthOptions>
|
||||||
|
{
|
||||||
|
public LocalAuthHandler(
|
||||||
|
IOptionsMonitor<LocalAuthOptions> options,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder
|
||||||
|
) : base(options, logger, encoder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(
|
||||||
|
AuthenticateResult.Fail("Local authentication does not directly support AuthenticateAsync")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||||
|
{
|
||||||
|
await Results
|
||||||
|
.Redirect("/api/localAuth")
|
||||||
|
.ExecuteAsync(Context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Implementations.LocalAuth;
|
||||||
|
|
||||||
|
public class LocalAuthOptions : AuthenticationSchemeOptions
|
||||||
|
{
|
||||||
|
public string? SignInScheme { get; set; }
|
||||||
|
}
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using Moonlight.ApiServer.Interfaces;
|
|
||||||
using Moonlight.Shared.Http.Responses.OAuth2;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations;
|
|
||||||
|
|
||||||
public class LocalOAuth2Provider : IOAuth2Provider
|
|
||||||
{
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
private readonly ILogger<LocalOAuth2Provider> Logger;
|
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
|
||||||
|
|
||||||
public LocalOAuth2Provider(
|
|
||||||
AppConfiguration configuration,
|
|
||||||
ILogger<LocalOAuth2Provider> logger,
|
|
||||||
DatabaseRepository<User> userRepository
|
|
||||||
)
|
|
||||||
{
|
|
||||||
UserRepository = userRepository;
|
|
||||||
Configuration = configuration;
|
|
||||||
Logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<string> Start()
|
|
||||||
{
|
|
||||||
var redirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect)
|
|
||||||
? Configuration.PublicUrl
|
|
||||||
: Configuration.Authentication.OAuth2.AuthorizationRedirect;
|
|
||||||
|
|
||||||
var endpoint = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationEndpoint)
|
|
||||||
? Configuration.PublicUrl + "/oauth2/authorize"
|
|
||||||
: Configuration.Authentication.OAuth2.AuthorizationEndpoint;
|
|
||||||
|
|
||||||
var clientId = Configuration.Authentication.OAuth2.ClientId;
|
|
||||||
|
|
||||||
var url = $"{endpoint}" +
|
|
||||||
$"?client_id={clientId}" +
|
|
||||||
$"&redirect_uri={redirectUri}" +
|
|
||||||
$"&response_type=code";
|
|
||||||
|
|
||||||
return Task.FromResult(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User?> Complete(string code)
|
|
||||||
{
|
|
||||||
// Create http client to call the auth provider
|
|
||||||
var httpClient = new HttpClient();
|
|
||||||
using var httpApiClient = new HttpApiClient(httpClient);
|
|
||||||
|
|
||||||
httpClient.DefaultRequestHeaders.Add("Authorization",
|
|
||||||
$"Basic {Configuration.Authentication.OAuth2.ClientSecret}");
|
|
||||||
|
|
||||||
// Build access endpoint
|
|
||||||
var accessEndpoint = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AccessEndpoint)
|
|
||||||
? $"{Configuration.PublicUrl}/oauth2/handle"
|
|
||||||
: Configuration.Authentication.OAuth2.AccessEndpoint;
|
|
||||||
|
|
||||||
// Build redirect uri
|
|
||||||
var redirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect)
|
|
||||||
? Configuration.PublicUrl
|
|
||||||
: Configuration.Authentication.OAuth2.AuthorizationRedirect;
|
|
||||||
|
|
||||||
// Call the auth provider
|
|
||||||
OAuth2HandleResponse handleData;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
handleData = await httpApiClient.PostJson<OAuth2HandleResponse>(accessEndpoint, new FormUrlEncodedContent(
|
|
||||||
[
|
|
||||||
new KeyValuePair<string, string>("grant_type", "authorization_code"),
|
|
||||||
new KeyValuePair<string, string>("code", code),
|
|
||||||
new KeyValuePair<string, string>("redirect_uri", redirectUri),
|
|
||||||
new KeyValuePair<string, string>("client_id", Configuration.Authentication.OAuth2.ClientId)
|
|
||||||
]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (HttpApiException e)
|
|
||||||
{
|
|
||||||
if (e.Status == 400)
|
|
||||||
Logger.LogTrace("The auth server returned an error: {e}", e);
|
|
||||||
else
|
|
||||||
Logger.LogCritical("The auth server returned an error: {e}", e);
|
|
||||||
|
|
||||||
throw new HttpApiException("Unable to request user data", 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notice: We just look up the user id here
|
|
||||||
// which works as our oauth2 provider is using the same db.
|
|
||||||
// a real oauth2 provider would create a user here
|
|
||||||
|
|
||||||
// Handle the returned data
|
|
||||||
var userId = handleData.UserId;
|
|
||||||
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == userId);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Moonlight.ApiServer.Configuration;
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Database;
|
using Moonlight.ApiServer.Database;
|
||||||
@@ -11,17 +11,21 @@ using Moonlight.ApiServer.Interfaces;
|
|||||||
using Moonlight.ApiServer.Models;
|
using Moonlight.ApiServer.Models;
|
||||||
using Moonlight.ApiServer.Plugins;
|
using Moonlight.ApiServer.Plugins;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
|
using OpenTelemetry.Logs;
|
||||||
using OpenTelemetry.Metrics;
|
using OpenTelemetry.Metrics;
|
||||||
|
using OpenTelemetry.Resources;
|
||||||
|
using OpenTelemetry.Trace;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations.Startup;
|
namespace Moonlight.ApiServer.Implementations.Startup;
|
||||||
|
|
||||||
public class CoreStartup : IPluginStartup
|
public class CoreStartup : IPluginStartup
|
||||||
{
|
{
|
||||||
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
|
public void AddPlugin(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
builder.Configuration.Bind(configuration);
|
||||||
#region Api Docs
|
|
||||||
|
#region Api Docs
|
||||||
|
|
||||||
if (configuration.Development.EnableApiDocs)
|
if (configuration.Development.EnableApiDocs)
|
||||||
{
|
{
|
||||||
@@ -64,22 +68,56 @@ public class CoreStartup : IPluginStartup
|
|||||||
|
|
||||||
#region Prometheus
|
#region Prometheus
|
||||||
|
|
||||||
if (configuration.Metrics.Enable)
|
if (configuration.OpenTelemetry.Enable)
|
||||||
{
|
{
|
||||||
builder.Services.AddSingleton<MetricsBackgroundService>();
|
var openTel = builder.Services.AddOpenTelemetry();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<MetricsBackgroundService>());
|
var openTelConfig = configuration.OpenTelemetry;
|
||||||
|
|
||||||
builder.Services.AddSingleton<IMetric, ApplicationMetric>();
|
var resourceBuilder = ResourceBuilder.CreateDefault();
|
||||||
builder.Services.AddSingleton<IMetric, UsersMetric>();
|
resourceBuilder.AddService(serviceName: "moonlight");
|
||||||
|
|
||||||
builder.Services.AddOpenTelemetry()
|
openTel.ConfigureResource(x => x.AddService(serviceName: "moonlight"));
|
||||||
.WithMetrics(providerBuilder =>
|
|
||||||
|
if (openTelConfig.Metrics.Enable)
|
||||||
|
{
|
||||||
|
builder.Services.AddSingleton<MetricsBackgroundService>();
|
||||||
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<MetricsBackgroundService>());
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IMetric, ApplicationMetric>();
|
||||||
|
builder.Services.AddSingleton<IMetric, UsersMetric>();
|
||||||
|
|
||||||
|
openTel.WithMetrics(providerBuilder =>
|
||||||
{
|
{
|
||||||
providerBuilder.AddPrometheusExporter();
|
|
||||||
providerBuilder.AddAspNetCoreInstrumentation();
|
providerBuilder.AddAspNetCoreInstrumentation();
|
||||||
|
providerBuilder.AddOtlpExporter();
|
||||||
|
|
||||||
|
if (openTelConfig.Metrics.EnablePrometheus)
|
||||||
|
providerBuilder.AddPrometheusExporter();
|
||||||
|
|
||||||
providerBuilder.AddMeter("moonlight");
|
providerBuilder.AddMeter("moonlight");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openTelConfig.Logs.Enable)
|
||||||
|
{
|
||||||
|
openTel.WithLogging();
|
||||||
|
|
||||||
|
builder.Logging.AddOpenTelemetry(options =>
|
||||||
|
{
|
||||||
|
options.SetResourceBuilder(resourceBuilder);
|
||||||
|
options.AddOtlpExporter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openTelConfig.Traces.Enable)
|
||||||
|
{
|
||||||
|
openTel.WithTracing(providerBuilder =>
|
||||||
|
{
|
||||||
|
providerBuilder.AddAspNetCoreInstrumentation();
|
||||||
|
|
||||||
|
providerBuilder.AddOtlpExporter();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -100,31 +138,27 @@ public class CoreStartup : IPluginStartup
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app)
|
public void UsePlugin(WebApplication app)
|
||||||
{
|
{
|
||||||
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
app.Configuration.Bind(configuration);
|
||||||
|
|
||||||
#region Prometheus
|
#region Prometheus
|
||||||
|
|
||||||
if (configuration.Metrics.Enable)
|
if (configuration.OpenTelemetry is { Enable: true, Metrics.EnablePrometheus: true })
|
||||||
app.UseOpenTelemetryPrometheusScrapingEndpoint();
|
app.UseOpenTelemetryPrometheusScrapingEndpoint();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder)
|
public void MapPlugin(WebApplication app)
|
||||||
{
|
{
|
||||||
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
app.Configuration.Bind(configuration);
|
||||||
|
|
||||||
if (configuration.Development.EnableApiDocs)
|
if (configuration.Development.EnableApiDocs)
|
||||||
routeBuilder.MapSwagger("/api/swagger/{documentName}");
|
app.MapSwagger("/api/swagger/{documentName}");
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using MoonCore.Extended.JwtInvalidation;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations;
|
|
||||||
|
|
||||||
public class UserAuthInvalidation : IJwtInvalidateHandler
|
|
||||||
{
|
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
|
||||||
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
|
||||||
|
|
||||||
public UserAuthInvalidation(
|
|
||||||
DatabaseRepository<User> userRepository,
|
|
||||||
DatabaseRepository<ApiKey> apiKeyRepository
|
|
||||||
)
|
|
||||||
{
|
|
||||||
UserRepository = userRepository;
|
|
||||||
ApiKeyRepository = apiKeyRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> Handle(ClaimsPrincipal principal)
|
|
||||||
{
|
|
||||||
var userIdClaim = principal.FindFirstValue("userId");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(userIdClaim))
|
|
||||||
{
|
|
||||||
var userId = int.Parse(userIdClaim);
|
|
||||||
|
|
||||||
var user = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == userId);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return true; // User is deleted, invalidate session
|
|
||||||
|
|
||||||
var iatStr = principal.FindFirstValue("iat")!;
|
|
||||||
var iat = DateTimeOffset.FromUnixTimeSeconds(long.Parse(iatStr));
|
|
||||||
|
|
||||||
// If the token has been issued before the token valid time, its expired, and we want to invalidate it
|
|
||||||
return user.TokenValidTimestamp > iat;
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiKeyIdClaim = principal.FindFirstValue("apiKeyId");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(apiKeyIdClaim))
|
|
||||||
{
|
|
||||||
var apiKeyId = int.Parse(apiKeyIdClaim);
|
|
||||||
|
|
||||||
var apiKey = await ApiKeyRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == apiKeyId);
|
|
||||||
|
|
||||||
// If the api key exists, we don't want to invalidate the request.
|
|
||||||
// If it doesn't exist we want to invalidate the request
|
|
||||||
return apiKey == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
Moonlight.ApiServer/Interfaces/IAuthCheckExtension.cs
Normal file
16
Moonlight.ApiServer/Interfaces/IAuthCheckExtension.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Interfaces;
|
||||||
|
|
||||||
|
public interface IAuthCheckExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This function will be called by the frontend reaching out to the api server for claim information.
|
||||||
|
/// You can use this function to give your frontend plugins access to user specific data which is
|
||||||
|
/// static for the session. E.g. the avatar url of a user
|
||||||
|
/// </summary>
|
||||||
|
/// <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>
|
||||||
|
public Task<AuthClaimResponse[]> GetFrontendClaimsAsync(ClaimsPrincipal principal);
|
||||||
|
}
|
||||||
@@ -4,5 +4,5 @@ namespace Moonlight.ApiServer.Interfaces;
|
|||||||
|
|
||||||
public interface IDiagnoseProvider
|
public interface IDiagnoseProvider
|
||||||
{
|
{
|
||||||
public Task ModifyZipArchive(ZipArchive archive);
|
public Task ModifyZipArchiveAsync(ZipArchive archive);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Interfaces;
|
|
||||||
|
|
||||||
public interface IOAuth2Provider
|
|
||||||
{
|
|
||||||
public Task<string> Start();
|
|
||||||
|
|
||||||
public Task<User?> Complete(string code);
|
|
||||||
}
|
|
||||||
25
Moonlight.ApiServer/Interfaces/IUserAuthExtension.cs
Normal file
25
Moonlight.ApiServer/Interfaces/IUserAuthExtension.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserAuthExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This function is called on every sign-in. It should be used to synchronize additional user data from the principal
|
||||||
|
/// or extend the claims saved in the user session
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The current user this method is called for</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>
|
||||||
|
public Task<bool> SyncAsync(User user, ClaimsPrincipal principal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IMPORTANT: Please note that heavy operations should not occur in this method as it will be called for every request
|
||||||
|
/// of every user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The current user this method is called for</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>
|
||||||
|
public Task<bool> ValidateAsync(User user, ClaimsPrincipal principal);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
19
Moonlight.ApiServer/Mappers/ApiKeyMapper.cs
Normal file
19
Moonlight.ApiServer/Mappers/ApiKeyMapper.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
15
Moonlight.ApiServer/Mappers/UserMapper.cs
Normal file
15
Moonlight.ApiServer/Mappers/UserMapper.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,47 +1,43 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
|
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Database\Migrations\" />
|
<Folder Include="Database\Migrations\"/>
|
||||||
<Folder Include="Helpers\" />
|
</ItemGroup>
|
||||||
</ItemGroup>
|
<PropertyGroup>
|
||||||
<PropertyGroup>
|
<PackageId>Moonlight.ApiServer</PackageId>
|
||||||
<PackageId>Moonlight.ApiServer</PackageId>
|
<Version>2.1.15</Version>
|
||||||
<Version>2.1.7</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>
|
<DevelopmentDependency>true</DevelopmentDependency>
|
||||||
<DevelopmentDependency>true</DevelopmentDependency>
|
<PackageTags>apiserver</PackageTags>
|
||||||
<PackageTags>apiserver</PackageTags>
|
<IsPackable>true</IsPackable>
|
||||||
<IsPackable>true</IsPackable>
|
</PropertyGroup>
|
||||||
</PropertyGroup>
|
<ItemGroup>
|
||||||
<ItemGroup>
|
<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.EntityFrameworkCore.Design" Version="9.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
|
||||||
<PackageReference Include="MoonCore" Version="1.9.2" />
|
<PackageReference Include="MoonCore" Version="2.0.6" />
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.3.5" />
|
<PackageReference Include="MoonCore.Extended" Version="1.4.2" />
|
||||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2" />
|
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1"/>
|
||||||
<PackageReference Include="Riok.Mapperly" Version="4.3.0-next.2" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/>
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.2" />
|
<PackageReference Include="Riok.Mapperly" Version="4.3.0-next.3" />
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
<PackageReference Include="SharpZipLib" Version="1.4.2"/>
|
||||||
</ItemGroup>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||||
<ItemGroup>
|
<PackageReference Include="Ben.Demystifier" Version="0.4.1"/>
|
||||||
<Compile Remove="storage\**\*" />
|
</ItemGroup>
|
||||||
<Content Remove="storage\**\*" />
|
|
||||||
<None Remove="storage\**\*" />
|
|
||||||
<None Remove="Properties\launchSettings.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Routing;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Plugins;
|
namespace Moonlight.ApiServer.Plugins;
|
||||||
|
|
||||||
public interface IPluginStartup
|
public interface IPluginStartup
|
||||||
{
|
{
|
||||||
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder);
|
public void AddPlugin(WebApplicationBuilder builder);
|
||||||
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app);
|
public void UsePlugin(WebApplication app);
|
||||||
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder);
|
public void MapPlugin(WebApplication app);
|
||||||
}
|
}
|
||||||
32
Moonlight.ApiServer/Services/ApiKeyAuthService.cs
Normal file
32
Moonlight.ApiServer/Services/ApiKeyAuthService.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Services;
|
||||||
|
|
||||||
|
public class ApiKeyAuthService
|
||||||
|
{
|
||||||
|
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
||||||
|
|
||||||
|
public ApiKeyAuthService(DatabaseRepository<ApiKey> apiKeyRepository)
|
||||||
|
{
|
||||||
|
ApiKeyRepository = apiKeyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
|
||||||
|
{
|
||||||
|
// Ignore malformed claims principal
|
||||||
|
if (principal is not { Identity.IsAuthenticated: true })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var apiKeyIdStr = principal.FindFirstValue("ApiKeyId");
|
||||||
|
|
||||||
|
if (!int.TryParse(apiKeyIdStr, out var apiKeyId))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return await ApiKeyRepository
|
||||||
|
.Get()
|
||||||
|
.AnyAsync(x => x.Id == apiKeyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,11 +29,11 @@ public class ApiKeyService
|
|||||||
Claims = new Dictionary<string, object>()
|
Claims = new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
"apiKeyId",
|
"ApiKeyId",
|
||||||
apiKey.Id
|
apiKey.Id
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"permissions",
|
"Permissions",
|
||||||
string.Join(";", apiKey.Permissions)
|
string.Join(";", apiKey.Permissions)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class DiagnoseService
|
|||||||
Logger = logger;
|
Logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<DiagnoseProvideResponse[]> GetProviders()
|
public Task<DiagnoseProvideResponse[]> GetProvidersAsync()
|
||||||
{
|
{
|
||||||
var availableProviders = new List<DiagnoseProvideResponse>();
|
var availableProviders = new List<DiagnoseProvideResponse>();
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ public class DiagnoseService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MemoryStream> GenerateDiagnose(string[] requestedProviders)
|
public async Task<MemoryStream> GenerateDiagnoseAsync(string[] requestedProviders)
|
||||||
{
|
{
|
||||||
IDiagnoseProvider[] providers;
|
IDiagnoseProvider[] providers;
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ public class DiagnoseService
|
|||||||
|
|
||||||
foreach (var provider in providers)
|
foreach (var provider in providers)
|
||||||
{
|
{
|
||||||
await provider.ModifyZipArchive(zipArchive);
|
await provider.ModifyZipArchiveAsync(zipArchive);
|
||||||
}
|
}
|
||||||
|
|
||||||
zipArchive.Dispose();
|
zipArchive.Dispose();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,7 @@ public class MetricsBackgroundService : BackgroundService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(
|
await Task.Delay(
|
||||||
TimeSpan.FromSeconds(Configuration.Metrics.Interval),
|
TimeSpan.FromSeconds(Configuration.OpenTelemetry.Metrics.Interval),
|
||||||
stoppingToken
|
stoppingToken
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
168
Moonlight.ApiServer/Services/UserAuthService.cs
Normal file
168
Moonlight.ApiServer/Services/UserAuthService.cs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Helpers;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
using Moonlight.ApiServer.Interfaces;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Services;
|
||||||
|
|
||||||
|
public class UserAuthService
|
||||||
|
{
|
||||||
|
private readonly ILogger<UserAuthService> Logger;
|
||||||
|
private readonly DatabaseRepository<User> UserRepository;
|
||||||
|
private readonly AppConfiguration Configuration;
|
||||||
|
private readonly IEnumerable<IUserAuthExtension> Extensions;
|
||||||
|
|
||||||
|
private const string UserIdClaim = "UserId";
|
||||||
|
private const string IssuedAtClaim = "IssuedAt";
|
||||||
|
|
||||||
|
public UserAuthService(
|
||||||
|
ILogger<UserAuthService> logger,
|
||||||
|
DatabaseRepository<User> userRepository,
|
||||||
|
AppConfiguration configuration,
|
||||||
|
IEnumerable<IUserAuthExtension> extensions
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
UserRepository = userRepository;
|
||||||
|
Configuration = configuration;
|
||||||
|
Extensions = extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||||
|
{
|
||||||
|
// Ignore malformed claims principal
|
||||||
|
if (principal is not { Identity.IsAuthenticated: true })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Search for email and username. We need both to create the user model if required.
|
||||||
|
// We do a ToLower here because external authentication provider might provide case-sensitive data
|
||||||
|
|
||||||
|
var email = principal.FindFirstValue(ClaimTypes.Email)?.ToLower();
|
||||||
|
var username = principal.FindFirstValue(ClaimTypes.Name)?.ToLower();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(username))
|
||||||
|
{
|
||||||
|
Logger.LogWarning(
|
||||||
|
"The authentication scheme {scheme} did not provide claim types: email, name. These are required to sync to user to the database",
|
||||||
|
principal.Identity.AuthenticationType
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you plan to use multiple auth providers it can be a good idea
|
||||||
|
// to use an identifier in the user model which consists of the provider and the NameIdentifier
|
||||||
|
// instead of the email address. For simplicity, we just use the email as the identifier so multiple auth providers
|
||||||
|
// can lead to the same account when the email matches
|
||||||
|
var user = await UserRepository
|
||||||
|
.Get()
|
||||||
|
.FirstOrDefaultAsync(u => u.Email == email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
string[] permissions = [];
|
||||||
|
|
||||||
|
// Yes I know we handle the first user admin thing in the LocalAuth too,
|
||||||
|
// but this only works fo the local auth. So if a user uses an external auth scheme
|
||||||
|
// like oauth2 discord, the first user admin toggle would do nothing
|
||||||
|
if (Configuration.Authentication.FirstUserAdmin)
|
||||||
|
{
|
||||||
|
var count = await UserRepository
|
||||||
|
.Get()
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
permissions = ["*"];
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await UserRepository.AddAsync(new User()
|
||||||
|
{
|
||||||
|
Email = email,
|
||||||
|
TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||||
|
Username = username,
|
||||||
|
Password = HashHelper.Hash(Formatter.GenerateString(64)),
|
||||||
|
Permissions = permissions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can sync other properties here
|
||||||
|
if (user.Username != username)
|
||||||
|
{
|
||||||
|
user.Username = username;
|
||||||
|
await UserRepository.UpdateAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich claims with required metadata
|
||||||
|
principal.Identities.First().AddClaims([
|
||||||
|
new Claim(UserIdClaim, user.Id.ToString()),
|
||||||
|
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()),
|
||||||
|
new Claim("Permissions", string.Join(';', user.Permissions))
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Call extensions
|
||||||
|
foreach (var extension in Extensions)
|
||||||
|
{
|
||||||
|
var result = await extension.SyncAsync(user, principal);
|
||||||
|
|
||||||
|
if (!result) // Exit immediately if result is false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
|
||||||
|
{
|
||||||
|
// Ignore malformed claims principal
|
||||||
|
if (principal is not { Identity.IsAuthenticated: true })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Validate if the user still exists, and then we want to validate the token issue time
|
||||||
|
// against the invalidation time
|
||||||
|
|
||||||
|
var userIdStr = principal.FindFirstValue(UserIdClaim);
|
||||||
|
|
||||||
|
if (!int.TryParse(userIdStr, out var userId))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var user = await UserRepository
|
||||||
|
.Get()
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Token time validation
|
||||||
|
var issuedAtStr = principal.FindFirstValue(IssuedAtClaim);
|
||||||
|
|
||||||
|
if (!long.TryParse(issuedAtStr, out var issuedAtUnix))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var issuedAt = DateTimeOffset
|
||||||
|
.FromUnixTimeSeconds(issuedAtUnix)
|
||||||
|
.ToUniversalTime();
|
||||||
|
|
||||||
|
// If the issued at timestamp is greater than the token validation timestamp
|
||||||
|
// everything is fine. If not it means that the token should be invalidated
|
||||||
|
// as it is too old
|
||||||
|
|
||||||
|
if (issuedAt < user.TokenValidTimestamp)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Call extensions
|
||||||
|
foreach (var extension in Extensions)
|
||||||
|
{
|
||||||
|
var result = await extension.ValidateAsync(user, principal);
|
||||||
|
|
||||||
|
if (!result) // Exit immediately if result is false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,64 +1,189 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using MoonCore.Extended.JwtInvalidation;
|
|
||||||
using MoonCore.Permissions;
|
using MoonCore.Permissions;
|
||||||
using Moonlight.ApiServer.Implementations;
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Interfaces;
|
using Moonlight.ApiServer.Implementations.LocalAuth;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task RegisterAuth()
|
private static void AddAuth(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
.AddAuthentication("coreAuthentication")
|
builder.Configuration.Bind(configuration);
|
||||||
.AddJwtBearer("coreAuthentication", options =>
|
|
||||||
|
builder.Services
|
||||||
|
.AddAuthentication(options => { options.DefaultScheme = "MainScheme"; })
|
||||||
|
.AddPolicyScheme("MainScheme", null, options =>
|
||||||
|
{
|
||||||
|
// If an api key is specified via the bearer auth header
|
||||||
|
// we want to use the ApiKey scheme for authenticating the request
|
||||||
|
options.ForwardDefaultSelector = context =>
|
||||||
|
{
|
||||||
|
var headers = context.Request.Headers;
|
||||||
|
|
||||||
|
// For regular api calls
|
||||||
|
if (headers.ContainsKey("Authorization"))
|
||||||
|
return "ApiKey";
|
||||||
|
|
||||||
|
// For websocket requests which cannot use the Authorization header
|
||||||
|
if (headers.Upgrade == "websocket" && headers.Connection == "Upgrade" && context.Request.Query.ContainsKey("access_token"))
|
||||||
|
return "ApiKey";
|
||||||
|
|
||||||
|
// Regular user traffic/auth
|
||||||
|
return "Session";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.AddJwtBearer("ApiKey", null, options =>
|
||||||
{
|
{
|
||||||
options.TokenValidationParameters = new()
|
options.TokenValidationParameters = new()
|
||||||
{
|
{
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||||
Configuration.Authentication.Secret
|
configuration.Authentication.Secret
|
||||||
)),
|
)),
|
||||||
ValidateIssuerSigningKey = true,
|
ValidateIssuerSigningKey = true,
|
||||||
ValidateLifetime = true,
|
ValidateLifetime = true,
|
||||||
ClockSkew = TimeSpan.Zero,
|
ClockSkew = TimeSpan.Zero,
|
||||||
ValidateAudience = true,
|
ValidateAudience = true,
|
||||||
ValidAudience = Configuration.PublicUrl,
|
ValidAudience = configuration.PublicUrl,
|
||||||
ValidateIssuer = true,
|
ValidateIssuer = true,
|
||||||
ValidIssuer = Configuration.PublicUrl
|
ValidIssuer = configuration.PublicUrl
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddJwtBearerInvalidation("coreAuthentication");
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IJwtInvalidateHandler, UserAuthInvalidation>();
|
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddAuthorization();
|
options.Events = new JwtBearerEvents()
|
||||||
|
{
|
||||||
|
OnTokenValidated = async context =>
|
||||||
|
{
|
||||||
|
var apiKeyAuthService = context
|
||||||
|
.HttpContext
|
||||||
|
.RequestServices
|
||||||
|
.GetRequiredService<ApiKeyAuthService>();
|
||||||
|
|
||||||
|
var result = await apiKeyAuthService.ValidateAsync(context.Principal);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
context.Fail("API key has been deleted");
|
||||||
|
},
|
||||||
|
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var accessToken = context.Request.Query["access_token"];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(accessToken))
|
||||||
|
context.Token = accessToken;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.AddCookie("Session", null, options =>
|
||||||
|
{
|
||||||
|
options.ExpireTimeSpan = TimeSpan.FromDays(configuration.Authentication.Sessions.ExpiresIn);
|
||||||
|
|
||||||
|
options.Cookie = new CookieBuilder()
|
||||||
|
{
|
||||||
|
Name = configuration.Authentication.Sessions.CookieName,
|
||||||
|
Path = "/",
|
||||||
|
IsEssential = true,
|
||||||
|
SecurePolicy = CookieSecurePolicy.SameAsRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
// As redirects won't work in our spa which uses API calls
|
||||||
|
// we need to customize the responses when certain actions happen
|
||||||
|
options.Events.OnRedirectToLogin = async context =>
|
||||||
|
{
|
||||||
|
await Results.Problem(
|
||||||
|
title: "Unauthenticated",
|
||||||
|
detail: "You need to authenticate yourself to use this endpoint",
|
||||||
|
statusCode: 401
|
||||||
|
)
|
||||||
|
.ExecuteAsync(context.HttpContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Events.OnRedirectToAccessDenied = async context =>
|
||||||
|
{
|
||||||
|
await Results.Problem(
|
||||||
|
title: "Permission denied",
|
||||||
|
detail: "You are missing the required permissions to access this endpoint",
|
||||||
|
statusCode: 403
|
||||||
|
)
|
||||||
|
.ExecuteAsync(context.HttpContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Events.OnSigningIn = async context =>
|
||||||
|
{
|
||||||
|
var userSyncService = context
|
||||||
|
.HttpContext
|
||||||
|
.RequestServices
|
||||||
|
.GetRequiredService<UserAuthService>();
|
||||||
|
|
||||||
|
var result = await userSyncService.SyncAsync(context.Principal);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
context.Principal = new();
|
||||||
|
else
|
||||||
|
context.Properties.IsPersistent = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Events.OnValidatePrincipal = async context =>
|
||||||
|
{
|
||||||
|
var userSyncService = context
|
||||||
|
.HttpContext
|
||||||
|
.RequestServices
|
||||||
|
.GetRequiredService<UserAuthService>();
|
||||||
|
|
||||||
|
var result = await userSyncService.ValidateAsync(context.Principal);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
context.RejectPrincipal();
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.AddScheme<LocalAuthOptions, LocalAuthHandler>(LocalAuthConstants.AuthenticationScheme, "Local Auth", options =>
|
||||||
|
{
|
||||||
|
options.ForwardAuthenticate = "Session";
|
||||||
|
options.ForwardSignIn = "Session";
|
||||||
|
options.ForwardSignOut = "Session";
|
||||||
|
|
||||||
|
options.SignInScheme = "Session";
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddAuthorizationPermissions(options =>
|
builder.Services.AddAuthorizationPermissions(options =>
|
||||||
{
|
{
|
||||||
options.ClaimName = "permissions";
|
options.ClaimName = "Permissions";
|
||||||
options.Prefix = "permissions:";
|
options.Prefix = "permissions:";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<UserAuthService>();
|
||||||
|
builder.Services.AddScoped<ApiKeyAuthService>();
|
||||||
|
|
||||||
// Add local oauth2 provider if enabled
|
// Setup data protection storage within storage folder
|
||||||
if (Configuration.Authentication.EnableLocalOAuth2)
|
// so its persists in containers
|
||||||
WebApplicationBuilder.Services.AddScoped<IOAuth2Provider, LocalOAuth2Provider>();
|
var dpKeyPath = Path.Combine("storage", "dataProtectionKeys");
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddScoped<UserDeletionService>();
|
Directory.CreateDirectory(dpKeyPath);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
builder.Services
|
||||||
|
.AddDataProtection()
|
||||||
|
.PersistKeysToFileSystem(
|
||||||
|
new DirectoryInfo(dpKeyPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.Services.AddScoped<UserDeletionService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task UseAuth()
|
private static void UseAuth(this WebApplication application)
|
||||||
{
|
{
|
||||||
WebApplication.UseAuthentication();
|
application.UseAuthentication();
|
||||||
|
application.UseAuthorization();
|
||||||
WebApplication.UseAuthorization();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,63 +1,62 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MoonCore.Extended.Extensions;
|
using MoonCore.Extended.Extensions;
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Plugins;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task RegisterBase()
|
private static void AddBase(this WebApplicationBuilder builder, IPluginStartup[] startups)
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services.AutoAddServices<Startup>();
|
builder.Services.AutoAddServices<IAssemblyMarker>();
|
||||||
WebApplicationBuilder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddApiExceptionHandler();
|
builder.Services.AddApiExceptionHandler();
|
||||||
|
|
||||||
// Add pre-existing services
|
|
||||||
WebApplicationBuilder.Services.AddSingleton(Configuration);
|
|
||||||
|
|
||||||
// Configure controllers
|
// Configure controllers
|
||||||
var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
|
var mvcBuilder = builder.Services.AddControllers();
|
||||||
|
|
||||||
// Add plugin assemblies as application parts
|
// Add plugin assemblies as application parts
|
||||||
foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct())
|
foreach (var pluginStartup in startups.Select(x => x.GetType().Assembly).Distinct())
|
||||||
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
|
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task UseBase()
|
private static void UseBase(this WebApplication application)
|
||||||
{
|
{
|
||||||
WebApplication.UseRouting();
|
application.UseRouting();
|
||||||
WebApplication.UseExceptionHandler();
|
application.UseExceptionHandler();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task MapBase()
|
private static void MapBase(this WebApplication application)
|
||||||
{
|
{
|
||||||
WebApplication.MapControllers();
|
application.MapControllers();
|
||||||
|
|
||||||
if (Configuration.Frontend.EnableHosting)
|
// Frontend
|
||||||
WebApplication.MapFallbackToController("Index", "Frontend");
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
application.Configuration.Bind(configuration);
|
||||||
return Task.CompletedTask;
|
|
||||||
|
if (configuration.Frontend.EnableHosting)
|
||||||
|
application.MapFallbackToController("Index", "Frontend");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ConfigureKestrel()
|
private static void ConfigureKestrel(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.WebHost.ConfigureKestrel(kestrelOptions =>
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
builder.Configuration.Bind(configuration);
|
||||||
|
|
||||||
|
builder.WebHost.ConfigureKestrel(kestrelOptions =>
|
||||||
{
|
{
|
||||||
var maxUploadInBytes = ByteConverter
|
var maxUploadInBytes = ByteConverter
|
||||||
.FromMegaBytes(Configuration.Kestrel.UploadLimit)
|
.FromMegaBytes(configuration.Kestrel.UploadLimit)
|
||||||
.Bytes;
|
.Bytes;
|
||||||
|
|
||||||
kestrelOptions.Limits.MaxRequestBodySize = maxUploadInBytes;
|
kestrelOptions.Limits.MaxRequestBodySize = maxUploadInBytes;
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MoonCore.EnvConfiguration;
|
using MoonCore.EnvConfiguration;
|
||||||
@@ -6,30 +7,23 @@ using Moonlight.ApiServer.Configuration;
|
|||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private async Task SetupAppConfiguration()
|
private static void AddConfiguration(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
var configPath = Path.Combine("storage", "config.yml");
|
// Yaml
|
||||||
|
var yamlPath = Path.Combine("storage", "config.yml");
|
||||||
|
|
||||||
await YamlDefaultGenerator.Generate<AppConfiguration>(configPath);
|
YamlDefaultGenerator.GenerateAsync<AppConfiguration>(yamlPath).Wait();
|
||||||
|
|
||||||
// Configure configuration (wow)
|
builder.Configuration.AddYamlFile(yamlPath);
|
||||||
var configurationBuilder = new ConfigurationBuilder();
|
|
||||||
|
|
||||||
configurationBuilder.AddYamlFile(configPath);
|
|
||||||
configurationBuilder.AddEnvironmentVariables(prefix: "MOONLIGHT_", separator: "_");
|
|
||||||
|
|
||||||
var configurationRoot = configurationBuilder.Build();
|
|
||||||
|
|
||||||
// Retrieve configuration
|
// Env
|
||||||
Configuration = AppConfiguration.CreateEmpty();
|
builder.Configuration.AddEnvironmentVariables(prefix: "MOONLIGHT_", separator: "_");
|
||||||
configurationRoot.Bind(Configuration);
|
|
||||||
}
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
builder.Configuration.Bind(configuration);
|
||||||
private Task RegisterAppConfiguration()
|
|
||||||
{
|
builder.Services.AddSingleton(configuration);
|
||||||
WebApplicationBuilder.Services.AddSingleton(Configuration);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,17 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extended.Extensions;
|
using MoonCore.Extended.Extensions;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task RegisterDatabase()
|
private static void AddDatabase(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services.AddDatabaseMappings();
|
builder.Services.AddDbAutoMigrations();
|
||||||
WebApplicationBuilder.Services.AddServiceCollectionAccessor();
|
builder.Services.AddDatabaseMappings();
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddScoped(typeof(DatabaseRepository<>));
|
builder.Services.AddScoped(typeof(DatabaseRepository<>));
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PrepareDatabase()
|
|
||||||
{
|
|
||||||
await WebApplication.Services.EnsureDatabaseMigrated();
|
|
||||||
|
|
||||||
WebApplication.Services.GenerateDatabaseMappings();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Hangfire.EntityFrameworkCore;
|
using Hangfire.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -7,11 +8,11 @@ using Moonlight.ApiServer.Database;
|
|||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task RegisterHangfire()
|
private static void AddMoonlightHangfire(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services.AddHangfire((provider, configuration) =>
|
builder.Services.AddHangfire((provider, configuration) =>
|
||||||
{
|
{
|
||||||
configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
|
configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
|
||||||
configuration.UseSimpleAssemblyNameTypeSerializer();
|
configuration.UseSimpleAssemblyNameTypeSerializer();
|
||||||
@@ -23,26 +24,22 @@ public partial class Startup
|
|||||||
}, new EFCoreStorageOptions());
|
}, new EFCoreStorageOptions());
|
||||||
});
|
});
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddHangfireServer();
|
builder.Services.AddHangfireServer();
|
||||||
|
|
||||||
WebApplicationBuilder.Logging.AddFilter(
|
builder.Logging.AddFilter(
|
||||||
"Hangfire.Server.BackgroundServerProcess",
|
"Hangfire.Server.BackgroundServerProcess",
|
||||||
LogLevel.Warning
|
LogLevel.Warning
|
||||||
);
|
);
|
||||||
|
|
||||||
WebApplicationBuilder.Logging.AddFilter(
|
builder.Logging.AddFilter(
|
||||||
"Hangfire.BackgroundJobServer",
|
"Hangfire.BackgroundJobServer",
|
||||||
LogLevel.Warning
|
LogLevel.Warning
|
||||||
);
|
);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task UseHangfire()
|
private static void UseMoonlightHangfire(this WebApplication application)
|
||||||
{
|
{
|
||||||
if (WebApplication.Environment.IsDevelopment())
|
if (application.Environment.IsDevelopment())
|
||||||
WebApplication.UseHangfireDashboard();
|
application.UseHangfireDashboard();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,61 +1,55 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MoonCore.Logging;
|
using MoonCore.Logging;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task SetupLogging()
|
private static void AddLogging(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
var loggerFactory = new LoggerFactory();
|
// Logging providers
|
||||||
loggerFactory.AddAnsiConsole();
|
builder.Logging.ClearProviders();
|
||||||
|
|
||||||
Logger = loggerFactory.CreateLogger<Startup>();
|
builder.Logging.AddAnsiConsole();
|
||||||
|
builder.Logging.AddFile(Path.Combine("storage", "logs", "moonlight.log"));
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RegisterLogging()
|
|
||||||
{
|
|
||||||
// Configure application logging
|
|
||||||
WebApplicationBuilder.Logging.ClearProviders();
|
|
||||||
WebApplicationBuilder.Logging.AddAnsiConsole();
|
|
||||||
WebApplicationBuilder.Logging.AddFile(Path.Combine("storage", "logs", "moonlight.log"));
|
|
||||||
|
|
||||||
// Logging levels
|
// Logging levels
|
||||||
var logConfigPath = Path.Combine("storage", "logConfig.json");
|
var logConfigPath = Path.Combine("storage", "logConfig.json");
|
||||||
|
|
||||||
// Ensure logging config, add a default one is missing
|
// Ensure default log levels exist
|
||||||
if (!File.Exists(logConfigPath))
|
if (!File.Exists(logConfigPath))
|
||||||
{
|
{
|
||||||
var defaultLogLevels = new Dictionary<string, string>
|
var defaultLogLevels = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "Default", "Information" },
|
{ "Default", "Information" },
|
||||||
{ "Microsoft.AspNetCore", "Warning" },
|
{ "Microsoft.AspNetCore", "Warning" },
|
||||||
{ "System.Net.Http.HttpClient", "Warning" }
|
{ "System.Net.Http.HttpClient", "Warning" },
|
||||||
|
{ "Moonlight.ApiServer.Implementations.LocalAuth.LocalAuthHandler", "Warning" }
|
||||||
};
|
};
|
||||||
|
|
||||||
var logLevelsJson = JsonSerializer.Serialize(defaultLogLevels);
|
var logLevelsJson = JsonSerializer.Serialize(defaultLogLevels);
|
||||||
await File.WriteAllTextAsync(logConfigPath, logLevelsJson);
|
File.WriteAllText(logConfigPath, logLevelsJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add logging configuration
|
// Read log levels
|
||||||
var logLevels = JsonSerializer.Deserialize<Dictionary<string, string>>(
|
var logLevels = JsonSerializer.Deserialize<Dictionary<string, string>>(
|
||||||
await File.ReadAllTextAsync(logConfigPath)
|
File.ReadAllText(logConfigPath)
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
|
// Apply configured log levels
|
||||||
foreach (var level in logLevels)
|
foreach (var level in logLevels)
|
||||||
WebApplicationBuilder.Logging.AddFilter(level.Key, Enum.Parse<LogLevel>(level.Value));
|
builder.Logging.AddFilter(level.Key, Enum.Parse<LogLevel>(level.Value));
|
||||||
|
|
||||||
// Mute exception handler middleware
|
// Mute exception handler middleware
|
||||||
// https://github.com/dotnet/aspnetcore/issues/19740
|
// https://github.com/dotnet/aspnetcore/issues/19740
|
||||||
WebApplicationBuilder.Logging.AddFilter(
|
builder.Logging.AddFilter(
|
||||||
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
|
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
|
||||||
LogLevel.Critical
|
LogLevel.Critical
|
||||||
);
|
);
|
||||||
|
|
||||||
WebApplicationBuilder.Logging.AddFilter(
|
builder.Logging.AddFilter(
|
||||||
"Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware",
|
"Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware",
|
||||||
LogLevel.Critical
|
LogLevel.Critical
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task PrintVersion()
|
private static void 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);
|
||||||
@@ -21,23 +23,22 @@ public partial class Startup
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task CreateStorage()
|
private static void CreateStorageAsync()
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory("storage");
|
Directory.CreateDirectory("storage");
|
||||||
Directory.CreateDirectory(Path.Combine("storage", "logs"));
|
Directory.CreateDirectory(Path.Combine("storage", "logs"));
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RegisterCors()
|
|
||||||
{
|
|
||||||
var allowedOrigins = Configuration.Kestrel.AllowedOrigins;
|
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddCors(options =>
|
private static void AddMoonlightCors(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
builder.Configuration.Bind(configuration);
|
||||||
|
|
||||||
|
var allowedOrigins = configuration.Kestrel.AllowedOrigins;
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
var cors = new CorsPolicyBuilder();
|
var cors = new CorsPolicyBuilder();
|
||||||
|
|
||||||
@@ -60,14 +61,10 @@ public partial class Startup
|
|||||||
cors.Build()
|
cors.Build()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task UseCors()
|
private static void UseMoonlightCors(this WebApplication application)
|
||||||
{
|
{
|
||||||
WebApplication.UseCors();
|
application.UseCors();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,87 +1,25 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MoonCore.Logging;
|
|
||||||
using Moonlight.ApiServer.Plugins;
|
using Moonlight.ApiServer.Plugins;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private IServiceProvider PluginLoadServiceProvider;
|
private static void AddPlugins(this WebApplicationBuilder builder, IPluginStartup[] startups)
|
||||||
private IPluginStartup[] PluginStartups;
|
|
||||||
|
|
||||||
private Task InitializePlugins()
|
|
||||||
{
|
{
|
||||||
// Create service provider for starting up
|
foreach (var startup in startups)
|
||||||
var serviceCollection = new ServiceCollection();
|
startup.AddPlugin(builder);
|
||||||
|
|
||||||
serviceCollection.AddSingleton(Configuration);
|
|
||||||
|
|
||||||
serviceCollection.AddLogging(builder =>
|
|
||||||
{
|
|
||||||
builder.ClearProviders();
|
|
||||||
builder.AddAnsiConsole();
|
|
||||||
});
|
|
||||||
|
|
||||||
PluginLoadServiceProvider = serviceCollection.BuildServiceProvider();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HookPluginBuild()
|
private static void UsePlugins(this WebApplication application, IPluginStartup[] startups)
|
||||||
{
|
{
|
||||||
foreach (var pluginAppStartup in PluginStartups)
|
foreach (var startup in startups)
|
||||||
{
|
startup.UsePlugin(application);
|
||||||
try
|
|
||||||
{
|
|
||||||
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError(
|
|
||||||
"An error occured while processing 'BuildApp' for '{name}': {e}",
|
|
||||||
pluginAppStartup.GetType().FullName,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HookPluginConfigure()
|
private static void MapPlugins(this WebApplication application, IPluginStartup[] startups)
|
||||||
{
|
{
|
||||||
foreach (var pluginAppStartup in PluginStartups)
|
foreach (var startup in startups)
|
||||||
{
|
startup.MapPlugin(application);
|
||||||
try
|
|
||||||
{
|
|
||||||
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError(
|
|
||||||
"An error occured while processing 'ConfigureApp' for '{name}': {e}",
|
|
||||||
pluginAppStartup.GetType().FullName,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HookPluginEndpoints()
|
|
||||||
{
|
|
||||||
foreach (var pluginEndpointStartup in PluginStartups)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError(
|
|
||||||
"An error occured while processing 'ConfigureEndpoints' for '{name}': {e}",
|
|
||||||
pluginEndpointStartup.GetType().FullName,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
26
Moonlight.ApiServer/Startup/Startup.SignalR.cs
Normal file
26
Moonlight.ApiServer/Startup/Startup.SignalR.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Http.Hubs;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
|
public static partial class Startup
|
||||||
|
{
|
||||||
|
private static void AddMoonlightSignalR(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
var configuration = AppConfiguration.CreateEmpty();
|
||||||
|
builder.Configuration.Bind(configuration);
|
||||||
|
|
||||||
|
var signalRBuilder = builder.Services.AddSignalR();
|
||||||
|
|
||||||
|
if (configuration.SignalR.UseRedis)
|
||||||
|
signalRBuilder.AddStackExchangeRedis(configuration.SignalR.RedisConnectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MapMoonlightSignalR(this WebApplication application)
|
||||||
|
{
|
||||||
|
application.MapHub<DiagnoseHub>("/api/admin/system/diagnose/ws");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +1,42 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Moonlight.ApiServer.Configuration;
|
|
||||||
using Moonlight.ApiServer.Plugins;
|
using Moonlight.ApiServer.Plugins;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Startup;
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private string[] Args;
|
public static void AddMoonlight(this WebApplicationBuilder builder, IPluginStartup[] startups)
|
||||||
|
|
||||||
// Logger
|
|
||||||
public ILogger<Startup> Logger { get; private set; }
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
public AppConfiguration Configuration { get; private set; }
|
|
||||||
|
|
||||||
// WebApplication Stuff
|
|
||||||
public WebApplication WebApplication { get; private set; }
|
|
||||||
public WebApplicationBuilder WebApplicationBuilder { get; private set; }
|
|
||||||
|
|
||||||
public Task Initialize(string[] args, IPluginStartup[]? plugins = null)
|
|
||||||
{
|
{
|
||||||
Args = args;
|
PrintVersionAsync();
|
||||||
PluginStartups = plugins ?? [];
|
CreateStorageAsync();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
builder.AddConfiguration();
|
||||||
|
builder.AddLogging();
|
||||||
|
|
||||||
|
builder.ConfigureKestrel();
|
||||||
|
builder.AddBase(startups);
|
||||||
|
builder.AddDatabase();
|
||||||
|
builder.AddAuth();
|
||||||
|
builder.AddMoonlightCors();
|
||||||
|
builder.AddMoonlightHangfire();
|
||||||
|
builder.AddMoonlightSignalR();
|
||||||
|
|
||||||
|
builder.AddPlugins(startups);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddMoonlight(WebApplicationBuilder builder)
|
public static void UseMoonlight(this WebApplication application, IPluginStartup[] startups)
|
||||||
{
|
{
|
||||||
WebApplicationBuilder = builder;
|
application.UseBase();
|
||||||
|
application.UseMoonlightCors();
|
||||||
await PrintVersion();
|
application.UseAuth();
|
||||||
|
application.UseMoonlightHangfire();
|
||||||
await CreateStorage();
|
application.UsePlugins(startups);
|
||||||
await SetupAppConfiguration();
|
|
||||||
await SetupLogging();
|
|
||||||
await InitializePlugins();
|
|
||||||
|
|
||||||
await ConfigureKestrel();
|
|
||||||
await RegisterAppConfiguration();
|
|
||||||
await RegisterLogging();
|
|
||||||
await RegisterBase();
|
|
||||||
await RegisterDatabase();
|
|
||||||
await RegisterAuth();
|
|
||||||
await RegisterCors();
|
|
||||||
await RegisterHangfire();
|
|
||||||
await HookPluginBuild();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddMoonlight(WebApplication application)
|
public static void MapMoonlight(this WebApplication application, IPluginStartup[] startups)
|
||||||
{
|
{
|
||||||
WebApplication = application;
|
application.MapBase();
|
||||||
|
application.MapMoonlightSignalR();
|
||||||
await PrepareDatabase();
|
application.MapPlugins(startups);
|
||||||
|
|
||||||
await UseCors();
|
|
||||||
await UseBase();
|
|
||||||
await UseAuth();
|
|
||||||
await UseHangfire();
|
|
||||||
await HookPluginConfigure();
|
|
||||||
|
|
||||||
await MapBase();
|
|
||||||
await HookPluginEndpoints();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,15 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.8" />
|
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.9" />
|
||||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2" />
|
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.3" />
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.9" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\css\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="Plugins.props" />
|
<Import Project="Plugins.props" />
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ using Moonlight.Client.Startup;
|
|||||||
var pluginLoader = new PluginLoader();
|
var pluginLoader = new PluginLoader();
|
||||||
pluginLoader.Initialize();
|
pluginLoader.Initialize();
|
||||||
|
|
||||||
var startup = new Startup();
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
await startup.Initialize(pluginLoader.Instances);
|
builder.AddMoonlight(pluginLoader.Instances);
|
||||||
|
|
||||||
var wasmHostBuilder = WebAssemblyHostBuilder.CreateDefault(args);
|
var app = builder.Build();
|
||||||
|
|
||||||
await startup.AddMoonlight(wasmHostBuilder);
|
app.ConfigureMoonlight(pluginLoader.Instances);
|
||||||
|
|
||||||
var wasmApp = wasmHostBuilder.Build();
|
await app.RunAsync();
|
||||||
|
|
||||||
await startup.AddMoonlight(wasmApp);
|
|
||||||
|
|
||||||
await wasmApp.RunAsync();
|
|
||||||
@@ -9,14 +9,10 @@
|
|||||||
@theme {
|
@theme {
|
||||||
--font-inter: "Inter", var(--font-sans);
|
--font-inter: "Inter", var(--font-sans);
|
||||||
--font-scp: "Source Code Pro", var(--font-mono);
|
--font-scp: "Source Code Pro", var(--font-mono);
|
||||||
|
|
||||||
--color-background: var(--mooncore-color-background);
|
|
||||||
--color-base-150: var(--mooncore-color-base-150);
|
|
||||||
--color-base-250: var(--mooncore-color-base-250);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@plugin "flyonui" {
|
@plugin "flyonui" {
|
||||||
themes: mooncore --default;
|
themes: moonlight --default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@source "./node_modules/flyonui/dist/index.js";
|
@source "./node_modules/flyonui/dist/index.js";
|
||||||
@@ -45,73 +41,50 @@
|
|||||||
width: var(--blazor-load-percentage, 0%);
|
width: var(--blazor-load-percentage, 0%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@plugin "flyonui/theme" {
|
|
||||||
name: "mooncore";
|
|
||||||
default: true;
|
|
||||||
prefersdark: true;
|
|
||||||
color-scheme: "dark";
|
|
||||||
--color-base-100: var(--mooncore-color-base-100);
|
|
||||||
--color-base-200: var(--mooncore-color-base-200);
|
|
||||||
--color-base-300: var(--mooncore-color-base-300);
|
|
||||||
--color-base-content: var(--mooncore-color-base-content);
|
|
||||||
--color-primary: var(--mooncore-color-primary);
|
|
||||||
--color-primary-content: var(--mooncore-color-primary-content);
|
|
||||||
--color-secondary: var(--mooncore-color-secondary);
|
|
||||||
--color-secondary-content: var(--mooncore-color-secondary-content);
|
|
||||||
--color-accent: var(--mooncore-color-accent);
|
|
||||||
--color-accent-content: var(--mooncore-color-accent-content);
|
|
||||||
--color-neutral: var(--mooncore-color-neutral);
|
|
||||||
--color-neutral-content: var(--mooncore-color-neutral-content);
|
|
||||||
--color-info: var(--mooncore-color-info);
|
|
||||||
--color-info-content: var(--mooncore-color-info-content);
|
|
||||||
--color-success: var(--mooncore-color-success);
|
|
||||||
--color-success-content: var(--mooncore-color-success-content);
|
|
||||||
--color-warning: var(--mooncore-color-warning);
|
|
||||||
--color-warning-content: var(--mooncore-color-warning-content);
|
|
||||||
--color-error: var(--mooncore-color-error);
|
|
||||||
--color-error-content: var(--mooncore-color-error-content);
|
|
||||||
--radius-selector: var(--mooncore-radius-selector);
|
|
||||||
--radius-field: var(--mooncore-radius-field);
|
|
||||||
--radius-box: var(--mooncore-radius-box);
|
|
||||||
--size-selector: var(--mooncore-size-selector);
|
|
||||||
--size-field: var(--mooncore-size-field);
|
|
||||||
--border: var(--mooncore-border);
|
|
||||||
--depth: var(--mooncore-depth);
|
|
||||||
--noise: var(--mooncore-noise);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.btn {
|
.btn {
|
||||||
@apply text-sm font-medium inline-flex items-center justify-center;
|
@apply text-sm font-medium inline-flex items-center justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
@apply border-base-content/30 bg-base-100;
|
@apply border-base-content/30 bg-base-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@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-100/50;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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-100/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 {
|
.advance-select-menu {
|
||||||
@apply bg-base-150;
|
@apply !border-base-content/20 border-2 ring-0! outline-0! bg-base-200/50 !pt-0 !px-0 !py-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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,35 @@
|
|||||||
@theme {
|
@plugin "flyonui/theme" {
|
||||||
--mooncore-color-background: #0c0f18;
|
name: "moonlight";
|
||||||
--mooncore-color-base-100: #1e2b47;
|
prefersdark: true;
|
||||||
--mooncore-color-base-150: #1a2640;
|
color-scheme: "dark";
|
||||||
--mooncore-color-base-200: #101a2e;
|
|
||||||
--mooncore-color-base-250: #0f1729;
|
--color-base-100: #192032;
|
||||||
--mooncore-color-base-300: #0c1221;
|
--color-base-200: #101522;
|
||||||
--mooncore-color-base-content: #dde5f5;
|
--color-base-300: #070a14;
|
||||||
--mooncore-color-primary: oklch(.511 .262 276.966);
|
|
||||||
--mooncore-color-primary-content: #dde5f5;
|
--color-base-content: #dde5f5;
|
||||||
--mooncore-color-secondary: oklch(37% 0.034 259.733);
|
--color-primary: oklch(.511 .262 276.966);
|
||||||
--mooncore-color-secondary-content: #dde5f5;
|
--color-primary-content: #dde5f5;
|
||||||
--mooncore-color-accent: oklch(.627 .265 303.9);
|
--color-secondary: oklch(37% 0.034 259.733);
|
||||||
--mooncore-color-accent-content: #dde5f5;
|
--color-secondary-content: #dde5f5;
|
||||||
--mooncore-color-neutral: #dde5f5;
|
--color-accent: oklch(49.1% 0.27 292.581);
|
||||||
--mooncore-color-neutral-content: oklch(14% 0.005 285.823);
|
--color-accent-content: #dde5f5;
|
||||||
--mooncore-color-info: oklch(.546 .245 262.881);
|
--color-neutral: oklch(27% 0.041 260.031);
|
||||||
--mooncore-color-info-content: #dde5f5;
|
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||||
--mooncore-color-success: oklch(.627 .194 149.214);
|
--color-info: oklch(.546 .245 262.881);
|
||||||
--mooncore-color-success-content: #dde5f5;
|
--color-info-content: #dde5f5;
|
||||||
--mooncore-color-warning: oklch(.828 .189 84.429);
|
--color-success: oklch(.627 .194 149.214);
|
||||||
--mooncore-color-warning-content: #dde5f5;
|
--color-success-content: #dde5f5;
|
||||||
--mooncore-color-error: oklch(.586 .253 17.585);
|
--color-warning: oklch(.828 .189 84.429);
|
||||||
--mooncore-color-error-content: #dde5f5;
|
--color-warning-content: #dde5f5;
|
||||||
--mooncore-radius-selector: 0.25rem;
|
--color-error: oklch(.586 .253 17.585);
|
||||||
--mooncore-radius-field: 0.5rem;
|
--color-error-content: #dde5f5;
|
||||||
--mooncore-radius-box: 0.5rem;
|
--radius-selector: 0.25rem;
|
||||||
--mooncore-size-selector: 0.25rem;
|
--radius-field: 0.5rem;
|
||||||
--mooncore-size-field: 0.25rem;
|
--radius-box: 0.5rem;
|
||||||
--mooncore-border: 1px;
|
--size-selector: 0.25rem;
|
||||||
--mooncore-depth: 0;
|
--size-field: 0.25rem;
|
||||||
--mooncore-noise: 0;
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
}
|
}
|
||||||
3
Moonlight.Client/IAssemblyMarker.cs
Normal file
3
Moonlight.Client/IAssemblyMarker.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Moonlight.Client;
|
||||||
|
|
||||||
|
public interface IAssemblyMarker;
|
||||||
@@ -7,14 +7,14 @@ namespace Moonlight.Client.Implementations;
|
|||||||
|
|
||||||
public class CoreStartup : IPluginStartup
|
public class CoreStartup : IPluginStartup
|
||||||
{
|
{
|
||||||
public Task BuildApplication(IServiceProvider serviceProvider, WebAssemblyHostBuilder builder)
|
public void AddPlugin(WebAssemblyHostBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Services.AddSingleton<ISidebarItemProvider, DefaultSidebarItemProvider>();
|
builder.Services.AddSingleton<ISidebarItemProvider, DefaultSidebarItemProvider>();
|
||||||
builder.Services.AddSingleton<IOverviewElementProvider, DefaultOverviewElementProvider>();
|
builder.Services.AddSingleton<IOverviewElementProvider, DefaultOverviewElementProvider>();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ConfigureApplication(IServiceProvider serviceProvider, WebAssemblyHost app)
|
public void ConfigurePlugin(WebAssemblyHost app)
|
||||||
=> Task.CompletedTask;
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
19
Moonlight.Client/Implementations/LogErrorFilter.cs
Normal file
19
Moonlight.Client/Implementations/LogErrorFilter.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using MoonCore.Blazor.FlyonUi.Exceptions;
|
||||||
|
|
||||||
|
namespace Moonlight.Client.Implementations;
|
||||||
|
|
||||||
|
public class LogErrorFilter : IGlobalErrorFilter
|
||||||
|
{
|
||||||
|
private readonly ILogger<LogErrorFilter> Logger;
|
||||||
|
|
||||||
|
public LogErrorFilter(ILogger<LogErrorFilter> logger)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> HandleExceptionAsync(Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Global error processed");
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
using MoonCore.Blazor.FlyonUi.Files;
|
using MoonCore.Blazor.FlyonUi.Files;
|
||||||
using MoonCore.Blazor.FlyonUi.Files.Manager;
|
using MoonCore.Blazor.FlyonUi.Files.Manager;
|
||||||
|
using MoonCore.Blazor.FlyonUi.Files.Manager.Abstractions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
||||||
|
|
||||||
namespace Moonlight.Client.Implementations;
|
namespace Moonlight.Client.Implementations;
|
||||||
|
|
||||||
public class SystemFsAccess : IFsAccess
|
public class SystemFsAccess : IFsAccess, ICombineAccess, IArchiveAccess, IDownloadUrlAccess
|
||||||
{
|
{
|
||||||
private readonly HttpApiClient ApiClient;
|
private readonly HttpApiClient ApiClient;
|
||||||
|
|
||||||
private const string BaseApiUrl = "api/admin/system/files";
|
private const string BaseApiUrl = "api/admin/system/files";
|
||||||
|
|
||||||
public SystemFsAccess(HttpApiClient apiClient)
|
public SystemFsAccess(HttpApiClient apiClient)
|
||||||
@@ -16,21 +18,21 @@ public class SystemFsAccess : IFsAccess
|
|||||||
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}"
|
||||||
@@ -46,43 +48,107 @@ public class SystemFsAccess : IFsAccess
|
|||||||
}).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 Task Read(string path, Func<Stream, Task> onHandleData)
|
public async Task ReadAsync(string path, Func<Stream, Task> onHandleData)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
await using var stream = await ApiClient.GetStream(
|
||||||
|
$"{BaseApiUrl}/download?path={path}"
|
||||||
|
);
|
||||||
|
|
||||||
|
await onHandleData.Invoke(stream);
|
||||||
|
|
||||||
|
stream.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Write(string path, Stream dataStream)
|
public async Task WriteAsync(string path, Stream dataStream)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
using var multiPartForm = new MultipartFormDataContent();
|
||||||
|
|
||||||
|
multiPartForm.Add(new StreamContent(dataStream), "file", "file");
|
||||||
|
|
||||||
|
await ApiClient.Post(
|
||||||
|
$"{BaseApiUrl}/upload?path={path}",
|
||||||
|
multiPartForm
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 UploadChunk(string path, int chunkId, long chunkSize, long totalSize, byte[] data)
|
public async Task CombineAsync(string destination, string[] files)
|
||||||
{
|
{
|
||||||
using var formContent = new MultipartFormDataContent();
|
|
||||||
formContent.Add(new ByteArrayContent(data), "file", "file");
|
|
||||||
|
|
||||||
await ApiClient.Post(
|
await ApiClient.Post(
|
||||||
$"{BaseApiUrl}/upload?path={path}&chunkId={chunkId}&chunkSize={chunkSize}&totalSize={totalSize}",
|
$"{BaseApiUrl}/combine",
|
||||||
formContent
|
new CombineRequest()
|
||||||
|
{
|
||||||
|
Destination = destination,
|
||||||
|
Files = files
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<byte[]> DownloadChunk(string path, int chunkId, long chunkSize)
|
public ArchiveFormat[] ArchiveFormats { get; } =
|
||||||
|
[
|
||||||
|
new("zip", ["zip"], "Zip Archive"),
|
||||||
|
new("tar.gz", ["tar.gz"], "Tar.gz Archive")
|
||||||
|
];
|
||||||
|
|
||||||
|
public async Task ArchiveAsync(
|
||||||
|
string destination,
|
||||||
|
ArchiveFormat format,
|
||||||
|
string root,
|
||||||
|
FsEntry[] files,
|
||||||
|
Func<string, Task>? onProgress = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
await ApiClient.Post($"{BaseApiUrl}/compress", new CompressRequest()
|
||||||
|
{
|
||||||
|
Destination = destination,
|
||||||
|
Items = files.Select(x => x.Name).ToArray(),
|
||||||
|
Root = root,
|
||||||
|
Format = format.Identifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnarchiveAsync(
|
||||||
|
string path,
|
||||||
|
ArchiveFormat format,
|
||||||
|
string destination,
|
||||||
|
Func<string, Task>? onProgress = null)
|
||||||
|
{
|
||||||
|
await ApiClient.Post(
|
||||||
|
$"{BaseApiUrl}/decompress",
|
||||||
|
new DecompressRequest()
|
||||||
|
{
|
||||||
|
Format = format.Identifier,
|
||||||
|
Destination = destination,
|
||||||
|
Path = path
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetFileUrlAsync(string path)
|
||||||
|
=> await GetDownloadUrlAsync(path);
|
||||||
|
|
||||||
|
public async Task<string> GetFolderUrlAsync(string path)
|
||||||
|
=> await GetDownloadUrlAsync(path);
|
||||||
|
|
||||||
|
private async Task<string> GetDownloadUrlAsync(string path)
|
||||||
|
{
|
||||||
|
var response = await ApiClient.PostJson<DownloadUrlResponse>(
|
||||||
|
$"{BaseApiUrl}/downloadUrl?path={path}"
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.Url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MoonCore.Blazor.FlyonUi.Exceptions;
|
||||||
|
using MoonCore.Blazor.FlyonUi.Toasts;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
|
||||||
|
namespace Moonlight.Client.Implementations;
|
||||||
|
|
||||||
|
public class UnauthenticatedErrorFilter : IGlobalErrorFilter
|
||||||
|
{
|
||||||
|
private readonly NavigationManager Navigation;
|
||||||
|
private readonly ToastService ToastService;
|
||||||
|
|
||||||
|
public UnauthenticatedErrorFilter(
|
||||||
|
NavigationManager navigation,
|
||||||
|
ToastService toastService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Navigation = navigation;
|
||||||
|
ToastService = toastService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HandleExceptionAsync(Exception ex)
|
||||||
|
{
|
||||||
|
if (ex is not HttpApiException { Status: 401 })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await ToastService.InfoAsync("Session expired", "Your session has expired. Reloading..");
|
||||||
|
|
||||||
|
Navigation.NavigateTo("/api/auth/logout", true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Moonlight.Client/Models/ThemeTransferModel.cs
Normal file
14
Moonlight.Client/Models/ThemeTransferModel.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Moonlight.Shared.Misc;
|
||||||
|
|
||||||
|
namespace Moonlight.Client.Models;
|
||||||
|
|
||||||
|
public class ThemeTransferModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Author { get; set; }
|
||||||
|
public string Version { get; set; }
|
||||||
|
public string? UpdateUrl { get; set; }
|
||||||
|
public string? DonateUrl { get; set; }
|
||||||
|
|
||||||
|
public ApplicationTheme Content { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageTags>frontend</PackageTags>
|
<PackageTags>frontend</PackageTags>
|
||||||
<PackageId>Moonlight.Client</PackageId>
|
<PackageId>Moonlight.Client</PackageId>
|
||||||
<Version>2.1.7</Version>
|
<Version>2.1.15</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>
|
||||||
@@ -21,26 +21,20 @@
|
|||||||
<CompressionEnabled>false</CompressionEnabled>
|
<CompressionEnabled>false</CompressionEnabled>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Blazor-ApexCharts" Version="6.0.0" />
|
<PackageReference Include="Blazor-ApexCharts" Version="6.0.2" />
|
||||||
<PackageReference Include="MoonCore" Version="1.9.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
|
||||||
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.9" />
|
||||||
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
|
||||||
|
<PackageReference Include="MoonCore" Version="2.0.6" />
|
||||||
|
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.3.4" />
|
||||||
|
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="storage\**\*" />
|
<Compile Remove="storage\**\*" />
|
||||||
<Content Remove="storage\**\*" />
|
<Content Remove="storage\**\*" />
|
||||||
<None Remove="storage\**\*" />
|
<None Remove="storage\**\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!--
|
|
||||||
Specify the /p:BuildPWA=true flag to build moonlight as a PWA.
|
|
||||||
This flag is by default disabled to allow nuget package generation
|
|
||||||
-->
|
|
||||||
<PropertyGroup Condition="'$(BuildPWA)' == 'true'">
|
|
||||||
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup Condition="'$(BuildPWA)' == 'true'">
|
|
||||||
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
|
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ namespace Moonlight.Client.Plugins;
|
|||||||
|
|
||||||
public interface IPluginStartup
|
public interface IPluginStartup
|
||||||
{
|
{
|
||||||
public Task BuildApplication(IServiceProvider serviceProvider, WebAssemblyHostBuilder builder);
|
public void AddPlugin(WebAssemblyHostBuilder builder);
|
||||||
public Task ConfigureApplication(IServiceProvider serviceProvider, WebAssemblyHost app);
|
public void ConfigurePlugin(WebAssemblyHost app);
|
||||||
}
|
}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using System.Web;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
using MoonCore.Blazor.FlyonUi.Auth;
|
|
||||||
using MoonCore.Blazor.Services;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.Shared.Http.Requests.Auth;
|
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
|
||||||
|
|
||||||
namespace Moonlight.Client.Services;
|
|
||||||
|
|
||||||
public class RemoteAuthStateManager : AuthenticationStateManager
|
|
||||||
{
|
|
||||||
private readonly NavigationManager NavigationManager;
|
|
||||||
private readonly HttpApiClient HttpApiClient;
|
|
||||||
private readonly LocalStorageService LocalStorageService;
|
|
||||||
private readonly ILogger<RemoteAuthStateManager> Logger;
|
|
||||||
|
|
||||||
public RemoteAuthStateManager(
|
|
||||||
HttpApiClient httpApiClient,
|
|
||||||
LocalStorageService localStorageService,
|
|
||||||
NavigationManager navigationManager,
|
|
||||||
ILogger<RemoteAuthStateManager> logger
|
|
||||||
)
|
|
||||||
{
|
|
||||||
HttpApiClient = httpApiClient;
|
|
||||||
LocalStorageService = localStorageService;
|
|
||||||
NavigationManager = navigationManager;
|
|
||||||
Logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
||||||
=> await LoadAuthState();
|
|
||||||
|
|
||||||
public override async Task HandleLogin()
|
|
||||||
{
|
|
||||||
var uri = new Uri(NavigationManager.Uri);
|
|
||||||
var codeParam = HttpUtility.ParseQueryString(uri.Query).Get("code");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(codeParam)) // If this is true, we need to log in the user
|
|
||||||
{
|
|
||||||
await StartLogin();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var loginCompleteData = await HttpApiClient.PostJson<LoginCompleteResponse>(
|
|
||||||
"api/auth/complete",
|
|
||||||
new LoginCompleteRequest()
|
|
||||||
{
|
|
||||||
Code = codeParam
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await LocalStorageService.SetString("AccessToken", loginCompleteData.AccessToken);
|
|
||||||
|
|
||||||
NavigationManager.NavigateTo("/");
|
|
||||||
NotifyAuthenticationStateChanged(LoadAuthState());
|
|
||||||
}
|
|
||||||
catch (HttpApiException e)
|
|
||||||
{
|
|
||||||
Logger.LogError("Unable to complete login: {e}", e);
|
|
||||||
|
|
||||||
await StartLogin();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task Logout()
|
|
||||||
{
|
|
||||||
if (await LocalStorageService.ContainsKey("AccessToken"))
|
|
||||||
await LocalStorageService.SetString("AccessToken", "");
|
|
||||||
|
|
||||||
NotifyAuthenticationStateChanged(LoadAuthState());
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Utilities
|
|
||||||
|
|
||||||
private async Task StartLogin()
|
|
||||||
{
|
|
||||||
var loginStartData = await HttpApiClient.GetJson<LoginStartResponse>("api/auth/start");
|
|
||||||
|
|
||||||
NavigationManager.NavigateTo(loginStartData.Url, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<AuthenticationState> LoadAuthState()
|
|
||||||
{
|
|
||||||
AuthenticationState newState;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var checkData = await HttpApiClient.GetJson<CheckResponse>("api/auth/check");
|
|
||||||
|
|
||||||
newState = new(new ClaimsPrincipal(
|
|
||||||
new ClaimsIdentity(
|
|
||||||
[
|
|
||||||
new Claim("username", checkData.Username),
|
|
||||||
new Claim("email", checkData.Email),
|
|
||||||
new Claim("permissions", string.Join(";", checkData.Permissions))
|
|
||||||
],
|
|
||||||
"RemoteAuthStateManager"
|
|
||||||
)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (HttpApiException)
|
|
||||||
{
|
|
||||||
newState = new(new ClaimsPrincipal(
|
|
||||||
new ClaimsIdentity()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
45
Moonlight.Client/Services/RemoteAuthStateProvider.cs
Normal file
45
Moonlight.Client/Services/RemoteAuthStateProvider.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
|
||||||
|
namespace Moonlight.Client.Services;
|
||||||
|
|
||||||
|
public class RemoteAuthStateProvider : AuthenticationStateProvider
|
||||||
|
{
|
||||||
|
private readonly HttpApiClient ApiClient;
|
||||||
|
|
||||||
|
public RemoteAuthStateProvider(HttpApiClient apiClient)
|
||||||
|
{
|
||||||
|
ApiClient = apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||||
|
{
|
||||||
|
ClaimsPrincipal principal;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var claims = await ApiClient.GetJson<AuthClaimResponse[]>(
|
||||||
|
"api/auth/check"
|
||||||
|
);
|
||||||
|
|
||||||
|
principal = new ClaimsPrincipal(
|
||||||
|
new ClaimsIdentity(
|
||||||
|
claims.Select(x => new Claim(x.Type, x.Value)),
|
||||||
|
"RemoteAuthentication"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (HttpApiException e)
|
||||||
|
{
|
||||||
|
if (e.Status != 401 && e.Status != 403)
|
||||||
|
throw;
|
||||||
|
|
||||||
|
principal = new ClaimsPrincipal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AuthenticationState(principal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Models;
|
|
||||||
using Moonlight.Shared.Http.Requests.Admin.Sys.Theme;
|
using Moonlight.Shared.Http.Requests.Admin.Sys.Theme;
|
||||||
using Moonlight.Shared.Http.Responses.Admin;
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
using Moonlight.Shared.Misc;
|
|
||||||
|
|
||||||
namespace Moonlight.Client.Services;
|
namespace Moonlight.Client.Services;
|
||||||
|
|
||||||
@@ -17,21 +15,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 +30,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 +38,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}"
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,27 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MoonCore.Blazor.FlyonUi.Auth;
|
using MoonCore.Blazor.FlyonUi.Exceptions;
|
||||||
using MoonCore.Permissions;
|
using MoonCore.Permissions;
|
||||||
|
using Moonlight.Client.Implementations;
|
||||||
using Moonlight.Client.Services;
|
using Moonlight.Client.Services;
|
||||||
|
|
||||||
namespace Moonlight.Client.Startup;
|
namespace Moonlight.Client.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task RegisterAuthentication()
|
private static void AddAuth(this WebAssemblyHostBuilder builder)
|
||||||
{
|
{
|
||||||
WebAssemblyHostBuilder.Services.AddAuthorizationCore();
|
builder.Services.AddAuthorizationCore();
|
||||||
WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddAuthenticationStateManager<RemoteAuthStateManager>();
|
builder.Services.AddScoped<AuthenticationStateProvider, RemoteAuthStateProvider>();
|
||||||
|
builder.Services.AddScoped<IGlobalErrorFilter, UnauthenticatedErrorFilter>();
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddAuthorizationPermissions(options =>
|
builder.Services.AddAuthorizationPermissions(options =>
|
||||||
{
|
{
|
||||||
options.ClaimName = "permissions";
|
options.ClaimName = "Permissions";
|
||||||
options.Prefix = "permissions:";
|
options.Prefix = "permissions:";
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,42 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MoonCore.Blazor.FlyonUi;
|
using MoonCore.Blazor.FlyonUi;
|
||||||
using MoonCore.Blazor.Services;
|
using MoonCore.Blazor.FlyonUi.Exceptions;
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.Client.Implementations;
|
||||||
using Moonlight.Client.Services;
|
using Moonlight.Client.Services;
|
||||||
using Moonlight.Client.UI;
|
using Moonlight.Client.UI;
|
||||||
|
|
||||||
namespace Moonlight.Client.Startup;
|
namespace Moonlight.Client.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task RegisterBase()
|
private static void AddBase(this WebAssemblyHostBuilder builder)
|
||||||
{
|
{
|
||||||
WebAssemblyHostBuilder.RootComponents.Add<App>("#app");
|
builder.RootComponents.Add<App>("#app");
|
||||||
WebAssemblyHostBuilder.RootComponents.Add<HeadOutlet>("head::after");
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddScoped(_ =>
|
builder.Services.AddScoped(_ =>
|
||||||
new HttpClient
|
new HttpClient
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri(Configuration.ApiUrl)
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddScoped(sp =>
|
builder.Services.AddScoped(sp =>
|
||||||
{
|
{
|
||||||
var httpClient = sp.GetRequiredService<HttpClient>();
|
var httpClient = sp.GetRequiredService<HttpClient>();
|
||||||
var httpApiClient = new HttpApiClient(httpClient);
|
return new HttpApiClient(httpClient);
|
||||||
|
|
||||||
var localStorageService = sp.GetRequiredService<LocalStorageService>();
|
|
||||||
|
|
||||||
httpApiClient.OnConfigureRequest += async request =>
|
|
||||||
{
|
|
||||||
var accessToken = await localStorageService.GetString("AccessToken");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(accessToken))
|
|
||||||
return;
|
|
||||||
|
|
||||||
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
|
||||||
};
|
|
||||||
|
|
||||||
return httpApiClient;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
|
builder.Services.AddFileManagerOperations();
|
||||||
WebAssemblyHostBuilder.Services.AddFileManagerOperations();
|
builder.Services.AddFlyonUiServices();
|
||||||
WebAssemblyHostBuilder.Services.AddFlyonUiServices();
|
|
||||||
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
|
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddScoped<ThemeService>();
|
builder.Services.AddScoped<ThemeService>();
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AutoAddServices<Startup>();
|
builder.Services.AutoAddServices<IAssemblyMarker>();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
builder.Services.AddScoped<IGlobalErrorFilter, LogErrorFilter>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using MoonCore.Logging;
|
using MoonCore.Logging;
|
||||||
|
|
||||||
namespace Moonlight.Client.Startup;
|
namespace Moonlight.Client.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task SetupLogging()
|
private static void AddLogging(this WebAssemblyHostBuilder builder)
|
||||||
{
|
{
|
||||||
var loggerFactory = new LoggerFactory();
|
builder.Logging.ClearProviders();
|
||||||
loggerFactory.AddAnsiConsole();
|
builder.Logging.AddAnsiConsole();
|
||||||
|
|
||||||
Logger = loggerFactory.CreateLogger<Startup>();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task RegisterLogging()
|
|
||||||
{
|
|
||||||
WebAssemblyHostBuilder.Logging.ClearProviders();
|
|
||||||
WebAssemblyHostBuilder.Logging.AddAnsiConsole();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Moonlight.Shared.Misc;
|
|
||||||
|
|
||||||
namespace Moonlight.Client.Startup;
|
namespace Moonlight.Client.Startup;
|
||||||
|
|
||||||
public partial class Startup
|
public static partial class Startup
|
||||||
{
|
{
|
||||||
private Task PrintVersion()
|
private static void PrintVersion()
|
||||||
{
|
{
|
||||||
// Fancy start console output... yes very fancy :>
|
// Fancy start console output... yes very fancy :>
|
||||||
Console.Write("Running ");
|
Console.Write("Running ");
|
||||||
@@ -23,30 +19,5 @@ public partial class Startup
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadConfiguration()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var httpClient = new HttpClient();
|
|
||||||
httpClient.BaseAddress = new Uri(WebAssemblyHostBuilder.HostEnvironment.BaseAddress);
|
|
||||||
|
|
||||||
var jsonText = await httpClient.GetStringAsync("frontend.json");
|
|
||||||
|
|
||||||
Configuration = JsonSerializer.Deserialize<FrontendConfiguration>(jsonText, new JsonSerializerOptions()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
})!;
|
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddSingleton(Configuration);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogCritical("Unable to load configuration. Unable to continue: {e}", e);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user