Started implementing modular payment gateways. Implemented a basic plugin system

TODO: Add capability for plugins to modify the kestrel pipeline
This commit is contained in:
Baumgartner Marcel 2023-10-23 17:04:36 +02:00
parent ff9bcc6433
commit 863a002370
8 changed files with 200 additions and 3 deletions

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Models.Abstractions;
public abstract class PaymentGateway
{
public abstract string Name { get; }
public abstract string Icon { get; }
public abstract Task<string> Start(double price);
}

View file

@ -0,0 +1,10 @@
namespace Moonlight.App.Plugins.Contexts;
public class PluginContext
{
public IServiceCollection Services { get; set; }
public IServiceProvider Provider { get; set; }
public IServiceScope Scope { get; set; }
public List<Action> PreInitTasks = new();
public List<Action> PostInitTasks = new();
}

View file

@ -0,0 +1,10 @@
using Moonlight.App.Plugins.Contexts;
namespace Moonlight.App.Plugins;
public abstract class MoonlightPlugin
{
public PluginContext Context { get; set; }
public abstract Task Enable();
public abstract Task Disable();
}

View file

@ -0,0 +1,120 @@
using System.Reflection;
using Moonlight.App.Helpers;
using Moonlight.App.Plugins;
using Moonlight.App.Plugins.Contexts;
namespace Moonlight.App.Services;
public class PluginService
{
private readonly List<MoonlightPlugin> Plugins = new();
public async Task Load(IServiceCollection services)
{
var path = PathBuilder.Dir("storage", "plugins");
Directory.CreateDirectory(path);
var files = FindFiles(path)
.Where(x => x.EndsWith(".dll"))
.ToArray();
foreach (var file in files)
{
try
{
var assembly = Assembly.LoadFile(PathBuilder.File(Directory.GetCurrentDirectory(), file));
int plugins = 0;
foreach (var type in assembly.GetTypes())
{
if (type.IsSubclassOf(typeof(MoonlightPlugin)))
{
try
{
var plugin = (Activator.CreateInstance(type) as MoonlightPlugin)!;
// Create environment
plugin.Context = new PluginContext()
{
Services = services
};
try
{
await plugin.Enable();
// After here we can treat the plugin as successfully loaded
plugins++;
Plugins.Add(plugin);
}
catch (Exception e)
{
Logger.Fatal($"Unhandled exception while enabling plugin '{type.Name}'");
Logger.Fatal(e);
}
}
catch (Exception e)
{
Logger.Fatal($"Failed to create plugin environment for '{type.Name}'");
Logger.Fatal(e);
}
}
}
if(plugins == 0) // If 0, we can assume that it was a library dll
Logger.Info($"Loaded {file} as a library");
else
Logger.Info($"Loaded {plugins} plugin(s) from {file}");
}
catch (Exception e)
{
Logger.Fatal($"Unable to load assembly from file '{file}'");
Logger.Fatal(e);
}
}
Logger.Info($"Loaded {Plugins.Count} plugin(s)");
}
public async Task RunPreInit()
{
foreach (var plugin in Plugins)
{
Logger.Info($"Running pre init tasks for {plugin.GetType().Name}");
foreach (var preInitTask in plugin.Context.PreInitTasks)
await Task.Run(preInitTask);
}
}
public async Task RunPrePost(IServiceProvider provider)
{
foreach (var plugin in Plugins)
{
// Pass through the dependency injection
var scope = provider.CreateScope();
plugin.Context.Provider = scope.ServiceProvider;
plugin.Context.Scope = scope;
Logger.Info($"Running post init tasks for {plugin.GetType().Name}");
foreach (var postInitTask in plugin.Context.PostInitTasks)
await Task.Run(postInitTask);
}
}
private string[] FindFiles(string dir)
{
var result = new List<string>();
foreach (var file in Directory.GetFiles(dir))
result.Add(file);
foreach (var directory in Directory.GetDirectories(dir))
{
result.AddRange(FindFiles(directory));
}
return result.ToArray();
}
}

View file

