Initial commit with inital CMS

This commit is contained in:
2026-06-15 17:00:16 +02:00
commit 95d986790e
132 changed files with 7624 additions and 0 deletions
@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
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;
namespace SlpModularCms.Api.Extensions;
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;
}
}
@@ -0,0 +1,35 @@
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;
}
}
}
}
}
@@ -0,0 +1,107 @@
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);
}
}
}
}
+49
View File
@@ -0,0 +1,49 @@
using SlpModularCms.Api.Extensions;
using SlpModularCms.Api.Infrastructure;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Load local developer overrides
builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true);
// 1. Initialize Module Orchestrator
var loggerFactory = LoggerFactory.Create(lb => lb.AddConsole());
var orchestrator = new ModuleOrchestrator(loggerFactory.CreateLogger<ModuleOrchestrator>());
orchestrator.DiscoverModules();
// 2. Add Core Infrastructure
builder.Services.AddCoreInfrastructure(builder.Configuration);
// 3. Add Module Services
orchestrator.RegisterModuleServices(builder.Services);
// 4. Global Controller Configuration with Conventions
builder.Services.AddControllers(options =>
{
options.Conventions.Add(new ApiPrefixConvention("api/v1"));
});
var app = builder.Build();
// 5. Global Exception Handling
app.UseExceptionHandler();
// 6. Configure Pipeline
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
// 7. Use Module Middleware
orchestrator.UseModules(app);
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5284",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"launchUrl": "scalar"
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7221;http://localhost:5284",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"launchUrl": "scalar"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
# SlpModularCms.Api
## AIDLC Context
Als ontwikkelaar wil ik een modulaire CMS API bouwen. Deze API zal een raamwerk hebben met veelgebruikte functionaliteiten en gebruikersbeheer.
Het doel is dat ik deze API kan gebruiken voor klanten om websites en applicaties te bouwen.
Elke klant kan andere wensen hebben, maar per branch of type website zullen er altijd functionaliteiten zijn die hetzelfde zijn.
Extra functionaliteiten moeten dus met modules worden geïmplementeerd, zodat ze eenvoudig kunnen worden toegevoegd of verwijderd.
Om mijzelf in te dekken wil ik iets inbouwen zodat ik modules op afstand kan uitzetten of dat ik de CMS kan blokkeren in het geval dat een klant zich niet aan de afspraak houd zoals een betaling niet doen of andere dingen.
Dit betekent dat er standaard iets moet worden ingebouwd dat de API een controle uitvoert met een call naar een zogenoemde "master"-API om te controleren of alles nog beschikbaar is. Misschien moet het zelfs bij de master worden opgeslagen in de database én bij de client zelf.
De "master"-API moet een module krijgen waarin alles wordt opgeslagen en waarin de API kan controleren of de klant nog toegang heeft tot de CMS, maar deze module mag later pas. Voor de MVP wil ik het raamwerk hebben met authenticatie, authorisatie en gebruikersbeheer voor gebruikers.
Zodra dat staat kan er gekeken worden naar een simpele eerste module.
Daarna kan de module voor de "master"-API worden geïmplementeerd waar ook de connectie tussen "master"- en client-API moet worden bedacht. Hier kan later over worden nagedacht, maar den kaan iets als client-secret pairs.
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Scalar.AspNetCore" Version="2.16.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SlpModularCms.Core\SlpModularCms.Core.csproj" />
<ProjectReference Include="..\SlpModularCms.Modules.Availability\SlpModularCms.Modules.Availability.csproj" />
<ProjectReference Include="..\SlpModularCms.Modules.Identity\SlpModularCms.Modules.Identity.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
@SlpModularCms.Api_HostAddress = http://localhost:5284
GET {{SlpModularCms.Api_HostAddress}}/weatherforecast/
Accept: application/json
###
@@ -0,0 +1,22 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=SlpModularCms;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"JwtSettings": {
"Secret": "SuperSecretKeyForDevelopmentOnly_MustBeLongerThan32Bytes!",
"Issuer": "SlpModularCms",
"Audience": "SlpModularCmsPortal",
"ExpiryMinutes": 60,
"RefreshTokenExpiryDays": 7
},
"Availability": {
"CircuitBreakerSeconds": 30,
"StatusCacheSeconds": 1
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=<production-db-host>;Database=SlpModularCms;User Id=<db-user>;Password=<db-password>;TrustServerCertificate=True"
},
"JwtSettings": {
"Secret": "<secure-long-random-secret-key-from-env>",
"Issuer": "SlpModularCms",
"Audience": "SlpModularCmsPortal",
"ExpiryMinutes": 60,
"RefreshTokenExpiryDays": 7
},
"Availability": {
"CircuitBreakerSeconds": 30,
"StatusCacheSeconds": 1
}
}