Refactor app architecture and clean local artifacts

This commit is contained in:
AzuTear
2026-06-24 23:43:14 +02:00
parent 17134b3b82
commit fef1d36fe8
274 changed files with 37724 additions and 6065 deletions
+40
View File
@@ -0,0 +1,40 @@
namespace Backend.Security;
public static class AdminRoles
{
public const string Viewer = "viewer";
public const string ContentAdmin = "content_admin";
public const string Admin = "admin";
public const string Owner = "owner";
public static string Normalize(string? role)
{
var normalizedRole = (role ?? string.Empty).Trim().ToLowerInvariant().Replace('-', '_');
return normalizedRole switch
{
Owner => Owner,
Admin => Admin,
ContentAdmin => ContentAdmin,
_ => Viewer,
};
}
public static bool IsKnownRole(string? role)
{
var normalizedRole = (role ?? string.Empty).Trim().ToLowerInvariant().Replace('-', '_');
return normalizedRole is Viewer or ContentAdmin or Admin or Owner;
}
public static bool CanAccessAdmin(string? role) =>
Normalize(role) is ContentAdmin or Admin or Owner;
public static bool CanManageContent(string? role) =>
Normalize(role) is ContentAdmin or Admin or Owner;
public static bool CanManageAdminWorkspace(string? role) =>
Normalize(role) is Admin or Owner;
public static bool CanManageOperationalSettings(string? role) =>
Normalize(role) is Owner;
}
+23
View File
@@ -0,0 +1,23 @@
using Backend.Services;
namespace Backend.Security;
public sealed class AdminSessionFilter(IUserSessionService userSessionService) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var session = await userSessionService.ResolveSessionAsync(context.HttpContext, context.HttpContext.RequestAborted);
if (session is null)
{
return Results.Unauthorized();
}
if (!AdminRoles.CanAccessAdmin(session.Role))
{
return Results.Json(new { message = "Admin access requires an elevated role." }, statusCode: StatusCodes.Status403Forbidden);
}
context.HttpContext.SetCurrentSession(session);
return await next(context);
}
}
+59
View File
@@ -0,0 +1,59 @@
using System.Security.Cryptography;
using System.Text;
namespace Backend.Security;
public static class DemoCredentialHasher
{
private const int SaltSize = 16;
private const int HashSize = 32;
private const int Iterations = 100_000;
public static (string Hash, string Salt) HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
Iterations,
HashAlgorithmName.SHA256,
HashSize);
return (Convert.ToBase64String(hash), Convert.ToBase64String(salt));
}
public static bool VerifyPassword(string password, string expectedHash, string salt)
{
if (string.IsNullOrWhiteSpace(expectedHash) || string.IsNullOrWhiteSpace(salt))
{
return false;
}
try
{
var saltBytes = Convert.FromBase64String(salt);
var expectedBytes = Convert.FromBase64String(expectedHash);
var candidateBytes = Rfc2898DeriveBytes.Pbkdf2(
password,
saltBytes,
Iterations,
HashAlgorithmName.SHA256,
expectedBytes.Length);
return CryptographicOperations.FixedTimeEquals(candidateBytes, expectedBytes);
}
catch (FormatException)
{
return false;
}
}
public static bool FixedTimePlainTextEquals(string candidate, string expected)
{
var candidateBytes = Encoding.UTF8.GetBytes(candidate);
var expectedBytes = Encoding.UTF8.GetBytes(expected);
return candidateBytes.Length == expectedBytes.Length
&& CryptographicOperations.FixedTimeEquals(candidateBytes, expectedBytes);
}
}
@@ -0,0 +1,14 @@
using Backend.Domain;
namespace Backend.Security;
public static class HttpContextSessionExtensions
{
private const string SessionItemKey = "__current_session";
public static void SetCurrentSession(this HttpContext context, UserSession session) =>
context.Items[SessionItemKey] = session;
public static UserSession? GetCurrentSession(this HttpContext context) =>
context.Items.TryGetValue(SessionItemKey, out var value) ? value as UserSession : null;
}
@@ -0,0 +1,35 @@
namespace Backend.Security;
public sealed class SecurityHeadersMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
context.Response.OnStarting(() =>
{
var headers = context.Response.Headers;
headers["X-Content-Type-Options"] = "nosniff";
headers["X-Frame-Options"] = "DENY";
headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
headers["Cross-Origin-Opener-Policy"] = "same-origin";
if (!headers.ContainsKey("Content-Security-Policy"))
{
headers["Content-Security-Policy"] =
"default-src 'self'; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.gstatic.com data:; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"script-src 'self'; " +
"connect-src 'self' https:; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self';";
}
return Task.CompletedTask;
});
await next(context);
}
}