From e43d6bff06d5bce0d027c31a2764b3088d917566 Mon Sep 17 00:00:00 2001 From: Baumgartner Marcel Date: Wed, 5 Jun 2024 11:26:33 +0200 Subject: [PATCH 1/6] Upgraded to dotnet 8 --- Moonlight/Moonlight.csproj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 88b2155f..847bd682 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable Linux @@ -91,10 +91,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + From 2bb3b0fd48d4bd9ed8888f4b542819bd76fa0504 Mon Sep 17 00:00:00 2001 From: Baumgartner Marcel Date: Wed, 5 Jun 2024 13:27:09 +0200 Subject: [PATCH 2/6] Added api documentation/definition system system --- Moonlight/Assets/Core/css/scalar.css | 66 +++++++++ .../Core/Attributes/ApiDocumentAttribute.cs | 11 ++ .../Core/Configuration/CoreConfiguration.cs | 8 ++ Moonlight/Core/CoreFeature.cs | 135 ++++++++++++------ .../Controllers/ApiReferenceController.cs | 53 +++++++ .../Core/Http/Controllers/AssetController.cs | 2 + .../Core/Http/Controllers/AvatarController.cs | 2 + .../ApiDefinition/InternalApiDefinition.cs | 14 ++ Moonlight/Core/Interfaces/IApiDefinition.cs | 9 ++ .../Abstractions/Feature/PreInitContext.cs | 2 + Moonlight/Core/Models/ScalarOptions.cs | 43 ++++++ Moonlight/Core/Services/FeatureService.cs | 3 +- Moonlight/Program.cs | 2 +- 13 files changed, 305 insertions(+), 45 deletions(-) create mode 100644 Moonlight/Assets/Core/css/scalar.css create mode 100644 Moonlight/Core/Attributes/ApiDocumentAttribute.cs create mode 100644 Moonlight/Core/Http/Controllers/ApiReferenceController.cs create mode 100644 Moonlight/Core/Implementations/ApiDefinition/InternalApiDefinition.cs create mode 100644 Moonlight/Core/Interfaces/IApiDefinition.cs create mode 100644 Moonlight/Core/Models/ScalarOptions.cs diff --git a/Moonlight/Assets/Core/css/scalar.css b/Moonlight/Assets/Core/css/scalar.css new file mode 100644 index 00000000..061c233c --- /dev/null +++ b/Moonlight/Assets/Core/css/scalar.css @@ -0,0 +1,66 @@ +.light-mode { + --scalar-background-1: #fff; + --scalar-background-2: #f8fafc; + --scalar-background-3: #e7e7e7; + --scalar-background-accent: #8ab4f81f; + --scalar-color-1: #000; + --scalar-color-2: #6b7280; + --scalar-color-3: #9ca3af; + --scalar-color-accent: #00c16a; + --scalar-border-color: #e5e7eb; + --scalar-color-green: #069061; + --scalar-color-red: #ef4444; + --scalar-color-yellow: #f59e0b; + --scalar-color-blue: #1d4ed8; + --scalar-color-orange: #fb892c; + --scalar-color-purple: #6d28d9; + --scalar-button-1: #000; + --scalar-button-1-hover: rgba(0, 0, 0, 0.9); + --scalar-button-1-color: #fff; +} +.dark-mode { + --scalar-background-1: #020420; + --scalar-background-2: #121a31; + --scalar-background-3: #1e293b; + --scalar-background-accent: #8ab4f81f; + --scalar-color-1: #fff; + --scalar-color-2: #cbd5e1; + --scalar-color-3: #94a3b8; + --scalar-color-accent: #00dc82; + --scalar-border-color: #1e293b; + --scalar-color-green: #069061; + --scalar-color-red: #f87171; + --scalar-color-yellow: #fde68a; + --scalar-color-blue: #60a5fa; + --scalar-color-orange: #fb892c; + --scalar-color-purple: #ddd6fe; + --scalar-button-1: hsla(0, 0%, 100%, 0.9); + --scalar-button-1-hover: hsla(0, 0%, 100%, 0.8); + --scalar-button-1-color: #000; +} +.dark-mode .t-doc__sidebar, +.light-mode .t-doc__sidebar { + --scalar-sidebar-background-1: var(--scalar-background-1); + --scalar-sidebar-color-1: var(--scalar-color-1); + --scalar-sidebar-color-2: var(--scalar-color-3); + --scalar-sidebar-border-color: var(--scalar-border-color); + --scalar-sidebar-item-hover-background: transparent; + --scalar-sidebar-item-hover-color: var(--scalar-color-1); + --scalar-sidebar-item-active-background: transparent; + --scalar-sidebar-color-active: var(--scalar-color-accent); + --scalar-sidebar-search-background: transparent; + --scalar-sidebar-search-color: var(--scalar-color-3); + --scalar-sidebar-search-border-color: var(--scalar-border-color); + --scalar-sidebar-indent-border: var(--scalar-border-color); + --scalar-sidebar-indent-border-hover: var(--scalar-color-1); + --scalar-sidebar-indent-border-active: var(--scalar-color-accent); +} +.scalar-card .request-card-footer { + --scalar-background-3: var(--scalar-background-2); + --scalar-button-1: #0f172a; + --scalar-button-1-hover: rgba(30, 41, 59, 0.5); + --scalar-button-1-color: #fff; +} +.scalar-card .show-api-client-button { + border: 1px solid #334155 !important; +} \ No newline at end of file diff --git a/Moonlight/Core/Attributes/ApiDocumentAttribute.cs b/Moonlight/Core/Attributes/ApiDocumentAttribute.cs new file mode 100644 index 00000000..5f83e86a --- /dev/null +++ b/Moonlight/Core/Attributes/ApiDocumentAttribute.cs @@ -0,0 +1,11 @@ +namespace Moonlight.Core.Attributes; + +public class ApiDocumentAttribute : Attribute +{ + public string Name { get; set; } + + public ApiDocumentAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Moonlight/Core/Configuration/CoreConfiguration.cs b/Moonlight/Core/Configuration/CoreConfiguration.cs index 6af7336c..48758aab 100644 --- a/Moonlight/Core/Configuration/CoreConfiguration.cs +++ b/Moonlight/Core/Configuration/CoreConfiguration.cs @@ -17,6 +17,14 @@ public class CoreConfiguration [JsonProperty("Customisation")] public CustomisationData Customisation { get; set; } = new(); [JsonProperty("Security")] public SecurityData Security { get; set; } = new(); + [JsonProperty("Development")] public DevelopmentData Development { get; set; } = new(); + + public class DevelopmentData + { + [JsonProperty("EnableApiReference")] + [Description("This enables the api reference at your-moonlight.domain/admin/api/reference. Changing this requires a restart")] + public bool EnableApiReference { get; set; } = false; + } public class HttpData { diff --git a/Moonlight/Core/CoreFeature.cs b/Moonlight/Core/CoreFeature.cs index a65c282a..aef6a824 100644 --- a/Moonlight/Core/CoreFeature.cs +++ b/Moonlight/Core/CoreFeature.cs @@ -20,7 +20,10 @@ using Moonlight.Core.Models.Abstractions.Feature; using Moonlight.Core.Models.Enums; using Moonlight.Core.Repositories; using Moonlight.Core.Services; -using Moonlight.Core.UI.Components.Cards; +using Microsoft.OpenApi.Models; +using Moonlight.Core.Attributes; +using Moonlight.Core.Implementations.ApiDefinition; +using Swashbuckle.AspNetCore.SwaggerGen; namespace Moonlight.Core; @@ -32,7 +35,7 @@ public class CoreFeature : MoonlightFeature Author = "MasuOwO and contributors"; IssueTracker = "https://github.com/Moonlight-Panel/Moonlight/issues"; } - + public override Task OnPreInitialized(PreInitContext context) { // Load configuration @@ -41,17 +44,17 @@ public class CoreFeature : MoonlightFeature ); var config = configService.Get(); - + // Services context.EnableDependencyInjection(); - + var builder = context.Builder; - + builder.Services.AddDbContext(); - + // builder.Services.AddSingleton(new JwtService(config.Security.Token)); - + // Mooncore services builder.Services.AddScoped(typeof(Repository<>), typeof(GenericRepository<>)); builder.Services.AddScoped(); @@ -60,7 +63,7 @@ public class CoreFeature : MoonlightFeature builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - + builder.Services.AddMoonCoreUi(configuration => { configuration.ToastJavascriptPrefix = "moonlight.toasts"; @@ -69,13 +72,13 @@ public class CoreFeature : MoonlightFeature configuration.ClipboardJavascriptPrefix = "moonlight.clipboard"; configuration.FileDownloadJavascriptPrefix = "moonlight.utils"; }); - + // Add external services and blazor/asp.net stuff builder.Services.AddRazorPages(); builder.Services.AddHttpContextAccessor(); builder.Services.AddControllers(); builder.Services.AddBlazorTable(); - + // Configure blazor pipeline in detail builder.Services.AddServerSideBlazor().AddHubOptions(options => { @@ -85,15 +88,15 @@ public class CoreFeature : MoonlightFeature // Setup authentication if required if (config.Authentication.UseDefaultAuthentication) builder.Services.AddScoped(); - + // Setup http upload limit context.Builder.WebHost.ConfigureKestrel(options => { options.Limits.MaxRequestBodySize = ByteSizeValue.FromMegaBytes(config.Http.UploadLimit).Bytes; }); - + // Assets - + // - Javascript context.AddAsset("Core", "js/bootstrap.js"); context.AddAsset("Core", "js/moonlight.js"); @@ -101,24 +104,58 @@ public class CoreFeature : MoonlightFeature context.AddAsset("Core", "js/toaster.js"); context.AddAsset("Core", "js/sidebar.js"); context.AddAsset("Core", "js/alerter.js"); - + // - Css context.AddAsset("Core", "css/blazor.css"); context.AddAsset("Core", "css/boxicons.css"); context.AddAsset("Core", "css/sweetalert2dark.css"); context.AddAsset("Core", "css/utils.css"); - + + // Api + if (config.Development.EnableApiReference) + { + builder.Services.AddSwaggerGen(async options => + { + foreach (var definition in await context.Plugins.GetImplementations()) + { + options.SwaggerDoc( + definition.GetId(), + new OpenApiInfo() + { + Title = definition.GetName(), + Version = definition.GetVersion() + } + ); + } + + options.SwaggerGeneratorOptions.DocInclusionPredicate = (document, description) => + { + foreach (var attribute in description.CustomAttributes()) + { + if (attribute is ApiDocumentAttribute documentAttribute) + return document == documentAttribute.Name; + } + + return false; + }; + }); + } + return Task.CompletedTask; } public override async Task OnInitialized(InitContext context) { var app = context.Application; - + + // Config + var configService = app.Services.GetRequiredService>(); + var config = configService.Get(); + // Allow MoonlightService to access the app var moonlightService = app.Services.GetRequiredService(); moonlightService.Application = app; - + // Define permissions var permissionService = app.Services.GetRequiredService(); @@ -127,25 +164,31 @@ public class CoreFeature : MoonlightFeature Name = "See Admin Page", Description = "Allows access to the admin page and the connected stats (server and user count)" }); - + await permissionService.Register(1000, new() { Name = "Manage users", Description = "Allows access to users and their sessions" }); - + await permissionService.Register(9000, new() { Name = "View exceptions", Description = "Allows to see the raw message of exceptions when thrown in a view" }); + + await permissionService.Register(9998, new() + { + Name = "Manage admin api access", + Description = "Allows access to manage api keys and their permissions" + }); await permissionService.Register(9999, new() { Name = "Manage system", Description = "Allows access to the core system if moonlight and all configuration files" }); - + // app.UseStaticFiles(); app.UseRouting(); @@ -153,7 +196,7 @@ public class CoreFeature : MoonlightFeature app.MapFallbackToPage("/_Host"); app.MapControllers(); app.UseWebSockets(); - + // Plugins var pluginService = app.Services.GetRequiredService(); @@ -162,26 +205,26 @@ public class CoreFeature : MoonlightFeature await pluginService.RegisterImplementation(new PluginsDiagnoseAction()); await pluginService.RegisterImplementation(new FeatureDiagnoseAction()); await pluginService.RegisterImplementation(new LogDiagnoseAction()); - + // UI await pluginService.RegisterImplementation(new UserCount()); await pluginService.RegisterImplementation(new GreetingMessages()); - + // Startup job services var startupJobService = app.Services.GetRequiredService(); await startupJobService.AddJob("Default user creation", TimeSpan.FromSeconds(3), async provider => { using var scope = provider.CreateScope(); - + var configService = scope.ServiceProvider.GetRequiredService>(); var userRepo = scope.ServiceProvider.GetRequiredService>(); var authenticationProvider = scope.ServiceProvider.GetRequiredService(); - - if(!configService.Get().Authentication.UseDefaultAuthentication) + + if (!configService.Get().Authentication.UseDefaultAuthentication) return; - - if(userRepo.Get().Any()) + + if (userRepo.Get().Any()) return; // Define credentials @@ -202,43 +245,52 @@ public class CoreFeature : MoonlightFeature var user = userRepo.Get().First(x => x.Username == username); user.Permissions = 9999; userRepo.Update(user); - + Logger.Info($"Default login: Email: '{email}' Password: '{password}'"); }); + + // Api + if (config.Development.EnableApiReference) + { + app.MapSwagger("/api/core/reference/openapi/{documentName}"); + + await pluginService.RegisterImplementation(new InternalApiDefinition()); + } } public override Task OnUiInitialized(UiInitContext context) { context.EnablePages(); - + // User pages context.AddSidebarItem("Dashboard", "bxs-dashboard", "/", needsExactMatch: true, index: int.MinValue); - + // Admin pages - context.AddSidebarItem("Dashboard", "bxs-dashboard", "/admin", needsExactMatch: true, isAdmin: true, index: int.MinValue); + context.AddSidebarItem("Dashboard", "bxs-dashboard", "/admin", needsExactMatch: true, isAdmin: true, + index: int.MinValue); context.AddSidebarItem("Users", "bxs-group", "/admin/users", needsExactMatch: false, isAdmin: true); context.AddSidebarItem("System", "bxs-component", "/admin/sys", needsExactMatch: false, isAdmin: true); - + return Task.CompletedTask; } public override async Task OnSessionInitialized(SessionInitContext context) { var lazyLoader = context.LazyLoader; - + // - Authentication var cookieService = context.ServiceProvider.GetRequiredService(); var identityService = context.ServiceProvider.GetRequiredService(); - + await lazyLoader.SetText("Authenticating"); var token = await cookieService.GetValue("token"); await identityService.Authenticate(token); - + // - Session await lazyLoader.SetText("Starting session"); var scopedStorageService = context.ServiceProvider.GetRequiredService(); var sessionService = context.ServiceProvider.GetRequiredService(); - + var navigationManager = context.ServiceProvider.GetRequiredService(); var alertService = context.ServiceProvider.GetRequiredService(); @@ -253,15 +305,12 @@ public class CoreFeature : MoonlightFeature }; // Setup updating - navigationManager.LocationChanged += (_, _) => - { - session.UpdatedAt = DateTime.UtcNow; - }; - + navigationManager.LocationChanged += (_, _) => { session.UpdatedAt = DateTime.UtcNow; }; + // Save session and session service to view storage scopedStorageService.Set("Session", session); scopedStorageService.Set("SessionService", sessionService); - + // Register session await sessionService.Add(session); } diff --git a/Moonlight/Core/Http/Controllers/ApiReferenceController.cs b/Moonlight/Core/Http/Controllers/ApiReferenceController.cs new file mode 100644 index 00000000..8bae2e68 --- /dev/null +++ b/Moonlight/Core/Http/Controllers/ApiReferenceController.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Mvc; +using MoonCore.Services; +using Moonlight.Core.Attributes; +using Moonlight.Core.Configuration; +using Moonlight.Core.Models; +using Newtonsoft.Json; + +namespace Moonlight.Core.Http.Controllers; + +[ApiController] +[ApiDocument("internal")] +[Route("/api/core/reference")] +public class ApiReferenceController : Controller +{ + private readonly ConfigService ConfigService; + + public ApiReferenceController(ConfigService configService) + { + ConfigService = configService; + } + + [HttpGet] + public async Task Get([FromQuery] string document) + { + if (!ConfigService.Get().Development.EnableApiReference) + return BadRequest("Api reference is disabled"); + + var options = new ScalarOptions(); + var optionsJson = JsonConvert.SerializeObject(options, Formatting.Indented); + + var html = "\n" + + "\n" + + "\n" + + "Moonlight Api Reference\n" + + "\n" + + "\n" + + "\n" + + "\n" + + $"\n" + + "\n" + + "\n" + + "\n" + + ""; + + return Content(html, "text/html"); + } +} \ No newline at end of file diff --git a/Moonlight/Core/Http/Controllers/AssetController.cs b/Moonlight/Core/Http/Controllers/AssetController.cs index 2d22c4d8..573f0b29 100644 --- a/Moonlight/Core/Http/Controllers/AssetController.cs +++ b/Moonlight/Core/Http/Controllers/AssetController.cs @@ -1,9 +1,11 @@ using Microsoft.AspNetCore.Mvc; using MoonCore.Helpers; +using Moonlight.Core.Attributes; namespace Moonlight.Core.Http.Controllers; [ApiController] +[ApiDocument("internal")] [Route("api/core/asset")] public class AssetController : Controller { diff --git a/Moonlight/Core/Http/Controllers/AvatarController.cs b/Moonlight/Core/Http/Controllers/AvatarController.cs index 1b235ff5..5a77a6d4 100644 --- a/Moonlight/Core/Http/Controllers/AvatarController.cs +++ b/Moonlight/Core/Http/Controllers/AvatarController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using MoonCore.Abstractions; using MoonCore.Helpers; using MoonCore.Services; +using Moonlight.Core.Attributes; using Moonlight.Core.Configuration; using Moonlight.Core.Database.Entities; using Moonlight.Core.Services; @@ -11,6 +12,7 @@ using Moonlight.Core.Services; namespace Moonlight.Core.Http.Controllers; [ApiController] +[ApiDocument("internal")] [Route("api/core/avatar")] public class AvatarController : Controller { diff --git a/Moonlight/Core/Implementations/ApiDefinition/InternalApiDefinition.cs b/Moonlight/Core/Implementations/ApiDefinition/InternalApiDefinition.cs new file mode 100644 index 00000000..7fab3bc9 --- /dev/null +++ b/Moonlight/Core/Implementations/ApiDefinition/InternalApiDefinition.cs @@ -0,0 +1,14 @@ +using Moonlight.Core.Interfaces; + +namespace Moonlight.Core.Implementations.ApiDefinition; + +public class InternalApiDefinition : IApiDefinition +{ + public string GetId() => "internal"; + + public string GetName() => "Internal API"; + + public string GetVersion() => "v2"; + + public string[] GetPermissions() => []; +} \ No newline at end of file diff --git a/Moonlight/Core/Interfaces/IApiDefinition.cs b/Moonlight/Core/Interfaces/IApiDefinition.cs new file mode 100644 index 00000000..356daa2a --- /dev/null +++ b/Moonlight/Core/Interfaces/IApiDefinition.cs @@ -0,0 +1,9 @@ +namespace Moonlight.Core.Interfaces; + +public interface IApiDefinition +{ + public string GetId(); + public string GetName(); + public string GetVersion(); + public string[] GetPermissions(); +} \ No newline at end of file diff --git a/Moonlight/Core/Models/Abstractions/Feature/PreInitContext.cs b/Moonlight/Core/Models/Abstractions/Feature/PreInitContext.cs index ac16d6bf..dee6415f 100644 --- a/Moonlight/Core/Models/Abstractions/Feature/PreInitContext.cs +++ b/Moonlight/Core/Models/Abstractions/Feature/PreInitContext.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Moonlight.Core.Services; namespace Moonlight.Core.Models.Abstractions.Feature; @@ -7,6 +8,7 @@ public class PreInitContext public WebApplicationBuilder Builder { get; set; } public List DiAssemblies { get; set; } = new(); public Dictionary> Assets { get; set; } = new(); + public PluginService Plugins { get; set; } public void EnableDependencyInjection() { diff --git a/Moonlight/Core/Models/ScalarOptions.cs b/Moonlight/Core/Models/ScalarOptions.cs new file mode 100644 index 00000000..b411fb55 --- /dev/null +++ b/Moonlight/Core/Models/ScalarOptions.cs @@ -0,0 +1,43 @@ +namespace Moonlight.Core.Models; + +// From https://github.com/scalar/scalar/blob/main/packages/scalar.aspnetcore/ScalarOptions.cs + +public class ScalarOptions +{ + public string Theme { get; set; } = "purple"; + + public bool? DarkMode { get; set; } + public bool? HideDownloadButton { get; set; } + public bool? ShowSideBar { get; set; } + + public bool? WithDefaultFonts { get; set; } + + public string? Layout { get; set; } + + public string? CustomCss { get; set; } + + public string? SearchHotkey { get; set; } + + public Dictionary? Metadata { get; set; } + + public ScalarAuthenticationOptions? Authentication { get; set; } +} + +public class ScalarAuthenticationOptions +{ + public string? PreferredSecurityScheme { get; set; } + + public ScalarAuthenticationApiKey? ApiKey { get; set; } +} + +public class ScalarAuthenticationoAuth2 +{ + public string? ClientId { get; set; } + + public List? Scopes { get; set; } +} + +public class ScalarAuthenticationApiKey +{ + public string? Token { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Core/Services/FeatureService.cs b/Moonlight/Core/Services/FeatureService.cs index c6448b20..5e9f3445 100644 --- a/Moonlight/Core/Services/FeatureService.cs +++ b/Moonlight/Core/Services/FeatureService.cs @@ -55,11 +55,12 @@ public class FeatureService return Task.CompletedTask; } - public async Task PreInit(WebApplicationBuilder builder) + public async Task PreInit(WebApplicationBuilder builder, PluginService pluginService) { Logger.Info("Pre-initializing features"); PreInitContext.Builder = builder; + PreInitContext.Plugins = pluginService; foreach (var feature in Features) { diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 3561037f..6a553486 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -105,7 +105,7 @@ builder.Services.AddSingleton(configService); builder.Services.AddSingleton(pluginService); // Feature hook -await featureService.PreInit(builder); +await featureService.PreInit(builder, pluginService); // Plugin hook await pluginService.PreInitialize(builder); From d2d2463164b000b29a0659df431a29a53cb34b1b Mon Sep 17 00:00:00 2001 From: Baumgartner Marcel Date: Wed, 5 Jun 2024 15:44:47 +0200 Subject: [PATCH 3/6] Added api keys and api ui --- Moonlight/Core/CoreFeature.cs | 7 +- Moonlight/Core/Database/DataContext.cs | 1 + Moonlight/Core/Database/Entities/ApiKey.cs | 11 + .../20240605120928_AddedApiKeys.Designer.cs | 657 ++++++++++++++++++ .../Migrations/20240605120928_AddedApiKeys.cs | 44 ++ .../Migrations/DataContextModelSnapshot.cs | 60 +- .../Controllers/ApiReferenceController.cs | 6 +- .../Models/Forms/ApiKeys/CreateApiKeyForm.cs | 18 + .../Models/Forms/ApiKeys/UpdateApiKeyForm.cs | 18 + .../Navigations/AdminApiNavigation.razor | 15 + Moonlight/Core/UI/Views/Admin/Api/Index.razor | 80 +++ Moonlight/Core/UI/Views/Admin/Api/Keys.razor | 68 ++ Moonlight/Moonlight.csproj | 4 + 13 files changed, 982 insertions(+), 7 deletions(-) create mode 100644 Moonlight/Core/Database/Entities/ApiKey.cs create mode 100644 Moonlight/Core/Database/Migrations/20240605120928_AddedApiKeys.Designer.cs create mode 100644 Moonlight/Core/Database/Migrations/20240605120928_AddedApiKeys.cs create mode 100644 Moonlight/Core/Models/Forms/ApiKeys/CreateApiKeyForm.cs create mode 100644 Moonlight/Core/Models/Forms/ApiKeys/UpdateApiKeyForm.cs create mode 100644 Moonlight/Core/UI/Components/Navigations/AdminApiNavigation.razor create mode 100644 Moonlight/Core/UI/Views/Admin/Api/Index.razor create mode 100644 Moonlight/Core/UI/Views/Admin/Api/Keys.razor diff --git a/Moonlight/Core/CoreFeature.cs b/Moonlight/Core/CoreFeature.cs index aef6a824..e100d481 100644 --- a/Moonlight/Core/CoreFeature.cs +++ b/Moonlight/Core/CoreFeature.cs @@ -251,11 +251,9 @@ public class CoreFeature : MoonlightFeature // Api if (config.Development.EnableApiReference) - { app.MapSwagger("/api/core/reference/openapi/{documentName}"); - - await pluginService.RegisterImplementation(new InternalApiDefinition()); - } + + await pluginService.RegisterImplementation(new InternalApiDefinition()); } public override Task OnUiInitialized(UiInitContext context) @@ -269,6 +267,7 @@ public class CoreFeature : MoonlightFeature context.AddSidebarItem("Dashboard", "bxs-dashboard", "/admin", needsExactMatch: true, isAdmin: true, index: int.MinValue); context.AddSidebarItem("Users", "bxs-group", "/admin/users", needsExactMatch: false, isAdmin: true); + context.AddSidebarItem("API", "bx-code-curly", "/admin/api", needsExactMatch: false, isAdmin: true); context.AddSidebarItem("System", "bxs-component", "/admin/sys", needsExactMatch: false, isAdmin: true); return Task.CompletedTask; diff --git a/Moonlight/Core/Database/DataContext.cs b/Moonlight/Core/Database/DataContext.cs index c2daaa76..d987ada6 100644 --- a/Moonlight/Core/Database/DataContext.cs +++ b/Moonlight/Core/Database/DataContext.cs @@ -12,6 +12,7 @@ public class DataContext : DbContext // Core public DbSet Users { get; set; } + public DbSet ApiKeys { get; set; } // Servers public DbSet Servers { get; set; } diff --git a/Moonlight/Core/Database/Entities/ApiKey.cs b/Moonlight/Core/Database/Entities/ApiKey.cs new file mode 100644 index 00000000..ae02fcd2 --- /dev/null +++ b/Moonlight/Core/Database/Entities/ApiKey.cs @@ -0,0 +1,11 @@ +namespace Moonlight.Core.Database.Entities; + +public class ApiKey +{ + public int Id { get; set; } + public string Key { get; set; } = ""; + public string Description { get; set; } = ""; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime ExpiresAt { get; set; } = DateTime.UtcNow; + public string PermissionJson { get; set; } = "[]"; +} \ No newline at end of file diff --git a/Moonlight/Core/Database/Migrations/20240605120928_AddedApiKeys.Designer.cs b/Moonlight/Core/Database/Migrations/20240605120928_AddedApiKeys.Designer.cs new file mode 100644 index 00000000..a40acb82 --- /dev/null +++ b/Moonlight/Core/Database/Migrations/20240605120928_AddedApiKeys.Designer.cs @@ -0,0 +1,657 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.Core.Database; + +#nullable disable + +namespace Moonlight.Core.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240605120928_AddedApiKeys")] + partial class AddedApiKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PermissionJson") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("int"); + + b.Property("TokenValidTimestamp") + .HasColumnType("datetime(6)"); + + b.Property("Totp") + .HasColumnType("tinyint(1)"); + + b.Property("TotpSecret") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Cpu") + .HasColumnType("int"); + + b.Property("DisablePublicNetwork") + .HasColumnType("tinyint(1)"); + + b.Property("Disk") + .HasColumnType("int"); + + b.Property("DockerImageIndex") + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("MainAllocationId") + .HasColumnType("int"); + + b.Property("Memory") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NetworkId") + .HasColumnType("int"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("OverrideStartupCommand") + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("UseVirtualDisk") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NetworkId"); + + b.HasIndex("NodeId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Note") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("ServerNodeId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("ServerNodeId"); + + b.ToTable("ServerAllocations"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Completed") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Successful") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerBackup"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AutoPull") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerImageId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ServerImageId"); + + b.ToTable("ServerDockerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllocationsNeeded") + .HasColumnType("int"); + + b.Property("AllowDockerImageChange") + .HasColumnType("tinyint(1)"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DefaultDockerImage") + .HasColumnType("int"); + + b.Property("DonateUrl") + .HasColumnType("longtext"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallShell") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OnlineDetection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ParseConfiguration") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartupCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdateUrl") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("ServerImages"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllowEdit") + .HasColumnType("tinyint(1)"); + + b.Property("AllowView") + .HasColumnType("tinyint(1)"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Filter") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerImageId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ServerImageId"); + + b.ToTable("ServerImageVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNetwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("UserId"); + + b.ToTable("ServerNetworks"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FtpPort") + .HasColumnType("int"); + + b.Property("HttpPort") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Ssl") + .HasColumnType("tinyint(1)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("ServerNodes"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ExecutionSeconds") + .HasColumnType("int"); + + b.Property("LastRun") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerSchedules"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DataJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("ServerScheduleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ServerScheduleId"); + + b.ToTable("ServerScheduleItems"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerAllocation", "MainAllocation") + .WithMany() + .HasForeignKey("MainAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNetwork", "Network") + .WithMany() + .HasForeignKey("NetworkId"); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Network"); + + b.Navigation("Node"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerAllocation", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", null) + .WithMany("Allocations") + .HasForeignKey("ServerNodeId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerBackup", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Backups") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerDockerImage", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("DockerImages") + .HasForeignKey("ServerImageId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null) + .WithMany("Variables") + .HasForeignKey("ServerImageId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNetwork", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerNode", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.Core.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Node"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerSchedule", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Schedules") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerScheduleItem", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.ServerSchedule", null) + .WithMany("Items") + .HasForeignKey("ServerScheduleId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b => + { + b.HasOne("Moonlight.Features.Servers.Entities.Server", null) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Backups"); + + b.Navigation("Schedules"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerSchedule", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/Core/Database/Migrations/20240605120928_AddedApiKeys.cs b/Moonlight/Core/Database/Migrations/20240605120928_AddedApiKeys.cs new file mode 100644 index 00000000..99eb351d --- /dev/null +++ b/Moonlight/Core/Database/Migrations/20240605120928_AddedApiKeys.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.Core.Database.Migrations +{ + /// + public partial class AddedApiKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeys", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Key = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + ExpiresAt = table.Column(type: "datetime(6)", nullable: false), + PermissionJson = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeys"); + } + } +} diff --git a/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs index c6a063bd..7b9b056e 100644 --- a/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/Core/Database/Migrations/DataContextModelSnapshot.cs @@ -2,6 +2,7 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Moonlight.Core.Database; @@ -16,15 +17,50 @@ namespace Moonlight.Core.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.2") + .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 64); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Moonlight.Core.Database.Entities.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PermissionJson") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("ApiKeys"); + }); + modelBuilder.Entity("Moonlight.Core.Database.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("datetime(6)"); @@ -68,6 +104,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Cpu") .HasColumnType("int"); @@ -129,6 +167,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("IpAddress") .IsRequired() .HasColumnType("longtext"); @@ -161,6 +201,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Completed") .HasColumnType("tinyint(1)"); @@ -189,6 +231,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AutoPull") .HasColumnType("tinyint(1)"); @@ -216,6 +260,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AllocationsNeeded") .HasColumnType("int"); @@ -278,6 +324,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AllowEdit") .HasColumnType("tinyint(1)"); @@ -322,6 +370,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Name") .IsRequired() .HasColumnType("longtext"); @@ -347,6 +397,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Fqdn") .IsRequired() .HasColumnType("longtext"); @@ -379,6 +431,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("ExecutionSeconds") .HasColumnType("int"); @@ -405,6 +459,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Action") .IsRequired() .HasColumnType("longtext"); @@ -432,6 +488,8 @@ namespace Moonlight.Core.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Key") .IsRequired() .HasColumnType("longtext"); diff --git a/Moonlight/Core/Http/Controllers/ApiReferenceController.cs b/Moonlight/Core/Http/Controllers/ApiReferenceController.cs index 8bae2e68..cd047354 100644 --- a/Moonlight/Core/Http/Controllers/ApiReferenceController.cs +++ b/Moonlight/Core/Http/Controllers/ApiReferenceController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; using MoonCore.Services; using Moonlight.Core.Attributes; using Moonlight.Core.Configuration; @@ -20,7 +21,7 @@ public class ApiReferenceController : Controller } [HttpGet] - public async Task Get([FromQuery] string document) + public async Task Get([FromQuery][RegularExpression("^[a-z0-9_\\-]+$")] string document) { if (!ConfigService.Get().Development.EnableApiReference) return BadRequest("Api reference is disabled"); @@ -34,6 +35,7 @@ public class ApiReferenceController : Controller "Moonlight Api Reference\n" + "\n" + "\n" + + "\n"+ "\n" + "\n" + $"\n" + diff --git a/Moonlight/Core/Models/Forms/ApiKeys/CreateApiKeyForm.cs b/Moonlight/Core/Models/Forms/ApiKeys/CreateApiKeyForm.cs new file mode 100644 index 00000000..1ba1a765 --- /dev/null +++ b/Moonlight/Core/Models/Forms/ApiKeys/CreateApiKeyForm.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Core.Models.Forms.ApiKeys; + +public class CreateApiKeyForm +{ + [Required(ErrorMessage = "You need to provide a description")] + [Description("Write a note here for which application the api key is used for")] + public string Description { get; set; } = ""; + + [Required(ErrorMessage = "You need to specify the expiration date of the api key")] + [Description("Specify when the api key should expire")] + public DateTime ExpiresAt { get; set; } = DateTime.UtcNow; + + [Required(ErrorMessage = "You need to specify what permissions the api key should have")] + public string Permissions { get; set; } = "[]"; +} \ No newline at end of file diff --git a/Moonlight/Core/Models/Forms/ApiKeys/UpdateApiKeyForm.cs b/Moonlight/Core/Models/Forms/ApiKeys/UpdateApiKeyForm.cs new file mode 100644 index 00000000..1ce3d059 --- /dev/null +++ b/Moonlight/Core/Models/Forms/ApiKeys/UpdateApiKeyForm.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.Core.Models.Forms.ApiKeys; + +public class UpdateApiKeyForm +{ + [Required(ErrorMessage = "You need to provide a description")] + [Description("Write a note here for which application the api key is used for")] + public string Description { get; set; } = ""; + + [Required(ErrorMessage = "You need to specify the expiration date of the api key")] + [Description("Specify when the api key should expire")] + public DateTime ExpiresAt { get; set; } + + [Required(ErrorMessage = "You need to specify what permissions the api key should have")] + public string Permissions { get; set; } = "[]"; +} \ No newline at end of file diff --git a/Moonlight/Core/UI/Components/Navigations/AdminApiNavigation.razor b/Moonlight/Core/UI/Components/Navigations/AdminApiNavigation.razor new file mode 100644 index 00000000..d589ab6d --- /dev/null +++ b/Moonlight/Core/UI/Components/Navigations/AdminApiNavigation.razor @@ -0,0 +1,15 @@ +
+ +
+ +@code +{ + [Parameter] public int Index { get; set; } = 0; +} \ No newline at end of file diff --git a/Moonlight/Core/UI/Views/Admin/Api/Index.razor b/Moonlight/Core/UI/Views/Admin/Api/Index.razor new file mode 100644 index 00000000..06d66566 --- /dev/null +++ b/Moonlight/Core/UI/Views/Admin/Api/Index.razor @@ -0,0 +1,80 @@ +@page "/admin/api" + +@using MoonCore.Services +@using Moonlight.Core.Configuration +@using Moonlight.Core.Interfaces +@using Moonlight.Core.Services +@using Moonlight.Core.UI.Components.Navigations + +@inject PluginService PluginService +@inject ConfigService ConfigService + +@attribute [RequirePermission(9998)] + + + + + These apis allow other applications to communicate with moonlight and for example create a new user. + These apis are still work in progress and might change a lot so dont be mad at me if i change how they work. + + +
+
+ + + + + + + + + + +
+
+ +@code +{ + private ApiModel[] Apis; + + private async Task LoadApis(LazyLoader _) + { + List models = new(); + + foreach (var definition in await PluginService.GetImplementations()) + { + models.Add(new() + { + Id = definition.GetId(), + Name = definition.GetName(), + Version = definition.GetVersion() + }); + } + + Apis = models.ToArray(); + } + + class ApiModel + { + public string Id { get; set; } + public string Name { get; set; } + public string Version { get; set; } + } +} diff --git a/Moonlight/Core/UI/Views/Admin/Api/Keys.razor b/Moonlight/Core/UI/Views/Admin/Api/Keys.razor new file mode 100644 index 00000000..c524c8e7 --- /dev/null +++ b/Moonlight/Core/UI/Views/Admin/Api/Keys.razor @@ -0,0 +1,68 @@ +@page "/admin/api/keys" + +@using MoonCore.Abstractions +@using MoonCore.Helpers +@using MoonCoreUI.Services +@using Moonlight.Core.Database.Entities +@using Moonlight.Core.Models.Forms.ApiKeys +@using Moonlight.Core.UI.Components.Navigations + +@inject ClipboardService ClipboardService +@inject ToastService ToastService + +@attribute [RequirePermission(9998)] + + + +
+ + + + + + + + + + + + + + + +
+ +@code +{ + private IEnumerable ApiKeysLoader(Repository repository) + { + return repository.Get(); + } + + private async Task ValidateAdd(ApiKey apiKey) + { + var key = Formatter.GenerateString(32); + apiKey.Key = key; + + await ClipboardService.Copy(key); + await ToastService.Info("Copied api key into your clipboard"); + } +} diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 847bd682..3007059b 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -87,6 +87,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive From 77d24ed90f5ef53fb3bf552e48df2322cbbfd9e4 Mon Sep 17 00:00:00 2001 From: Baumgartner Marcel Date: Wed, 5 Jun 2024 15:51:21 +0200 Subject: [PATCH 4/6] Small ui adjustment --- Moonlight/Core/UI/Views/Admin/Api/Keys.razor | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Moonlight/Core/UI/Views/Admin/Api/Keys.razor b/Moonlight/Core/UI/Views/Admin/Api/Keys.razor index c524c8e7..79223d13 100644 --- a/Moonlight/Core/UI/Views/Admin/Api/Keys.razor +++ b/Moonlight/Core/UI/Views/Admin/Api/Keys.razor @@ -12,7 +12,7 @@ @attribute [RequirePermission(9998)] - +
- @apiKeyHalf - @bogusHalf + + @apiKeyHalf + [...] +
@@ -45,7 +47,7 @@ @Formatter.FormatDate(context!.ExpiresAt) - + @@ -56,7 +58,7 @@ { return repository.Get(); } - + private async Task ValidateAdd(ApiKey apiKey) { var key = Formatter.GenerateString(32); @@ -65,4 +67,4 @@ await ClipboardService.Copy(key); await ToastService.Info("Copied api key into your clipboard"); } -} +} \ No newline at end of file From 1cc32fa5c4c174aa131a2f2db8ea2acadbc9b70c Mon Sep 17 00:00:00 2001 From: Baumgartner Marcel Date: Wed, 5 Jun 2024 16:20:24 +0200 Subject: [PATCH 5/6] Started adding api permission check --- .../Core/Attributes/ApiPermissionAttribute.cs | 11 ++++ Moonlight/Core/CoreFeature.cs | 3 + .../Middleware/ApiPermissionMiddleware.cs | 55 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 Moonlight/Core/Attributes/ApiPermissionAttribute.cs create mode 100644 Moonlight/Core/Http/Middleware/ApiPermissionMiddleware.cs diff --git a/Moonlight/Core/Attributes/ApiPermissionAttribute.cs b/Moonlight/Core/Attributes/ApiPermissionAttribute.cs new file mode 100644 index 00000000..c0b8ea1b --- /dev/null +++ b/Moonlight/Core/Attributes/ApiPermissionAttribute.cs @@ -0,0 +1,11 @@ +namespace Moonlight.Core.Attributes; + +public class ApiPermissionAttribute : Attribute +{ + public string Permission { get; set; } + + public ApiPermissionAttribute(string permission) + { + Permission = permission; + } +} \ No newline at end of file diff --git a/Moonlight/Core/CoreFeature.cs b/Moonlight/Core/CoreFeature.cs index e100d481..7165ee44 100644 --- a/Moonlight/Core/CoreFeature.cs +++ b/Moonlight/Core/CoreFeature.cs @@ -22,6 +22,7 @@ using Moonlight.Core.Repositories; using Moonlight.Core.Services; using Microsoft.OpenApi.Models; using Moonlight.Core.Attributes; +using Moonlight.Core.Http.Middleware; using Moonlight.Core.Implementations.ApiDefinition; using Swashbuckle.AspNetCore.SwaggerGen; @@ -252,6 +253,8 @@ public class CoreFeature : MoonlightFeature // Api if (config.Development.EnableApiReference) app.MapSwagger("/api/core/reference/openapi/{documentName}"); + + app.UseMiddleware(); await pluginService.RegisterImplementation(new InternalApiDefinition()); } diff --git a/Moonlight/Core/Http/Middleware/ApiPermissionMiddleware.cs b/Moonlight/Core/Http/Middleware/ApiPermissionMiddleware.cs new file mode 100644 index 00000000..176586a1 --- /dev/null +++ b/Moonlight/Core/Http/Middleware/ApiPermissionMiddleware.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using MoonCore.Helpers; +using Moonlight.Core.Attributes; + +namespace Moonlight.Core.Http.Middleware; + +public class ApiPermissionMiddleware +{ + private RequestDelegate Next; + + public ApiPermissionMiddleware(RequestDelegate next) + { + Next = next; + } + + public async Task Invoke(HttpContext context) + { + if (CheckRequest(context)) + await Next(context); + else + { + context.Response.StatusCode = 403; + await context.Response.WriteAsync("Permission denied"); + } + } + + private bool CheckRequest(HttpContext context) + { + var endpoint = context.GetEndpoint(); + + if (endpoint == null) + return true; + + var metadata = endpoint + .Metadata + .GetMetadata(); + + if (metadata == null) + return true; + + if (metadata.ControllerTypeInfo.CustomAttributes + .All(x => x.AttributeType != typeof(ApiControllerAttribute))) + return true; + + var permissionAttr = + metadata.ControllerTypeInfo.CustomAttributes.FirstOrDefault(x => + x.AttributeType == typeof(ApiPermissionAttribute)); + + if (permissionAttr == null) + return true; + + if(metadata.) + } +} \ No newline at end of file From 1254925c4a32f6254aded70622a7d66fcff065cf Mon Sep 17 00:00:00 2001 From: Baumgartner Marcel Date: Thu, 6 Jun 2024 08:59:52 +0200 Subject: [PATCH 6/6] Implemented api permission system (backend) --- .../Middleware/ApiPermissionMiddleware.cs | 66 +++++++++++++++---- .../Models/Forms/ApiKeys/CreateApiKeyForm.cs | 3 +- .../Models/Forms/ApiKeys/UpdateApiKeyForm.cs | 3 +- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/Moonlight/Core/Http/Middleware/ApiPermissionMiddleware.cs b/Moonlight/Core/Http/Middleware/ApiPermissionMiddleware.cs index 176586a1..2a43d52d 100644 --- a/Moonlight/Core/Http/Middleware/ApiPermissionMiddleware.cs +++ b/Moonlight/Core/Http/Middleware/ApiPermissionMiddleware.cs @@ -1,17 +1,20 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using MoonCore.Helpers; +using Microsoft.AspNetCore.Mvc.Controllers; +using MoonCore.Abstractions; using Moonlight.Core.Attributes; +using Moonlight.Core.Database.Entities; +using Newtonsoft.Json; namespace Moonlight.Core.Http.Middleware; public class ApiPermissionMiddleware { private RequestDelegate Next; + private readonly IServiceProvider Provider; - public ApiPermissionMiddleware(RequestDelegate next) + public ApiPermissionMiddleware(RequestDelegate next, IServiceProvider provider) { Next = next; + Provider = provider; } public async Task Invoke(HttpContext context) @@ -39,17 +42,56 @@ public class ApiPermissionMiddleware if (metadata == null) return true; - if (metadata.ControllerTypeInfo.CustomAttributes - .All(x => x.AttributeType != typeof(ApiControllerAttribute))) + var controllerAttrInfo = metadata.ControllerTypeInfo.CustomAttributes + .FirstOrDefault(x => x.AttributeType == typeof(ApiPermissionAttribute)); + + var methodAttrInfo = metadata.MethodInfo.CustomAttributes + .FirstOrDefault(x => x.AttributeType == typeof(ApiPermissionAttribute)); + + if (methodAttrInfo == null && controllerAttrInfo == null) return true; - var permissionAttr = - metadata.ControllerTypeInfo.CustomAttributes.FirstOrDefault(x => - x.AttributeType == typeof(ApiPermissionAttribute)); + if (!context.Request.Headers.TryGetValue("Authorization", out var apiKeySv)) + return false; - if (permissionAttr == null) - return true; + // Entity framework won't work with the StringValues type returned by the Headers.TryGetValue method + // that's why we convert that to a regular string here + var apiKey = apiKeySv.ToString(); - if(metadata.) + if (string.IsNullOrEmpty(apiKey)) + return false; + + using var scope = Provider.CreateScope(); + var apiKeyRepo = scope.ServiceProvider.GetRequiredService>(); + + var apiKeyModel = apiKeyRepo + .Get() + .FirstOrDefault(x => x.Key == apiKey); + + if (apiKeyModel == null) + return false; + + if (apiKeyModel.ExpiresAt < DateTime.UtcNow) + return false; + + var permissions = JsonConvert.DeserializeObject(apiKeyModel.PermissionJson) ?? Array.Empty(); + + if (controllerAttrInfo != null) + { + var permissionToLookFor = controllerAttrInfo.ConstructorArguments.First().Value as string; + + if (permissionToLookFor != null && !permissions.Contains(permissionToLookFor)) + return false; + } + + if (methodAttrInfo != null) + { + var permissionToLookFor = methodAttrInfo.ConstructorArguments.First().Value as string; + + if (permissionToLookFor != null && !permissions.Contains(permissionToLookFor)) + return false; + } + + return true; } } \ No newline at end of file diff --git a/Moonlight/Core/Models/Forms/ApiKeys/CreateApiKeyForm.cs b/Moonlight/Core/Models/Forms/ApiKeys/CreateApiKeyForm.cs index 1ba1a765..9ed8f6eb 100644 --- a/Moonlight/Core/Models/Forms/ApiKeys/CreateApiKeyForm.cs +++ b/Moonlight/Core/Models/Forms/ApiKeys/CreateApiKeyForm.cs @@ -14,5 +14,6 @@ public class CreateApiKeyForm public DateTime ExpiresAt { get; set; } = DateTime.UtcNow; [Required(ErrorMessage = "You need to specify what permissions the api key should have")] - public string Permissions { get; set; } = "[]"; + [DisplayName("Permissions")] + public string PermissionJson { get; set; } = "[]"; } \ No newline at end of file diff --git a/Moonlight/Core/Models/Forms/ApiKeys/UpdateApiKeyForm.cs b/Moonlight/Core/Models/Forms/ApiKeys/UpdateApiKeyForm.cs index 1ce3d059..912494f2 100644 --- a/Moonlight/Core/Models/Forms/ApiKeys/UpdateApiKeyForm.cs +++ b/Moonlight/Core/Models/Forms/ApiKeys/UpdateApiKeyForm.cs @@ -14,5 +14,6 @@ public class UpdateApiKeyForm public DateTime ExpiresAt { get; set; } [Required(ErrorMessage = "You need to specify what permissions the api key should have")] - public string Permissions { get; set; } = "[]"; + [DisplayName("Permissions")] + public string PermissionJson { get; set; } = "[]"; } \ No newline at end of file