354 lines
15 KiB
C#
354 lines
15 KiB
C#
using System.Text.Json;
|
|
using Backend.Common;
|
|
using Backend.Contracts;
|
|
using Backend.Data;
|
|
using Backend.Domain;
|
|
using Backend.Security;
|
|
using Backend.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Backend.Endpoints;
|
|
|
|
public static class AdminSiteSettingsEndpoints
|
|
{
|
|
private const string FallbackMaintenanceTitle = "Sternenpause";
|
|
private const string FallbackMaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.";
|
|
|
|
public static RouteGroupBuilder MapAdminSiteSettingsEndpoints(this RouteGroupBuilder group)
|
|
{
|
|
group.MapGet("/site-settings", GetSiteSettings).WithName("GetAdminSiteSettings").WithOpenApi();
|
|
group.MapPut("/site-settings", UpdateSiteSettings).WithName("UpdateAdminSiteSettings").WithOpenApi();
|
|
group.MapGet("/operational-settings", GetOperationalSettings).WithName("GetAdminOperationalSettings").WithOpenApi();
|
|
group.MapPut("/operational-settings", UpdateOperationalSettings).WithName("UpdateAdminOperationalSettings").WithOpenApi();
|
|
return group;
|
|
}
|
|
|
|
private static async Task<IResult> GetSiteSettings(AwardsDbContext db)
|
|
{
|
|
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
|
if (settings is null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
return Results.Ok(new AdminSiteSettingsResponse(
|
|
settings.HostDisplayName,
|
|
settings.HostTagline,
|
|
settings.NewsletterUrl,
|
|
settings.PrivacyEmail,
|
|
settings.PrivacyPolicyContent,
|
|
settings.PrivacyPolicyUpdatedBy,
|
|
settings.PrivacyPolicyUpdatedAt,
|
|
settings.ImprintUrl,
|
|
settings.ContactUrl,
|
|
settings.SponsorsUrl,
|
|
SeasonMappings.ReadSocialLinks(settings),
|
|
SeasonMappings.ReadFaqItems(settings)));
|
|
}
|
|
|
|
private static async Task<IResult> UpdateSiteSettings(
|
|
HttpContext context,
|
|
UpdateSiteSettingsRequest request,
|
|
AwardsDbContext db,
|
|
IAdminAuditService adminAuditService)
|
|
{
|
|
var session = AdminEndpointConventions.CurrentSession(context);
|
|
if (!AdminRoles.CanManageContent(session.Role))
|
|
{
|
|
return Results.Json(new { message = "Landingpage content requires a content admin, admin or owner role." }, statusCode: StatusCodes.Status403Forbidden);
|
|
}
|
|
|
|
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
|
if (settings is null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
settings.HostDisplayName = request.HostDisplayName.Trim();
|
|
settings.HostTagline = request.HostTagline.Trim();
|
|
settings.NewsletterUrl = request.NewsletterUrl.Trim();
|
|
settings.PrivacyEmail = request.PrivacyEmail.Trim();
|
|
var trimmedPrivacyContent = request.PrivacyPolicyContent.Trim();
|
|
var privacyChanged = !string.Equals(settings.PrivacyPolicyContent, trimmedPrivacyContent, StringComparison.Ordinal);
|
|
settings.PrivacyPolicyContent = trimmedPrivacyContent;
|
|
if (privacyChanged)
|
|
{
|
|
settings.PrivacyPolicyUpdatedBy = session.DisplayName.Trim();
|
|
settings.PrivacyPolicyUpdatedAt = DateTimeOffset.UtcNow;
|
|
}
|
|
|
|
settings.ImprintUrl = request.ImprintUrl.Trim();
|
|
settings.ContactUrl = request.ContactUrl.Trim();
|
|
settings.SponsorsUrl = request.SponsorsUrl.Trim();
|
|
settings.SocialLinksJson = JsonSerializer.Serialize(request.SocialLinks ?? []);
|
|
settings.FaqJson = JsonSerializer.Serialize(request.Faq ?? []);
|
|
|
|
adminAuditService.AddEntry(
|
|
session.TwitchUserId,
|
|
"site-settings.update",
|
|
"site-settings",
|
|
settings.Id.ToString(),
|
|
"Public Site Settings wurden aktualisiert.",
|
|
new
|
|
{
|
|
settings.HostDisplayName,
|
|
privacyChanged,
|
|
socialLinkCount = request.SocialLinks?.Length ?? 0,
|
|
faqCount = request.Faq?.Length ?? 0,
|
|
},
|
|
RequestMetadataReader.Read(context));
|
|
|
|
await db.SaveChangesAsync(context.RequestAborted);
|
|
return Results.Ok(new { saved = true });
|
|
}
|
|
|
|
private static async Task<IResult> GetOperationalSettings(AwardsDbContext db, IConfiguration configuration)
|
|
{
|
|
var settings = await db.SiteSettings.AsNoTracking().FirstOrDefaultAsync(item => item.Id == 1);
|
|
if (settings is null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
var usesDatabaseDemo = settings.DemoLoginManagedByDatabase || HasDatabaseDemoCredentials(settings);
|
|
return Results.Ok(new AdminOperationalSettingsResponse(
|
|
usesDatabaseDemo,
|
|
usesDatabaseDemo ? settings.DemoLoginEnabled : IsDemoLoginEnabled(configuration),
|
|
usesDatabaseDemo ? settings.DemoLoginEmail : ReadDemoLoginIdentifier(configuration),
|
|
usesDatabaseDemo ? HasDatabaseDemoCredentials(settings) : !string.IsNullOrWhiteSpace(ReadDemoSetting(configuration, "Password", "VTSA_DEMO_ADMIN_PASSWORD")),
|
|
usesDatabaseDemo ? settings.DemoLoginTwitchUserId : ReadDemoSetting(configuration, "TwitchUserId", "VTSA_DEMO_ADMIN_TWITCH_ID"),
|
|
usesDatabaseDemo ? settings.DemoLoginDisplayName : ReadDemoSetting(configuration, "DisplayName", "VTSA_DEMO_ADMIN_DISPLAY_NAME"),
|
|
settings.MaintenanceModeEnabled,
|
|
string.IsNullOrWhiteSpace(settings.MaintenanceTitle) ? FallbackMaintenanceTitle : settings.MaintenanceTitle,
|
|
string.IsNullOrWhiteSpace(settings.MaintenanceMessage)
|
|
? FallbackMaintenanceMessage
|
|
: settings.MaintenanceMessage));
|
|
}
|
|
|
|
private static async Task<IResult> UpdateOperationalSettings(
|
|
HttpContext context,
|
|
UpdateOperationalSettingsRequest request,
|
|
AwardsDbContext db,
|
|
IConfiguration configuration,
|
|
IAdminAuditService adminAuditService)
|
|
{
|
|
var session = AdminEndpointConventions.CurrentSession(context);
|
|
if (!AdminRoles.CanManageOperationalSettings(session.Role))
|
|
{
|
|
return Results.Json(new { message = "Demo-Zugang und Wartungsmodus können nur Owner ändern." }, statusCode: StatusCodes.Status403Forbidden);
|
|
}
|
|
|
|
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
|
if (settings is null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
var before = CreateOperationalSettingsSnapshot(settings);
|
|
var loginIdentifier = request.DemoLoginEmail.Trim();
|
|
var twitchUserId = request.DemoLoginTwitchUserId.Trim();
|
|
var displayName = request.DemoLoginDisplayName.Trim();
|
|
var newPassword = request.DemoLoginPassword?.Trim() ?? string.Empty;
|
|
var fallbackPassword = ReadDemoSetting(configuration, "Password", "VTSA_DEMO_ADMIN_PASSWORD");
|
|
|
|
if (request.DemoLoginEnabled)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(loginIdentifier)
|
|
|| string.IsNullOrWhiteSpace(twitchUserId)
|
|
|| string.IsNullOrWhiteSpace(displayName))
|
|
{
|
|
return Results.BadRequest(new { message = "Demo-Login braucht Login, Twitch-ID und Anzeigenamen." });
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(newPassword)
|
|
&& !HasDatabaseDemoCredentials(settings)
|
|
&& string.IsNullOrWhiteSpace(fallbackPassword))
|
|
{
|
|
return Results.BadRequest(new { message = "Bitte setze beim ersten Aktivieren ein Demo-Passwort." });
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(newPassword) && newPassword.Length < 12)
|
|
{
|
|
return Results.BadRequest(new { message = "Das Demo-Passwort muss mindestens 12 Zeichen lang sein." });
|
|
}
|
|
|
|
settings.DemoLoginManagedByDatabase = true;
|
|
settings.DemoLoginEnabled = request.DemoLoginEnabled;
|
|
settings.DemoLoginEmail = loginIdentifier;
|
|
settings.DemoLoginTwitchUserId = string.IsNullOrWhiteSpace(twitchUserId) ? "jayuhime_admin" : twitchUserId;
|
|
settings.DemoLoginDisplayName = string.IsNullOrWhiteSpace(displayName) ? "Jayuhime Admin" : displayName;
|
|
|
|
var passwordToPersist = !string.IsNullOrWhiteSpace(newPassword)
|
|
? newPassword
|
|
: request.DemoLoginEnabled && !HasDatabaseDemoCredentials(settings)
|
|
? fallbackPassword
|
|
: string.Empty;
|
|
|
|
if (!string.IsNullOrWhiteSpace(passwordToPersist))
|
|
{
|
|
var credentials = DemoCredentialHasher.HashPassword(passwordToPersist);
|
|
settings.DemoLoginPasswordHash = credentials.Hash;
|
|
settings.DemoLoginPasswordSalt = credentials.Salt;
|
|
}
|
|
|
|
settings.MaintenanceModeEnabled = request.MaintenanceModeEnabled;
|
|
settings.MaintenanceTitle = NormalizeOperationalText(request.MaintenanceTitle, FallbackMaintenanceTitle, 120);
|
|
settings.MaintenanceMessage = NormalizeOperationalText(
|
|
request.MaintenanceMessage,
|
|
FallbackMaintenanceMessage,
|
|
600);
|
|
|
|
var changes = BuildOperationalSettingChanges(
|
|
before,
|
|
CreateOperationalSettingsSnapshot(settings),
|
|
!string.IsNullOrWhiteSpace(passwordToPersist));
|
|
|
|
adminAuditService.AddEntry(
|
|
session.TwitchUserId,
|
|
"operational-settings.update",
|
|
"site-settings",
|
|
settings.Id.ToString(),
|
|
"Demo-Zugang und Wartungsmodus wurden aktualisiert.",
|
|
new
|
|
{
|
|
settings.DemoLoginEnabled,
|
|
passwordChanged = !string.IsNullOrWhiteSpace(passwordToPersist),
|
|
settings.MaintenanceModeEnabled,
|
|
changes,
|
|
},
|
|
RequestMetadataReader.Read(context));
|
|
|
|
await db.SaveChangesAsync(context.RequestAborted);
|
|
|
|
return Results.Ok(new
|
|
{
|
|
saved = true,
|
|
demoLoginPasswordSet = HasDatabaseDemoCredentials(settings),
|
|
});
|
|
}
|
|
|
|
private static string NormalizeOperationalText(string value, string fallback, int maxLength)
|
|
{
|
|
var trimmed = value.Trim();
|
|
if (string.IsNullOrWhiteSpace(trimmed))
|
|
{
|
|
return fallback;
|
|
}
|
|
|
|
return trimmed.Length <= maxLength ? trimmed : trimmed[..maxLength];
|
|
}
|
|
|
|
private static bool IsDemoLoginEnabled(IConfiguration configuration)
|
|
{
|
|
var rawValue = configuration["VTSA_DEMO_LOGIN_ENABLED"]
|
|
?? configuration["DemoAdmin:Enabled"];
|
|
|
|
return bool.TryParse(rawValue, out var enabled) && enabled;
|
|
}
|
|
|
|
private static string ReadDemoSetting(IConfiguration configuration, string key, string environmentKey) =>
|
|
configuration[environmentKey] ?? configuration[$"DemoAdmin:{key}"] ?? string.Empty;
|
|
|
|
private static string ReadDemoLoginIdentifier(IConfiguration configuration)
|
|
{
|
|
var configuredLogin = ReadDemoSetting(configuration, "Login", "VTSA_DEMO_ADMIN_LOGIN");
|
|
return string.IsNullOrWhiteSpace(configuredLogin)
|
|
? ReadDemoSetting(configuration, "Email", "VTSA_DEMO_ADMIN_EMAIL")
|
|
: configuredLogin;
|
|
}
|
|
|
|
private static bool HasDatabaseDemoCredentials(SiteSettings settings) =>
|
|
!string.IsNullOrWhiteSpace(settings.DemoLoginEmail)
|
|
&& !string.IsNullOrWhiteSpace(settings.DemoLoginPasswordHash)
|
|
&& !string.IsNullOrWhiteSpace(settings.DemoLoginPasswordSalt);
|
|
|
|
private static OperationalSettingsSnapshot CreateOperationalSettingsSnapshot(SiteSettings settings) =>
|
|
new(
|
|
settings.DemoLoginManagedByDatabase,
|
|
settings.DemoLoginEnabled,
|
|
settings.DemoLoginEmail,
|
|
HasDatabaseDemoCredentials(settings),
|
|
settings.DemoLoginTwitchUserId,
|
|
settings.DemoLoginDisplayName,
|
|
settings.MaintenanceModeEnabled,
|
|
string.IsNullOrWhiteSpace(settings.MaintenanceTitle) ? FallbackMaintenanceTitle : settings.MaintenanceTitle,
|
|
string.IsNullOrWhiteSpace(settings.MaintenanceMessage) ? FallbackMaintenanceMessage : settings.MaintenanceMessage);
|
|
|
|
private static object[] BuildOperationalSettingChanges(
|
|
OperationalSettingsSnapshot before,
|
|
OperationalSettingsSnapshot after,
|
|
bool passwordChanged)
|
|
{
|
|
var changes = new List<object>();
|
|
|
|
AddOperationalChange(changes, "demoLoginManagedByDatabase", "Demo Quelle", before.DemoLoginManagedByDatabase, after.DemoLoginManagedByDatabase);
|
|
AddOperationalChange(changes, "demoLoginEnabled", "Demo Login", before.DemoLoginEnabled, after.DemoLoginEnabled);
|
|
AddOperationalChange(changes, "demoLoginEmail", "Demo Login", before.DemoLoginEmail, after.DemoLoginEmail);
|
|
AddOperationalChange(changes, "demoLoginTwitchUserId", "Demo Twitch-ID", before.DemoLoginTwitchUserId, after.DemoLoginTwitchUserId);
|
|
AddOperationalChange(changes, "demoLoginDisplayName", "Demo Anzeigename", before.DemoLoginDisplayName, after.DemoLoginDisplayName);
|
|
|
|
if (passwordChanged)
|
|
{
|
|
changes.Add(new
|
|
{
|
|
field = "demoLoginPassword",
|
|
label = "Demo Passwort",
|
|
@from = before.DemoLoginPasswordSet ? "gesetzt" : "nicht gesetzt",
|
|
to = "neu gesetzt",
|
|
sensitive = true,
|
|
});
|
|
}
|
|
|
|
AddOperationalChange(changes, "maintenanceModeEnabled", "Wartungsmodus", before.MaintenanceModeEnabled, after.MaintenanceModeEnabled);
|
|
AddOperationalChange(changes, "maintenanceTitle", "Wartungstitel", before.MaintenanceTitle, after.MaintenanceTitle);
|
|
AddOperationalChange(changes, "maintenanceMessage", "Wartungstext", before.MaintenanceMessage, after.MaintenanceMessage);
|
|
|
|
return changes.ToArray();
|
|
}
|
|
|
|
private static void AddOperationalChange<T>(
|
|
ICollection<object> changes,
|
|
string field,
|
|
string label,
|
|
T before,
|
|
T after)
|
|
{
|
|
if (EqualityComparer<T>.Default.Equals(before, after))
|
|
{
|
|
return;
|
|
}
|
|
|
|
changes.Add(new
|
|
{
|
|
field,
|
|
label,
|
|
@from = FormatOperationalAuditValue(before),
|
|
to = FormatOperationalAuditValue(after),
|
|
sensitive = false,
|
|
});
|
|
}
|
|
|
|
private static string FormatOperationalAuditValue<T>(T value)
|
|
{
|
|
if (value is bool booleanValue)
|
|
{
|
|
return booleanValue ? "aktiv" : "aus";
|
|
}
|
|
|
|
var text = Convert.ToString(value)?.Trim() ?? string.Empty;
|
|
return string.IsNullOrWhiteSpace(text) ? "leer" : text;
|
|
}
|
|
|
|
private sealed record OperationalSettingsSnapshot(
|
|
bool DemoLoginManagedByDatabase,
|
|
bool DemoLoginEnabled,
|
|
string DemoLoginEmail,
|
|
bool DemoLoginPasswordSet,
|
|
string DemoLoginTwitchUserId,
|
|
string DemoLoginDisplayName,
|
|
bool MaintenanceModeEnabled,
|
|
string MaintenanceTitle,
|
|
string MaintenanceMessage);
|
|
}
|