Implemented plugin loading via di on the api server. Fixed plugin loading in the client

This commit is contained in:
2025-02-24 20:03:37 +01:00
parent 69df761bf4
commit 3dd5d2958a
18 changed files with 157 additions and 415 deletions

View File

@@ -1,67 +0,0 @@
using System.Text.Json;
using MoonCore.Authentication;
using MoonCore.Extended.Abstractions;
using Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Http.Middleware;
public class ApiAuthenticationMiddleware
{
private readonly RequestDelegate Next;
private readonly ILogger<ApiAuthenticationMiddleware> Logger;
public ApiAuthenticationMiddleware(RequestDelegate next, ILogger<ApiAuthenticationMiddleware> logger)
{
Next = next;
Logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
await Authenticate(context);
await Next(context);
}
public Task Authenticate(HttpContext context)
{
var request = context.Request;
if(!request.Headers.ContainsKey("Authorization"))
return Task.CompletedTask;
if(request.Headers["Authorization"].Count == 0)
return Task.CompletedTask;
var authHeader = request.Headers["Authorization"].First();
if(string.IsNullOrEmpty(authHeader))
return Task.CompletedTask;
var parts = authHeader.Split(" ");
if(parts.Length != 2)
return Task.CompletedTask;
var bearerValue = parts[1];
if(!bearerValue.StartsWith("api_"))
return Task.CompletedTask;
if(bearerValue.Length != "api_".Length + 32)
return Task.CompletedTask;
var apiKeyRepo = context.RequestServices.GetRequiredService<DatabaseRepository<ApiKey>>();
var apiKey = apiKeyRepo.Get().FirstOrDefault(x => x.Secret == bearerValue);
if(apiKey == null)
return Task.CompletedTask;
var permissions = JsonSerializer.Deserialize<string[]>(apiKey.PermissionsJson) ?? [];
context.User = new PermClaimsPrinciple()
{
Permissions = permissions
};
return Task.CompletedTask;
}
}

View File

@@ -1,147 +0,0 @@
using Microsoft.AspNetCore.Mvc.Controllers;
using MoonCore.Attributes;
using MoonCore.Authentication;
using MoonCore.Extensions;
using Moonlight.ApiServer.Exceptions;
namespace Moonlight.ApiServer.Http.Middleware;
public class AuthorizationMiddleware
{
private readonly RequestDelegate Next;
private readonly ILogger<AuthorizationMiddleware> Logger;
public AuthorizationMiddleware(RequestDelegate next, ILogger<AuthorizationMiddleware> logger)
{
Next = next;
Logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (await Authorize(context))
{
try
{
await Next(context);
}
catch (MissingPermissionException e)
{
if (e.Permission == "meta.authenticated")
{
await Results.Problem(
title: "This endpoint requires a user authenticated token",
statusCode: 401
).ExecuteAsync(context);
}
else
{
await Results.Problem(
title: "You dont have the required permission",
detail: e.Permission,
statusCode: 403
).ExecuteAsync(context);
}
}
}
}
private async Task<bool> Authorize(HttpContext context)
{
var requiredPermissions = ResolveRequiredPermissions(context);
if (requiredPermissions.Length == 0)
return true;
// Check if no context => permissions have been loaded
if (context.User is not PermClaimsPrinciple permClaimsPrinciple)
{
await Results.Problem(
title: "An unauthenticated request is not allowed to use this endpoint",
statusCode: 401
).ExecuteAsync(context);
return false;
}
// Check if one of the required permissions is to be logged in
if (requiredPermissions.Any(x => x == "meta.authenticated") && permClaimsPrinciple.IdentityModel == null)
{
await Results.Problem(
title: "This endpoint requires a user authenticated token",
statusCode: 401
).ExecuteAsync(context);
return false;
}
foreach (var permission in requiredPermissions)
{
if(permission == "meta.authenticated") // We already verified that
continue;
if (!permClaimsPrinciple.HasPermission(permission))
{
await Results.Problem(
title: "You dont have the required permission",
detail: permission,
statusCode: 403
).ExecuteAsync(context);
return false;
}
}
return true;
}
private string[] ResolveRequiredPermissions(HttpContext context)
{
// Basic handling
var endpoint = context.GetEndpoint();
if (endpoint == null)
return [];
var metadata = endpoint
.Metadata
.GetMetadata<ControllerActionDescriptor>();
if (metadata == null)
return [];
// Retrieve attribute infos
var controllerAttrInfo = metadata
.ControllerTypeInfo
.CustomAttributes
.FirstOrDefault(x => x.AttributeType == typeof(RequirePermissionAttribute));
var methodAttrInfo = metadata
.MethodInfo
.CustomAttributes
.FirstOrDefault(x => x.AttributeType == typeof(RequirePermissionAttribute));
// Retrieve permissions from attribute infos
var controllerPermission = controllerAttrInfo != null
? controllerAttrInfo.ConstructorArguments.First().Value as string
: null;
var methodPermission = methodAttrInfo != null
? methodAttrInfo.ConstructorArguments.First().Value as string
: null;
// If both have a permission flag, return both
if (controllerPermission != null && methodPermission != null)
return [controllerPermission, methodPermission];
// If either of them have a permission set, return it
if (controllerPermission != null)
return [controllerPermission];
if (methodPermission != null)
return [methodPermission];
// If both have no permission set, allow everyone to access it
return [];
}
}

