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,25 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SlpModularCms.Core.Availability;
using SlpModularCms.Core.Modules;
using SlpModularCms.Modules.Availability.Middleware;
using SlpModularCms.Modules.Availability.Services;
namespace SlpModularCms.Modules.Availability;
public class AvailabilityModule : IModule
{
public string Name => "Availability";
public string Version => "1.0.0";
public void RegisterServices(IServiceCollection services)
{
services.AddScoped<IAvailabilityService, PersistentAvailabilityService>();
services.AddScoped<PersistentAvailabilityService>();
}
public void UseModule(IApplicationBuilder app)
{
app.UseMiddleware<AvailabilityMiddleware>();
}
}
@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SlpModularCms.Core.Availability;
using SlpModularCms.Modules.Availability.Services;
namespace SlpModularCms.Modules.Availability.Controllers;
[ApiController]
[Route("[controller]")]
public class AvailabilityController : ControllerBase
{
private readonly IAvailabilityService _availabilityService;
public AvailabilityController(IAvailabilityService availabilityService)
{
_availabilityService = availabilityService;
}
[HttpGet("status")]
[AllowAnonymous]
public async Task<IActionResult> GetStatus()
{
var status = await _availabilityService.IsAvailableAsync();
return Ok(new
{
Status = status.ToString(),
CheckedAt = DateTimeOffset.UtcNow,
Message = status == AvailabilityStatus.Available
? "System is running normally."
: "System is in maintenance or unavailable."
});
}
[HttpPost("admin/status")]
[Authorize(Policy = "OwnerOnly")]
public async Task<IActionResult> UpdateStatus([FromBody] UpdateStatusRequest request)
{
if (_availabilityService is PersistentAvailabilityService persistentService)
{
await persistentService.UpdateStatusAsync(
request.NewStatus,
request.Reason,
User.Identity?.Name);
return Ok();
}
return BadRequest("Status update niet ondersteund door huidige service.");
}
}
public record UpdateStatusRequest(
AvailabilityStatus NewStatus,
string? Reason
);
@@ -0,0 +1,83 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using SlpModularCms.Core.Availability;
using SlpModularCms.Core.Exceptions;
namespace SlpModularCms.Modules.Availability.Middleware;
/// <summary>
/// Middleware die de beschikbaarheid van het systeem controleert en requests blokkeert indien nodig.
/// </summary>
public class AvailabilityMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<AvailabilityMiddleware> _logger;
public AvailabilityMiddleware(RequestDelegate next, ILogger<AvailabilityMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, IAvailabilityService availabilityService)
{
// Bypass voor status endpoint
if (context.Request.Path.StartsWithSegments("/api/availability/status"))
{
await _next(context);
return;
}
var status = await availabilityService.IsAvailableAsync();
if (status == AvailabilityStatus.Available)
{
await _next(context);
return;
}
// Check voor Beheerder Bypass (Owner of Administrator)
if (IsAdminBypass(context))
{
await _next(context);
return;
}
_logger.LogWarning("Request geblokkeerd vanwege systeemstatus: {Status}. Path: {Path}", status, context.Request.Path);
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
var response = new ApiErrorResponse(
Message: status == AvailabilityStatus.Maintenance
? "Het systeem is momenteel in onderhoud. Probeer het later opnieuw."
: "De service is tijdelijk niet beschikbaar."
);
await context.Response.WriteAsJsonAsync(response);
}
private bool IsAdminBypass(HttpContext context)
{
var authHeader = context.Request.Headers.Authorization.ToString();
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return false;
}
try
{
var tokenString = authHeader.Substring("Bearer ".Length);
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(tokenString);
var roles = token.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value);
return roles.Any(r => r == "Owner" || r == "Administrator");
}
catch (Exception)
{
// Ongeldig token of parsing fout: geen bypass
return false;
}
}
}
@@ -0,0 +1,84 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using SlpModularCms.Core.Availability;
using SlpModularCms.Core.Data;
using SlpModularCms.Core.Identity.Entities;
namespace SlpModularCms.Modules.Availability.Services;
/// <summary>
/// Persistente implementatie van de IAvailabilityService met Circuit Breaker.
/// </summary>
public class PersistentAvailabilityService : IAvailabilityService
{
private readonly ApplicationDbContext _context;
private readonly AvailabilityOptions _options;
// Circuit Breaker state
private static DateTimeOffset _lastErrorTime = DateTimeOffset.MinValue;
private static AvailabilityStatus _cachedStatus = AvailabilityStatus.Available;
public PersistentAvailabilityService(ApplicationDbContext context, IOptions<AvailabilityOptions> options)
{
_context = context;
_options = options.Value;
}
public async Task<AvailabilityStatus> IsAvailableAsync()
{
// Check Circuit Breaker
if (DateTimeOffset.UtcNow - _lastErrorTime < TimeSpan.FromSeconds(_options.CircuitBreakerSeconds))
{
return _cachedStatus;
}
try
{
var state = await _context.AvailabilityStates.FirstOrDefaultAsync();
// Als er nog geen state is, maken we een default aan (Available)
if (state == null)
{
return AvailabilityStatus.Available;
}
_cachedStatus = state.Status;
return state.Status;
}
catch (Exception)
{
// Database fout: activeer Circuit Breaker
_lastErrorTime = DateTimeOffset.UtcNow;
_cachedStatus = AvailabilityStatus.Available; // Veilige fallback
return _cachedStatus;
}
}
/// <summary>
/// Update de globale systeemstatus.
/// </summary>
public async Task UpdateStatusAsync(AvailabilityStatus newStatus, string? reason, string? updatedBy)
{
var state = await _context.AvailabilityStates.FirstOrDefaultAsync();
if (state == null)
{
state = new GlobalAvailabilityState
{
Id = Guid.NewGuid()
};
_context.AvailabilityStates.Add(state);
}
state.Status = newStatus;
state.Message = reason;
state.LastUpdatedAt = DateTimeOffset.UtcNow;
state.UpdatedBy = updatedBy;
await _context.SaveChangesAsync();
// Reset cache
_cachedStatus = newStatus;
_lastErrorTime = DateTimeOffset.MinValue;
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\SlpModularCms.Core\SlpModularCms.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>