@ -1,4 +1,6 @@
namespace Moonlight.App.Services.Store; using Moonlight.App.Models.Abstractions;
namespace Moonlight.App.Services.Store;
public class StoreService public class StoreService
{ {
@ -6,9 +8,16 @@ public class StoreService
public StoreAdminService Admin => ServiceProvider.GetRequiredService<StoreAdminService>(); public StoreAdminService Admin => ServiceProvider.GetRequiredService<StoreAdminService>();
public StoreOrderService Order => ServiceProvider.GetRequiredService<StoreOrderService>(); public StoreOrderService Order => ServiceProvider.GetRequiredService<StoreOrderService>();
public readonly List<PaymentGateway> Gateways = new();
public StoreService(IServiceProvider serviceProvider) public StoreService(IServiceProvider serviceProvider)
{ {
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
} }
public Task RegisterGateway(PaymentGateway gateway)
{
Gateways.Add(gateway);
return Task.CompletedTask;
}
} }

View file

@ -18,6 +18,8 @@
<Folder Include="App\Http\Middleware\" /> <Folder Include="App\Http\Middleware\" />
<Folder Include="App\Http\Requests\" /> <Folder Include="App\Http\Requests\" />
<Folder Include="App\Http\Resources\" /> <Folder Include="App\Http\Resources\" />
<Folder Include="storage\logs\" />
<Folder Include="storage\mail\" />
<Folder Include="wwwroot\img\" /> <Folder Include="wwwroot\img\" />
</ItemGroup> </ItemGroup>
@ -44,4 +46,8 @@
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0-dev-00923" /> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.0-dev-00923" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="storage\config.json" />
</ItemGroup>
</Project> </Project>

View file

@ -29,6 +29,13 @@ Log.Logger = logConfig.CreateLogger();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Init plugin system
var pluginService = new PluginService();
builder.Services.AddSingleton(pluginService);
await pluginService.Load(builder.Services);
await pluginService.RunPreInit();
builder.Services.AddDbContext<DataContext>(); builder.Services.AddDbContext<DataContext>();
// Repositories // Repositories
@ -44,7 +51,7 @@ builder.Services.AddScoped<ModalService>();
builder.Services.AddScoped<AlertService>(); builder.Services.AddScoped<AlertService>();
// Services / Store // Services / Store
builder.Services.AddScoped<StoreService>(); builder.Services.AddSingleton<StoreService>();
builder.Services.AddScoped<StoreAdminService>(); builder.Services.AddScoped<StoreAdminService>();
builder.Services.AddScoped<StoreOrderService>(); builder.Services.AddScoped<StoreOrderService>();
builder.Services.AddScoped<TransactionService>(); builder.Services.AddScoped<TransactionService>();
@ -97,4 +104,6 @@ app.Services.GetRequiredService<AutoMailSendService>();
var serviceService = app.Services.GetRequiredService<ServiceAdminService>(); var serviceService = app.Services.GetRequiredService<ServiceAdminService>();
await serviceService.RegisterAction(ServiceType.Server, new DummyActions()); await serviceService.RegisterAction(ServiceType.Server, new DummyActions());
await pluginService.RunPrePost(app.Services);
app.Run(); app.Run();

View file

@ -3,11 +3,36 @@
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.Database.Entities.Store @using Moonlight.App.Database.Entities.Store
@using BlazorTable @using BlazorTable
@using Moonlight.App.Services.Store
@inject IdentityService IdentityService @inject IdentityService IdentityService
@inject ConfigService ConfigService @inject ConfigService ConfigService
@inject StoreService StoreService
<AccountNavigation Index="2" /> <AccountNavigation Index="2"/>
<div class="row mt-5">
<div class="col-md-6 col-12">
<div class="card card-body">
</div>
</div>
<div class="col-md-6 col-12">
<div class="card card-body">
<div class="row">
@foreach (var gateway in StoreService.Gateways)
{
<div class="col-md-4 col-12">
<a @onclick:preventDefault href="#" class="card card-body bg-hover-info text-center border border-info">
<i class="@(gateway.Icon)"></i>
<h4 class="fw-bold mb-0 align-middle">@(gateway.Name)</h4>
</a>
</div>
}
</div>
</div>
</div>
</div>
<div class="card mt-5"> <div class="card mt-5">
<div class="card-header"> <div class="card-header">