View File

@@ -1,33 +0,0 @@
using System.Text.Json;
using MoonCore.Authentication;
using Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Http.Middleware;
public class PermissionLoaderMiddleware
{
private readonly RequestDelegate Next;
public PermissionLoaderMiddleware(RequestDelegate next)
{
Next = next;
}
public async Task Invoke(HttpContext context)
{
await Load(context);
await Next(context);
}
private Task Load(HttpContext context)
{
if(context.User is not PermClaimsPrinciple permClaimsPrinciple)
return Task.CompletedTask;
if(permClaimsPrinciple.IdentityModel is not User user)
return Task.CompletedTask;
permClaimsPrinciple.Permissions = JsonSerializer.Deserialize<string[]>(user.PermissionsJson) ?? [];
return Task.CompletedTask;
}
}

View File

@@ -1,46 +0,0 @@
using Microsoft.OpenApi.Models;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Interfaces.Startup;
namespace Moonlight.ApiServer.Implementations.Startup;
public class ApiDocsStartup : IAppStartup, IEndpointStartup
{
private readonly ILogger<ApiDocsStartup> Logger;
private readonly AppConfiguration AppConfiguration;
public ApiDocsStartup(ILogger<ApiDocsStartup> logger, AppConfiguration appConfiguration)
{
Logger = logger;
AppConfiguration = appConfiguration;
}
public Task BuildApp(IHostApplicationBuilder builder)
{
if(!AppConfiguration.Development.EnableApiDocs)
return Task.CompletedTask;
builder.Services.AddEndpointsApiExplorer();
// Configure swagger api specification generator and set the document title for the api docs to use
builder.Services.AddSwaggerGen(options => options.SwaggerDoc("main", new OpenApiInfo()
{
Title = "Moonlight API"
}));
return Task.CompletedTask;
}
public Task ConfigureApp(IApplicationBuilder app) => Task.CompletedTask;
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder)
{
if(!AppConfiguration.Development.EnableApiDocs)
return Task.CompletedTask;
routeBuilder.MapSwagger("/api/swagger/{documentName}");
return Task.CompletedTask;
}
}

View File

@@ -1,24 +0,0 @@
using Moonlight.ApiServer.Interfaces.Startup;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer.Implementations.Startup;
public class CoreAssetStartup : IAppStartup
{
private readonly BundleService BundleService;
public CoreAssetStartup(BundleService bundleService)
{
BundleService = bundleService;
}
public Task BuildApp(IHostApplicationBuilder builder)
{
BundleService.BundleCss("css/core.min.css");
return Task.CompletedTask;
}
public Task ConfigureApp(IApplicationBuilder app)
=> Task.CompletedTask;
}

View File

@@ -1,15 +0,0 @@
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Interfaces.Startup;
namespace Moonlight.ApiServer.Implementations.Startup;
public class CoreDatabaseStartup : IDatabaseStartup
{
public Task ConfigureDatabase(DatabaseContextCollection collection)
{
collection.Add<CoreDataContext>();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,66 @@
using Microsoft.OpenApi.Models;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Interfaces.Startup;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer.Implementations.Startup;
public class CoreStartup : IPluginStartup
{
private readonly AppConfiguration Configuration;
private readonly BundleService BundleService;
public CoreStartup(AppConfiguration configuration, BundleService bundleService)
{
Configuration = configuration;
BundleService = bundleService;
}
public Task BuildApplication(IHostApplicationBuilder builder)
{
#region Api Docs
if (Configuration.Development.EnableApiDocs)
{
builder.Services.AddEndpointsApiExplorer();
// Configure swagger api specification generator and set the document title for the api docs to use
builder.Services.AddSwaggerGen(options => options.SwaggerDoc("main", new OpenApiInfo()
{
Title = "Moonlight API"
}));
}
#endregion
#region Assets
BundleService.BundleCss("css/core.min.css");
#endregion
return Task.CompletedTask;
}
public Task ConfigureApplication(IApplicationBuilder app)
{
return Task.CompletedTask;
}
public Task ConfigureDatabase(DatabaseContextCollection collection)
{
collection.Add<CoreDataContext>();
return Task.CompletedTask;
}
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder)
{
if(Configuration.Development.EnableApiDocs)
routeBuilder.MapSwagger("/api/swagger/{documentName}");
return Task.CompletedTask;
}
}

View File

@@ -1,7 +0,0 @@
namespace Moonlight.ApiServer.Interfaces.Startup;
public interface IAppStartup
{
public Task BuildApp(IHostApplicationBuilder builder);
public Task ConfigureApp(IApplicationBuilder app);
}

View File

@@ -1,8 +0,0 @@
using Moonlight.ApiServer.Helpers;
namespace Moonlight.ApiServer.Interfaces.Startup;
public interface IDatabaseStartup
{
public Task ConfigureDatabase(DatabaseContextCollection collection);
}

View File

@@ -1,6 +0,0 @@
namespace Moonlight.ApiServer.Interfaces.Startup;
public interface IEndpointStartup
{
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder);
}

