Adds SlpModularCms.Api.Slave for local master/slave dev testing (Unit 1)
Relocates ModuleOrchestrator, ServiceCollectionExtensions, and ApiPrefixConvention from SlpModularCms.Api into SlpModularCms.Core.Hosting so a new Master-less SlpModularCms.Api.Slave host project (ports 5285/7222) can share the same bootstrap code without duplicating it. This lets a developer run a master instance and a slave instance side by side locally to test the master/slave connection, without touching the existing master/slave protocol itself. Relocates the two orchestrator/convention test files from Modules.Identity.Tests to Core.Tests, dropping an incidental ProjectReference to SlpModularCms.Api that existed only for those tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,146 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using SlpModularCms.Core.Availability;
|
||||
using SlpModularCms.Core.Data;
|
||||
using SlpModularCms.Core.Exceptions;
|
||||
using SlpModularCms.Core.Identity.Authorization;
|
||||
using SlpModularCms.Core.Identity.Entities;
|
||||
using SlpModularCms.Core.Identity.Models;
|
||||
using SlpModularCms.Core.Identity.Services;
|
||||
using SlpModularCms.Api.Infrastructure;
|
||||
using System.Text;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SlpModularCms.Api.Extensions;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddCoreInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// 1. Database
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
// 2. Identity
|
||||
services.AddIdentityCore<ApplicationUser>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequireLowercase = true;
|
||||
options.Password.RequireUppercase = true;
|
||||
options.Password.RequireNonAlphanumeric = true;
|
||||
options.Password.RequiredLength = 8;
|
||||
})
|
||||
.AddRoles<ApplicationRole>()
|
||||
.AddRoleManager<RoleManager<ApplicationRole>>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||
|
||||
// 3. Auth & Identity Services
|
||||
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()
|
||||
?? throw new InvalidOperationException("JwtSettings not found in configuration.");
|
||||
|
||||
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
|
||||
services.Configure<AvailabilityOptions>(configuration.GetSection("Availability"));
|
||||
|
||||
services.AddScoped<IAuthService, AuthService>();
|
||||
services.AddScoped<IInvitationService, InvitationService>();
|
||||
services.AddScoped<ISetupService, SetupService>();
|
||||
|
||||
// 4. Authentication
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwtSettings.Issuer,
|
||||
ValidAudience = jwtSettings.Audience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)),
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
});
|
||||
|
||||
// 5. Authorization
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("OwnerOnly", policy => policy.Requirements.Add(new HierarchicalRoleRequirement("Owner")));
|
||||
options.AddPolicy("AdminOnly", policy => policy.Requirements.Add(new HierarchicalRoleRequirement("Administrator")));
|
||||
options.AddPolicy("UserOnly", policy => policy.Requirements.Add(new HierarchicalRoleRequirement("User")));
|
||||
});
|
||||
|
||||
services.AddSingleton<IAuthorizationHandler, HierarchicalRoleHandler>();
|
||||
|
||||
// 6. Exception Handling
|
||||
services.AddExceptionHandler<GlobalExceptionHandler>();
|
||||
services.AddProblemDetails();
|
||||
|
||||
// 7. Versioning
|
||||
services.AddApiVersioning(options =>
|
||||
{
|
||||
options.ReportApiVersions = true;
|
||||
});
|
||||
|
||||
// 8. Native .NET OpenAPI
|
||||
services.AddOpenApi();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCmsCors(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var allowedOrigins = configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
|
||||
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(builder =>
|
||||
{
|
||||
builder.WithOrigins(allowedOrigins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCmsRateLimiting(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
|
||||
options.AddFixedWindowLimiter("login", opt =>
|
||||
{
|
||||
var settings = configuration.GetSection("RateLimiting:Login");
|
||||
opt.PermitLimit = settings.GetValue<int>("PermitLimit", 5);
|
||||
opt.Window = TimeSpan.FromSeconds(settings.GetValue<int>("WindowSeconds", 60));
|
||||
opt.QueueLimit = 0;
|
||||
});
|
||||
|
||||
options.AddSlidingWindowLimiter("refresh", opt =>
|
||||
{
|
||||
var settings = configuration.GetSection("RateLimiting:Refresh");
|
||||
opt.PermitLimit = settings.GetValue<int>("PermitLimit", 20);
|
||||
opt.Window = TimeSpan.FromSeconds(settings.GetValue<int>("WindowSeconds", 60));
|
||||
opt.SegmentsPerWindow = 4;
|
||||
opt.QueueLimit = 0;
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
||||
namespace SlpModularCms.Api.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Conventie die automatisch de /api/v1/ prefix toevoegt aan alle controllers.
|
||||
/// </summary>
|
||||
public class ApiPrefixConvention : IApplicationModelConvention
|
||||
{
|
||||
private readonly AttributeRouteModel _routePrefix;
|
||||
|
||||
public ApiPrefixConvention(string prefix)
|
||||
{
|
||||
_routePrefix = new AttributeRouteModel(new RouteAttribute(prefix));
|
||||
}
|
||||
|
||||
public void Apply(ApplicationModel application)
|
||||
{
|
||||
foreach (var controller in application.Controllers)
|
||||
{
|
||||
foreach (var selector in controller.Selectors)
|
||||
{
|
||||
if (selector.AttributeRouteModel != null)
|
||||
{
|
||||
selector.AttributeRouteModel.Template = _routePrefix.Template + "/" + selector.AttributeRouteModel.Template;
|
||||
}
|
||||
else
|
||||
{
|
||||
selector.AttributeRouteModel = _routePrefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SlpModularCms.Core.Modules;
|
||||
|
||||
namespace SlpModularCms.Api.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Orkestrator die modules ontdekt en hun lifecycle beheert.
|
||||
/// </summary>
|
||||
public class ModuleOrchestrator
|
||||
{
|
||||
private readonly List<IModule> _modules = new();
|
||||
private readonly ILogger<ModuleOrchestrator> _logger;
|
||||
|
||||
public ModuleOrchestrator(ILogger<ModuleOrchestrator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void DiscoverModules()
|
||||
{
|
||||
_logger.LogInformation("Start module discovery...");
|
||||
|
||||
// Forceer het laden van module assemblies van disk
|
||||
var path = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var moduleFiles = Directory.GetFiles(path, "SlpModularCms.Modules.*.dll");
|
||||
|
||||
foreach (var file in moduleFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assemblyName = AssemblyName.GetAssemblyName(file);
|
||||
if (AppDomain.CurrentDomain.GetAssemblies().All(a => a.FullName != assemblyName.FullName))
|
||||
{
|
||||
Assembly.Load(assemblyName);
|
||||
_logger.LogDebug("Assembly geladen: {AssemblyName}", assemblyName.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Kon assembly niet laden van disk: {FilePath}", file);
|
||||
}
|
||||
}
|
||||
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => a.FullName != null && a.FullName.StartsWith("SlpModularCms.Modules"))
|
||||
.ToList();
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
var moduleTypes = assembly.GetTypes()
|
||||
.Where(t => typeof(IModule).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
|
||||
|
||||
foreach (var type in moduleTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Activator.CreateInstance(type) is IModule module)
|
||||
{
|
||||
_modules.Add(module);
|
||||
_logger.LogInformation("Module ontdekt: {ModuleName} v{Version}", module.Name, module.Version);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Fout bij het instantiëren van module type {TypeName}", type.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("{Count} modules succesvol geladen.", _modules.Count);
|
||||
}
|
||||
|
||||
public void RegisterModuleServices(IServiceCollection services)
|
||||
{
|
||||
foreach (var module in _modules)
|
||||
{
|
||||
try
|
||||
{
|
||||
module.RegisterServices(services);
|
||||
_logger.LogInformation("Services geregistreerd voor module: {ModuleName}", module.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Fout bij het registreren van services voor module {ModuleName}", module.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UseModules(IApplicationBuilder app)
|
||||
{
|
||||
foreach (var module in _modules)
|
||||
{
|
||||
try
|
||||
{
|
||||
module.UseModule(app);
|
||||
_logger.LogInformation("Module geactiveerd in pipeline: {ModuleName}", module.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Fout bij het activeren van module {ModuleName} in de pipeline", module.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using SlpModularCms.Api.Extensions;
|
||||
using SlpModularCms.Api.Infrastructure;
|
||||
using SlpModularCms.Core.Hosting;
|
||||
using Scalar.AspNetCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
Reference in New Issue
Block a user