From 2bb3b0fd48d4bd9ed8888f4b542819bd76fa0504 Mon Sep 17 00:00:00 2001 From: Baumgartner Marcel Date: Wed, 5 Jun 2024 13:27:09 +0200 Subject: [PATCH] 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);