View File

@@ -0,0 +1,11 @@
using Moonlight.ApiServer.Helpers;
namespace Moonlight.ApiServer.Interfaces.Startup;
public interface IPluginStartup
{
public Task BuildApplication(IHostApplicationBuilder builder);
public Task ConfigureApplication(IApplicationBuilder app);
public Task ConfigureDatabase(DatabaseContextCollection collection);
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder);
}

View File

@@ -24,8 +24,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MoonCore" Version="1.8.2" />
<PackageReference Include="MoonCore.Extended" Version="1.2.7" />
<PackageReference Include="MoonCore" Version="1.8.3" />
<PackageReference Include="MoonCore.Extended" Version="1.2.8" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />

View File

@@ -53,9 +53,7 @@ public class Startup
// Asset bundling
private BundleService BundleService;
private IAppStartup[] PluginAppStartups;
private IDatabaseStartup[] PluginDatabaseStartups;
private IEndpointStartup[] PluginEndpointStartups;
private IPluginStartup[] PluginStartups;
public async Task Run(string[] args, Assembly[]? additionalAssemblies = null)
{
@@ -303,35 +301,53 @@ public class Startup
private Task InitializePlugins()
{
var initialisationServiceCollection = new ServiceCollection();
// Define minimal service collection
var startupSc = new ServiceCollection();
// Configure base services for initialisation
initialisationServiceCollection.AddSingleton(Configuration);
startupSc.AddSingleton(Configuration);
BundleService = new BundleService();
initialisationServiceCollection.AddSingleton(BundleService);
startupSc.AddSingleton(BundleService);
initialisationServiceCollection.AddLogging(builder => { builder.AddProviders(LoggerProviders); });
// Configure plugin loading by using the interface service
initialisationServiceCollection.AddInterfaces(configuration =>
startupSc.AddLogging(builder =>
{
// We use moonlight itself as a plugin assembly
configuration.AddAssembly(typeof(Startup).Assembly);
configuration.AddAssemblies(PluginLoaderService.PluginAssemblies);
configuration.AddAssemblies(AdditionalAssemblies);
configuration.AddInterface<IAppStartup>();
configuration.AddInterface<IDatabaseStartup>();
configuration.AddInterface<IEndpointStartup>();
builder.ClearProviders();
builder.AddProviders(LoggerProviders);
});
//
var startupSp = startupSc.BuildServiceProvider();
// Initialize plugin startups
var startups = new List<IPluginStartup>();
var startupType = typeof(IPluginStartup);
var initialisationServiceProvider = initialisationServiceCollection.BuildServiceProvider();
var assembliesToScan = new List<Assembly>();
assembliesToScan.Add(typeof(Startup).Assembly);
assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies);
assembliesToScan.AddRange(AdditionalAssemblies);
foreach (var pluginAssembly in assembliesToScan)
{
var startupTypes = pluginAssembly
.ExportedTypes
.Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType))
.ToArray();
PluginAppStartups = initialisationServiceProvider.GetRequiredService<IAppStartup[]>();
PluginDatabaseStartups = initialisationServiceProvider.GetRequiredService<IDatabaseStartup[]>();
PluginEndpointStartups = initialisationServiceProvider.GetRequiredService<IEndpointStartup[]>();
foreach (var type in startupTypes)
{
var startup = ActivatorUtilities.CreateInstance(startupSp, type) as IPluginStartup;
if(startup == null)
continue;
startups.Add(startup);
}
}
PluginStartups = startups.ToArray();
return Task.CompletedTask;
}
@@ -364,11 +380,11 @@ public class Startup
private async Task HookPluginBuild()
{
foreach (var pluginAppStartup in PluginAppStartups)
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.BuildApp(WebApplicationBuilder);
await pluginAppStartup.BuildApplication(WebApplicationBuilder);
}
catch (Exception e)
{
@@ -383,11 +399,11 @@ public class Startup
private async Task HookPluginConfigure()
{
foreach (var pluginAppStartup in PluginAppStartups)
foreach (var pluginAppStartup in PluginStartups)
{
try
{
await pluginAppStartup.ConfigureApp(WebApplication);
await pluginAppStartup.ConfigureApplication(WebApplication);
}
catch (Exception e)
{
@@ -402,7 +418,7 @@ public class Startup
private async Task HookPluginEndpoints()
{
foreach (var pluginEndpointStartup in PluginEndpointStartups)
foreach (var pluginEndpointStartup in PluginStartups)
{
try
{
@@ -555,7 +571,7 @@ public class Startup
var databaseCollection = new DatabaseContextCollection();
foreach (var databaseStartup in PluginDatabaseStartups)
foreach (var databaseStartup in PluginStartups)
await databaseStartup.ConfigureDatabase(databaseCollection);
foreach (var database in databaseCollection)

View File

@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Moonlight.Client.Interfaces;
namespace Moonlight.Client.Implementations;
public class CoreStartup : IPluginStartup
{
public Task BuildApplication(WebAssemblyHostBuilder builder)
{
builder.Services.AddSingleton<ISidebarItemProvider, DefaultSidebarItemProvider>();
return Task.CompletedTask;
}
public Task ConfigureApplication(WebAssemblyHost app)
=> Task.CompletedTask;
}

View File

@@ -24,10 +24,10 @@
<PackageReference Include="Blazor-ApexCharts" Version="4.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all"/>
<PackageReference Include="MoonCore" Version="1.8.2" />
<PackageReference Include="MoonCore.Blazor" Version="1.2.8" />
<PackageReference Include="MoonCore" Version="1.8.3" />
<PackageReference Include="MoonCore.Blazor" Version="1.2.9" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.3.0" />
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.3.2" />
</ItemGroup>
<!--

View File

@@ -8,13 +8,12 @@ using MoonCore.Blazor.Tailwind.Extensions;
using MoonCore.Blazor.Tailwind.Auth;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.PluginFramework.Extensions;
using MoonCore.Plugins;
using Moonlight.Client.Implementations;
using Moonlight.Client.Interfaces;
using Moonlight.Client.Services;
using Moonlight.Client.UI;
using Moonlight.Shared.Misc;
using Moonlight.Client.UI;
namespace Moonlight.Client;
@@ -62,7 +61,6 @@ public class Startup
await RegisterLogging();
await RegisterBase();
await RegisterAuthentication();
await RegisterInterfaces();
await HookPluginBuild();
await BuildWebAssemblyHost();
@@ -153,7 +151,9 @@ public class Startup
WebAssemblyHostBuilder.Services.AddMoonCoreBlazorTailwind();
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
WebAssemblyHostBuilder.Services.AutoAddServices<Program>();
WebAssemblyHostBuilder.Services.AddScoped<ThemeService>();
//WebAssemblyHostBuilder.Services.AutoAddServices<Program>();
return Task.CompletedTask;
}
@@ -170,26 +170,6 @@ public class Startup
#endregion
#region Interfaces
private Task RegisterInterfaces()
{
WebAssemblyHostBuilder.Services.AddInterfaces(configuration =>
{
// We use moonlight itself as a plugin assembly
configuration.AddAssembly(typeof(Startup).Assembly);
configuration.AddAssemblies(ApplicationAssemblyService.AdditionalAssemblies);
configuration.AddAssemblies(ApplicationAssemblyService.PluginAssemblies);
configuration.AddInterface<ISidebarItemProvider>();
});
return Task.CompletedTask;
}
#endregion
#region Plugins
private async Task LoadPlugins()
@@ -240,12 +220,18 @@ public class Startup
// Initialize plugin startups
var startups = new List<IPluginStartup>();
var startupType = typeof(IPluginStartup);
var assembliesToScan = new List<Assembly>();
assembliesToScan.Add(typeof(Startup).Assembly);
assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies);
assembliesToScan.AddRange(ApplicationAssemblyService.AdditionalAssemblies);
foreach (var pluginAssembly in ApplicationAssemblyService.PluginAssemblies)
foreach (var pluginAssembly in assembliesToScan)
{
var startupTypes = pluginAssembly
.ExportedTypes
.Where(x => x.IsAbstract && x.IsInterface && x.IsAssignableTo(startupType))
.Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType))
.ToArray();
foreach (var type in startupTypes)

View File

@@ -1,5 +1,4 @@
@using Moonlight.Client.UI.Layouts
@using MoonCore.Blazor.Components
@using Moonlight.Client.Services
@inject ApplicationAssemblyService ApplicationAssemblyService

View File

@@ -4,7 +4,7 @@
@using Moonlight.Client.UI.Layouts
@inject NavigationManager Navigation
@inject ISidebarItemProvider[] SidebarItemProviders
@inject IEnumerable<ISidebarItemProvider> SidebarItemProviders
@{
var url = new Uri(Navigation.Uri);