Merge branch 'v2_ChangeArchitecture' into v2_ChangeArchitecture_AddDiagnose
This commit is contained in:
@@ -44,6 +44,8 @@ public class AppConfiguration
|
||||
public string? AuthorizationEndpoint { get; set; }
|
||||
public string? AccessEndpoint { get; set; }
|
||||
public string? AuthorizationRedirect { get; set; }
|
||||
|
||||
public bool FirstUserAdmin { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,5 +57,6 @@ public class AppConfiguration
|
||||
public class KestrelConfig
|
||||
{
|
||||
public int UploadLimit { get; set; } = 100;
|
||||
public string AllowedOrigins { get; set; } = "*";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
# Prepare runtime docker image
|
||||
#
|
||||
# OUTDATED
|
||||
# Use https://github.com/Moonlight-Panel/Deploy
|
||||
#
|
||||
|
||||
# Prepare runtime docker image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled AS base
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.FileProviders.Physical;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using MoonCore.Helpers;
|
||||
|
||||
namespace Moonlight.ApiServer.Helpers;
|
||||
|
||||
public class BundleAssetFileProvider : IFileProvider
|
||||
{
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
=> NotFoundDirectoryContents.Singleton;
|
||||
|
||||
public IFileInfo GetFileInfo(string subpath)
|
||||
{
|
||||
if(subpath != "/css/bundle.css")
|
||||
return new NotFoundFileInfo(subpath);
|
||||
|
||||
var physicalPath = PathBuilder.File("storage", "tmp", "bundle.css");
|
||||
|
||||
if(!File.Exists(physicalPath))
|
||||
return new NotFoundFileInfo(subpath);
|
||||
|
||||
return new PhysicalFileInfo(new FileInfo(physicalPath));
|
||||
}
|
||||
|
||||
public IChangeToken Watch(string filter)
|
||||
=> NullChangeToken.Singleton;
|
||||
}
|
||||
@@ -12,31 +12,13 @@ namespace Moonlight.ApiServer.Http.Controllers;
|
||||
public class FrontendController : Controller
|
||||
{
|
||||
private readonly FrontendService FrontendService;
|
||||
private readonly PluginService PluginService;
|
||||
|
||||
public FrontendController(FrontendService frontendService, PluginService pluginService)
|
||||
public FrontendController(FrontendService frontendService)
|
||||
{
|
||||
FrontendService = frontendService;
|
||||
PluginService = pluginService;
|
||||
}
|
||||
|
||||
[HttpGet("frontend.json")]
|
||||
public async Task<FrontendConfiguration> GetConfiguration()
|
||||
=> await FrontendService.GetConfiguration();
|
||||
|
||||
[HttpGet("plugins/{assemblyName}")]
|
||||
public async Task GetPluginAssembly(string assemblyName)
|
||||
{
|
||||
var assembliesMap = PluginService.GetAssemblies("client");
|
||||
|
||||
if (!assembliesMap.TryGetValue(assemblyName, out var path))
|
||||
throw new HttpApiException("The requested assembly could not be found", 404);
|
||||
|
||||
var absolutePath = Path.Combine(
|
||||
Directory.GetCurrentDirectory(),
|
||||
path
|
||||
);
|
||||
|
||||
await Results.File(absolutePath).ExecuteAsync(HttpContext);
|
||||
}
|
||||
}
|
||||
@@ -273,13 +273,22 @@ public class OAuth2Controller : Controller
|
||||
|
||||
if (await UserRepository.Get().AnyAsync(x => x.Email == email))
|
||||
throw new HttpApiException("A account with that email already exists", 400);
|
||||
|
||||
|
||||
var user = new User()
|
||||
{
|
||||
Username = username,
|
||||
Email = email,
|
||||
Password = HashHelper.Hash(password)
|
||||
Password = HashHelper.Hash(password),
|
||||
};
|
||||
|
||||
if (Configuration.Authentication.OAuth2.FirstUserAdmin)
|
||||
{
|
||||
var userCount = await UserRepository.Get().CountAsync();
|
||||
|
||||
if (userCount == 0)
|
||||
user.PermissionsJson = "[\"*\"]";
|
||||
|
||||
}
|
||||
|
||||
return await UserRepository.Add(user);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<form class="space-y-6" method="POST">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium leading-6 text-gray-100">Username</label>
|
||||
<label for="username" class="block text-sm font-medium leading-6 text-gray-100">Username</label>
|
||||
<div class="mt-2">
|
||||
<input id="username" name="username" type="text" required class="block bg-white/5 w-full rounded-md border-0 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-700 placeholder:text-gray-600 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
|
||||
@@ -3,24 +3,20 @@ using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Database;
|
||||
using Moonlight.ApiServer.Implementations.Diagnose;
|
||||
using Moonlight.ApiServer.Interfaces;
|
||||
using Moonlight.ApiServer.Interfaces.Startup;
|
||||
using Moonlight.ApiServer.Plugins;
|
||||
|
||||
namespace Moonlight.ApiServer.Implementations.Startup;
|
||||
|
||||
[PluginStartup]
|
||||
public class CoreStartup : IPluginStartup
|
||||
{
|
||||
private readonly AppConfiguration Configuration;
|
||||
|
||||
public CoreStartup(AppConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public Task BuildApplication(IHostApplicationBuilder builder)
|
||||
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
|
||||
{
|
||||
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
|
||||
|
||||
#region Api Docs
|
||||
|
||||
if (Configuration.Development.EnableApiDocs)
|
||||
if (configuration.Development.EnableApiDocs)
|
||||
{
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
@@ -62,14 +58,16 @@ public class CoreStartup : IPluginStartup
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ConfigureApplication(IApplicationBuilder app)
|
||||
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder)
|
||||
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder)
|
||||
{
|
||||
if(Configuration.Development.EnableApiDocs)
|
||||
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
|
||||
|
||||
if(configuration.Development.EnableApiDocs)
|
||||
routeBuilder.MapSwagger("/api/swagger/{documentName}");
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Moonlight.ApiServer.Interfaces.Startup;
|
||||
|
||||
public interface IPluginStartup
|
||||
{
|
||||
public Task BuildApplication(IHostApplicationBuilder builder);
|
||||
public Task ConfigureApplication(IApplicationBuilder app);
|
||||
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.ApiServer.Models;
|
||||
|
||||
public class FrontendConfigurationOption
|
||||
{
|
||||
public string[] Scripts { get; set; } = [];
|
||||
public string[] Styles { get; set; } = [];
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Moonlight.ApiServer.Models;
|
||||
|
||||
public class PluginManifest
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string[] Dependencies { get; set; } = [];
|
||||
|
||||
public string[] Scripts { get; set; } = [];
|
||||
public string[] Styles { get; set; } = [];
|
||||
|
||||
public Dictionary<string, string[]> Assemblies { get; set; } = new();
|
||||
}
|
||||
@@ -1,70 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj"/>
|
||||
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Database\Migrations\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>Moonlight.ApiServer</PackageId>
|
||||
<Version>2.1.0</Version>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>A build of the api server for moonlight development</Description>
|
||||
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
|
||||
<DevelopmentDependency>true</DevelopmentDependency>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ExCSS" Version="4.3.0"/>
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18"/>
|
||||
<PackageReference Include="Hangfire.Core" Version="1.8.18"/>
|
||||
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.10"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"/>
|
||||
<PackageReference Include="MoonCore" Version="1.8.5"/>
|
||||
<PackageReference Include="MoonCore.Extended" Version="1.3.2"/>
|
||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>src</PackagePath>
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="**\*.razor" Exclude="storage\**\*;bin\**\*;obj\**\*">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>src</PackagePath>
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Compile Remove="storage\**\*" />
|
||||
<Content Remove="storage\**\*" />
|
||||
<None Remove="storage\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj" />
|
||||
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Database\Migrations\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>Moonlight.ApiServer</PackageId>
|
||||
<Version>2.1.0</Version>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>A build of the api server for moonlight development</Description>
|
||||
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
|
||||
<DevelopmentDependency>true</DevelopmentDependency>
|
||||
<PackageTags>apiserver</PackageTags>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
|
||||
<PackageReference Include="Hangfire.Core" Version="1.8.18" />
|
||||
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
|
||||
<PackageReference Include="MoonCore" Version="1.8.6" />
|
||||
<PackageReference Include="MoonCore.Extended" Version="1.3.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>src</PackagePath>
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="**\*.razor" Exclude="storage\**\*;bin\**\*;obj\**\*">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>src</PackagePath>
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Compile Remove="storage\**\*" />
|
||||
<Content Remove="storage\**\*" />
|
||||
<None Remove="storage\**\*" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
8
Moonlight.ApiServer/Plugins/IPluginStartup.cs
Normal file
8
Moonlight.ApiServer/Plugins/IPluginStartup.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Moonlight.ApiServer.Plugins;
|
||||
|
||||
public interface IPluginStartup
|
||||
{
|
||||
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder);
|
||||
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app);
|
||||
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder);
|
||||
}
|
||||
7
Moonlight.ApiServer/Plugins/PluginStartupAttribute.cs
Normal file
7
Moonlight.ApiServer/Plugins/PluginStartupAttribute.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.ApiServer.Plugins;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PluginStartupAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using MoonCore.Attributes;
|
||||
using MoonCore.Exceptions;
|
||||
using MoonCore.Helpers;
|
||||
using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Models;
|
||||
using Moonlight.Shared.Misc;
|
||||
|
||||
namespace Moonlight.ApiServer.Services;
|
||||
@@ -14,18 +15,18 @@ namespace Moonlight.ApiServer.Services;
|
||||
public class FrontendService
|
||||
{
|
||||
private readonly AppConfiguration Configuration;
|
||||
private readonly PluginService PluginService;
|
||||
private readonly IWebHostEnvironment WebHostEnvironment;
|
||||
private readonly IEnumerable<FrontendConfigurationOption> ConfigurationOptions;
|
||||
|
||||
public FrontendService(
|
||||
AppConfiguration configuration,
|
||||
PluginService pluginService,
|
||||
IWebHostEnvironment webHostEnvironment
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IEnumerable<FrontendConfigurationOption> configurationOptions
|
||||
)
|
||||
{
|
||||
Configuration = configuration;
|
||||
PluginService = pluginService;
|
||||
WebHostEnvironment = webHostEnvironment;
|
||||
ConfigurationOptions = configurationOptions;
|
||||
}
|
||||
|
||||
public async Task<FrontendConfiguration> GetConfiguration()
|
||||
@@ -48,33 +49,15 @@ public class FrontendService
|
||||
.Deserialize<Dictionary<string, string>>(variablesJson) ?? new();
|
||||
}
|
||||
|
||||
// Collect assemblies for the 'client' section
|
||||
configuration.Assemblies = PluginService
|
||||
.GetAssemblies("client")
|
||||
.Keys
|
||||
.ToArray();
|
||||
|
||||
// Collect scripts to execute
|
||||
configuration.Scripts = PluginService
|
||||
.LoadedPlugins
|
||||
.Keys
|
||||
configuration.Scripts = ConfigurationOptions
|
||||
.SelectMany(x => x.Scripts)
|
||||
.ToArray();
|
||||
|
||||
// Collect styles
|
||||
var styles = new List<string>();
|
||||
|
||||
styles.AddRange(
|
||||
PluginService
|
||||
.LoadedPlugins
|
||||
.Keys
|
||||
.SelectMany(x => x.Styles)
|
||||
);
|
||||
|
||||
// Add bundle css
|
||||
styles.Add("css/bundle.min.css");
|
||||
|
||||
configuration.Styles = styles.ToArray();
|
||||
configuration.Styles = ConfigurationOptions
|
||||
.SelectMany(x => x.Styles)
|
||||
.ToArray();
|
||||
|
||||
return configuration;
|
||||
}
|
||||
@@ -111,42 +94,12 @@ public class FrontendService
|
||||
// Add blazor files
|
||||
await ArchiveFsItem(zipArchive, blazorPath, blazorPath, "_framework/");
|
||||
|
||||
// Add bundle.css
|
||||
var bundleContent = await File.ReadAllBytesAsync(Path.Combine("storage", "tmp", "bundle.css"));
|
||||
await ArchiveBytes(zipArchive, "css/bundle.css", bundleContent);
|
||||
|
||||
// Add frontend.json
|
||||
var frontendConfig = await GetConfiguration();
|
||||
frontendConfig.HostEnvironment = "Static";
|
||||
var frontendJson = JsonSerializer.Serialize(frontendConfig);
|
||||
await ArchiveText(zipArchive, "frontend.json", frontendJson);
|
||||
|
||||
// Add plugin wwwroot files
|
||||
foreach (var pluginPath in PluginService.LoadedPlugins.Values)
|
||||
{
|
||||
var wwwRootPluginPath = Path.Combine(pluginPath, "wwwroot/");
|
||||
|
||||
if (!Directory.Exists(wwwRootPluginPath))
|
||||
continue;
|
||||
|
||||
await ArchiveFsItem(zipArchive, wwwRootPluginPath, wwwRootPluginPath);
|
||||
}
|
||||
|
||||
// Add plugin assemblies for client to the zip file
|
||||
var assembliesMap = PluginService.GetAssemblies("client");
|
||||
|
||||
foreach (var assemblyName in assembliesMap.Keys)
|
||||
{
|
||||
var path = assembliesMap[assemblyName];
|
||||
|
||||
await ArchiveFsItem(
|
||||
zipArchive,
|
||||
path,
|
||||
path,
|
||||
$"plugins/{assemblyName}"
|
||||
);
|
||||
}
|
||||
|
||||
// Finish zip archive and reset stream so the code calling this function can process it
|
||||
zipArchive.Dispose();
|
||||
await memoryStream.FlushAsync();
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using MoonCore.Helpers;
|
||||
using Moonlight.ApiServer.Models;
|
||||
|
||||
namespace Moonlight.ApiServer.Services;
|
||||
|
||||
public class PluginService
|
||||
{
|
||||
private readonly ILogger<PluginService> Logger;
|
||||
private readonly string PluginRoot;
|
||||
|
||||
public readonly Dictionary<PluginManifest, string> LoadedPlugins = new();
|
||||
public IFileProvider WwwRootFileProvider;
|
||||
|
||||
public PluginService(ILogger<PluginService> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
|
||||
PluginRoot = PathBuilder.Dir("storage", "plugins");
|
||||
}
|
||||
|
||||
public async Task Load()
|
||||
{
|
||||
var jsonOptions = new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
var pluginDirs = Directory.GetDirectories(PluginRoot);
|
||||
var pluginMap = new Dictionary<PluginManifest, string>();
|
||||
|
||||
#region Scan plugins/ directory for plugin.json files
|
||||
|
||||
foreach (var dir in pluginDirs)
|
||||
{
|
||||
var metaPath = PathBuilder.File(dir, "plugin.json");
|
||||
|
||||
if (!File.Exists(metaPath))
|
||||
{
|
||||
Logger.LogWarning("Skipped '{dir}' as it is missing a plugin.json", dir);
|
||||
continue;
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(metaPath);
|
||||
|
||||
try
|
||||
{
|
||||
var meta = JsonSerializer.Deserialize<PluginManifest>(json, jsonOptions);
|
||||
|
||||
if (meta == null)
|
||||
throw new JsonException("Unable to parse. Return value was null");
|
||||
|
||||
pluginMap.Add(meta, dir);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
Logger.LogError("Unable to load plugin.json at '{path}': {e}", metaPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Depdenency check
|
||||
|
||||
foreach (var plugin in pluginMap.Keys)
|
||||
{
|
||||
var hasMissingDep = false;
|
||||
|
||||
foreach (var dependency in plugin.Dependencies)
|
||||
{
|
||||
if (pluginMap.Keys.All(x => x.Id != dependency))
|
||||
{
|
||||
hasMissingDep = true;
|
||||
Logger.LogWarning("Plugin '{name}' has missing dependency: {dep}", plugin.Name, dependency);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMissingDep)
|
||||
Logger.LogWarning("Unable to load '{name}' due to missing dependencies", plugin.Name);
|
||||
else
|
||||
LoadedPlugins.Add(plugin, pluginMap[plugin]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Create wwwroot file provider
|
||||
|
||||
Logger.LogInformation("Creating wwwroot file provider");
|
||||
WwwRootFileProvider = CreateWwwRootProvider();
|
||||
|
||||
#endregion
|
||||
|
||||
Logger.LogInformation("Loaded {count} plugins", LoadedPlugins.Count);
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetAssemblies(string section)
|
||||
{
|
||||
var assemblyMap = new Dictionary<string, string>();
|
||||
|
||||
foreach (var loadedPlugin in LoadedPlugins.Keys)
|
||||
{
|
||||
// Skip all plugins which haven't defined any assemblies in that section
|
||||
if (!loadedPlugin.Assemblies.ContainsKey(section))
|
||||
continue;
|
||||
|
||||
var pluginPath = LoadedPlugins[loadedPlugin];
|
||||
|
||||
foreach (var assembly in loadedPlugin.Assemblies[section])
|
||||
{
|
||||
var assemblyFile = Path.GetFileName(assembly);
|
||||
assemblyMap[assemblyFile] = PathBuilder.File(pluginPath, assembly);
|
||||
}
|
||||
}
|
||||
|
||||
return assemblyMap;
|
||||
}
|
||||
|
||||
private IFileProvider CreateWwwRootProvider()
|
||||
{
|
||||
List<IFileProvider> wwwRootProviders = new();
|
||||
|
||||
foreach (var pluginFolder in LoadedPlugins.Values)
|
||||
{
|
||||
var wwwRootPath = Path.GetFullPath(
|
||||
PathBuilder.Dir(pluginFolder, "wwwroot")
|
||||
);
|
||||
|
||||
if(!Directory.Exists(wwwRootPath))
|
||||
continue;
|
||||
|
||||
wwwRootProviders.Add(
|
||||
new PhysicalFileProvider(wwwRootPath)
|
||||
);
|
||||
}
|
||||
|
||||
return new CompositeFileProvider(wwwRootProviders);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Hangfire;
|
||||
using Hangfire.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MoonCore.Configuration;
|
||||
using MoonCore.EnvConfiguration;
|
||||
using MoonCore.Extended.Abstractions;
|
||||
using MoonCore.Extended.Extensions;
|
||||
@@ -15,16 +12,13 @@ using MoonCore.Extended.Helpers;
|
||||
using MoonCore.Extended.JwtInvalidation;
|
||||
using MoonCore.Extensions;
|
||||
using MoonCore.Helpers;
|
||||
using MoonCore.Services;
|
||||
using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Database;
|
||||
using Moonlight.ApiServer.Database.Entities;
|
||||
using Moonlight.ApiServer.Helpers;
|
||||
using Moonlight.ApiServer.Implementations;
|
||||
using Moonlight.ApiServer.Implementations.Startup;
|
||||
using Moonlight.ApiServer.Interfaces;
|
||||
using Moonlight.ApiServer.Interfaces.Startup;
|
||||
using Moonlight.ApiServer.Models;
|
||||
using Moonlight.ApiServer.Services;
|
||||
using Moonlight.ApiServer.Plugins;
|
||||
|
||||
namespace Moonlight.ApiServer;
|
||||
|
||||
@@ -34,8 +28,6 @@ namespace Moonlight.ApiServer;
|
||||
public class Startup
|
||||
{
|
||||
private string[] Args;
|
||||
private Assembly[] AdditionalAssemblies;
|
||||
private PluginManifest[] AdditionalPluginManifests;
|
||||
|
||||
// Logging
|
||||
private ILoggerProvider[] LoggerProviders;
|
||||
@@ -51,24 +43,20 @@ public class Startup
|
||||
private WebApplicationBuilder WebApplicationBuilder;
|
||||
|
||||
// Plugin Loading
|
||||
private PluginService PluginService;
|
||||
private AssemblyLoadContext PluginLoadContext;
|
||||
|
||||
private IPluginStartup[] PluginStartups;
|
||||
private IPluginStartup[] AdditionalPlugins;
|
||||
private IServiceProvider PluginLoadServiceProvider;
|
||||
|
||||
public async Task Run(string[] args, Assembly[]? additionalAssemblies = null,
|
||||
PluginManifest[]? additionalManifests = null)
|
||||
public async Task Run(string[] args, IPluginStartup[]? additionalPlugins = null)
|
||||
{
|
||||
Args = args;
|
||||
AdditionalAssemblies = additionalAssemblies ?? [];
|
||||
AdditionalPluginManifests = additionalManifests ?? [];
|
||||
AdditionalPlugins = additionalPlugins ?? [];
|
||||
|
||||
await PrintVersion();
|
||||
|
||||
await CreateStorage();
|
||||
await SetupAppConfiguration();
|
||||
await SetupLogging();
|
||||
await LoadPlugins();
|
||||
await InitializePlugins();
|
||||
|
||||
await CreateWebApplicationBuilder();
|
||||
@@ -89,7 +77,6 @@ public class Startup
|
||||
await PrepareDatabase();
|
||||
|
||||
await UseCors();
|
||||
await UsePluginAssets(); // We need to move the plugin assets to the top to allow plugins to override content
|
||||
await UseBase();
|
||||
await UseAuth();
|
||||
await UseHangfire();
|
||||
@@ -140,17 +127,13 @@ public class Startup
|
||||
|
||||
// Add pre-existing services
|
||||
WebApplicationBuilder.Services.AddSingleton(Configuration);
|
||||
WebApplicationBuilder.Services.AddSingleton(PluginService);
|
||||
|
||||
// Configure controllers
|
||||
var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
|
||||
|
||||
// Add plugin and additional assemblies as application parts
|
||||
foreach (var pluginAssembly in PluginLoadContext.Assemblies)
|
||||
mvcBuilder.AddApplicationPart(pluginAssembly);
|
||||
|
||||
foreach (var additionalAssembly in AdditionalAssemblies)
|
||||
mvcBuilder.AddApplicationPart(additionalAssembly);
|
||||
// Add plugin assemblies as application parts
|
||||
foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct())
|
||||
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -200,89 +183,33 @@ public class Startup
|
||||
|
||||
#region Plugin Loading
|
||||
|
||||
private async Task LoadPlugins()
|
||||
{
|
||||
// Load plugins
|
||||
PluginService = new PluginService(
|
||||
LoggerFactory.CreateLogger<PluginService>()
|
||||
);
|
||||
|
||||
// Add plugins manually if specified in the startup
|
||||
foreach (var manifest in AdditionalPluginManifests)
|
||||
PluginService.LoadedPlugins.Add(manifest, Directory.GetCurrentDirectory());
|
||||
|
||||
// Search and load all plugins
|
||||
await PluginService.Load();
|
||||
|
||||
// Search up assemblies for the apiServer
|
||||
var assemblyFiles = PluginService.GetAssemblies("apiServer")
|
||||
.Values
|
||||
.ToArray();
|
||||
|
||||
// Create the load context and add assemblies
|
||||
PluginLoadContext = new AssemblyLoadContext(null);
|
||||
|
||||
foreach (var assemblyFile in assemblyFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
PluginLoadContext.LoadFromAssemblyPath(
|
||||
Path.Combine(Directory.GetCurrentDirectory(), assemblyFile)
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("Unable to load plugin assembly '{assemblyFile}': {e}", assemblyFile, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task InitializePlugins()
|
||||
{
|
||||
// Define minimal service collection
|
||||
var startupSc = new ServiceCollection();
|
||||
// Create service provider for starting up
|
||||
var serviceCollection = new ServiceCollection();
|
||||
|
||||
// Configure base services for initialisation
|
||||
startupSc.AddSingleton(Configuration);
|
||||
serviceCollection.AddSingleton(Configuration);
|
||||
|
||||
startupSc.AddLogging(builder =>
|
||||
serviceCollection.AddLogging(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddProviders(LoggerProviders);
|
||||
});
|
||||
|
||||
//
|
||||
var startupSp = startupSc.BuildServiceProvider();
|
||||
PluginLoadServiceProvider = serviceCollection.BuildServiceProvider();
|
||||
|
||||
// Initialize plugin startups
|
||||
var startups = new List<IPluginStartup>();
|
||||
var startupType = typeof(IPluginStartup);
|
||||
// Collect startups
|
||||
var pluginStartups = new List<IPluginStartup>();
|
||||
|
||||
var assembliesToScan = new List<Assembly>();
|
||||
pluginStartups.Add(new CoreStartup());
|
||||
|
||||
assembliesToScan.Add(typeof(Startup).Assembly);
|
||||
assembliesToScan.AddRange(PluginLoadContext.Assemblies);
|
||||
assembliesToScan.AddRange(AdditionalAssemblies);
|
||||
pluginStartups.AddRange(AdditionalPlugins); // Used by the development server
|
||||
|
||||
foreach (var pluginAssembly in assembliesToScan)
|
||||
{
|
||||
var startupTypes = pluginAssembly
|
||||
.ExportedTypes
|
||||
.Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType))
|
||||
.ToArray();
|
||||
// Do NOT remove the following comment, as its used to place the plugin startup register calls
|
||||
// MLBUILD_PLUGIN_STARTUP_HERE
|
||||
|
||||
foreach (var type in startupTypes)
|
||||
{
|
||||
var startup = ActivatorUtilities.CreateInstance(startupSp, type) as IPluginStartup;
|
||||
|
||||
if (startup == null)
|
||||
continue;
|
||||
|
||||
startups.Add(startup);
|
||||
}
|
||||
}
|
||||
|
||||
PluginStartups = startups.ToArray();
|
||||
PluginStartups = pluginStartups.ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -292,21 +219,6 @@ public class Startup
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task UsePluginAssets()
|
||||
{
|
||||
WebApplication.UseStaticFiles(new StaticFileOptions()
|
||||
{
|
||||
FileProvider = new BundleAssetFileProvider()
|
||||
});
|
||||
|
||||
WebApplication.UseStaticFiles(new StaticFileOptions()
|
||||
{
|
||||
FileProvider = PluginService.WwwRootFileProvider
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#region Hooks
|
||||
|
||||
private async Task HookPluginBuild()
|
||||
@@ -315,7 +227,7 @@ public class Startup
|
||||
{
|
||||
try
|
||||
{
|
||||
await pluginAppStartup.BuildApplication(WebApplicationBuilder);
|
||||
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -334,7 +246,7 @@ public class Startup
|
||||
{
|
||||
try
|
||||
{
|
||||
await pluginAppStartup.ConfigureApplication(WebApplication);
|
||||
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -353,7 +265,7 @@ public class Startup
|
||||
{
|
||||
try
|
||||
{
|
||||
await pluginEndpointStartup.ConfigureEndpoints(WebApplication);
|
||||
await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -568,7 +480,7 @@ public class Startup
|
||||
});
|
||||
|
||||
WebApplicationBuilder.Services.AddAuthorization();
|
||||
|
||||
|
||||
// Add local oauth2 provider if enabled
|
||||
if (Configuration.Authentication.EnableLocalOAuth2)
|
||||
WebApplicationBuilder.Services.AddScoped<IOAuth2Provider, LocalOAuth2Provider>();
|
||||
@@ -593,12 +505,30 @@ public class Startup
|
||||
|
||||
private Task RegisterCors()
|
||||
{
|
||||
var allowedOrigins = Configuration.Kestrel.AllowedOrigins.Split(";", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
WebApplicationBuilder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(builder =>
|
||||
var cors = new CorsPolicyBuilder();
|
||||
|
||||
if (allowedOrigins.Contains("*"))
|
||||
{
|
||||
builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().Build();
|
||||
});
|
||||
cors.SetIsOriginAllowed(_ => true)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
}
|
||||
else
|
||||
{
|
||||
cors.WithOrigins(allowedOrigins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
}
|
||||
|
||||
options.AddDefaultPolicy(
|
||||
cors.Build()
|
||||
);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
||||
Reference in New Issue
Block a user