Harden request validation and admin UX cues
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 58s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m24s

This commit is contained in:
AzuTear
2026-06-30 08:45:28 +02:00
parent 1191abc914
commit 33ba00a682
11 changed files with 248 additions and 70 deletions
+87 -28
View File
@@ -155,10 +155,17 @@ public static class AdminSiteSettingsEndpoints
private static async Task<IResult> UpdateSiteSettings(
HttpContext context,
UpdateSiteSettingsRequest request,
UpdateSiteSettingsRequest? request,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
var requestValidationError = ValidateSiteSettingsRequest(request);
if (requestValidationError is not null)
{
return requestValidationError;
}
var validatedRequest = request!;
var session = AdminEndpointConventions.CurrentSession(context);
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
@@ -167,20 +174,20 @@ public static class AdminSiteSettingsEndpoints
return Results.NotFound();
}
var urlValidationError = NormalizeSiteSettingsUrls(request, out var normalizedUrls, out var socialLinks);
var urlValidationError = NormalizeSiteSettingsUrls(validatedRequest, out var normalizedUrls, out var socialLinks);
if (urlValidationError is not null)
{
return urlValidationError;
}
settings.HostDisplayName = request.HostDisplayName.Trim();
settings.HostTagline = request.HostTagline.Trim();
settings.HostArtistName = request.HostArtistName.Trim();
settings.HostDisplayName = validatedRequest.HostDisplayName.Trim();
settings.HostTagline = validatedRequest.HostTagline.Trim();
settings.HostArtistName = validatedRequest.HostArtistName.Trim();
settings.NewsletterUrl = normalizedUrls.NewsletterUrl;
settings.ShareXUrl = normalizedUrls.ShareXUrl;
settings.ShareDiscordUrl = normalizedUrls.ShareDiscordUrl;
settings.PrivacyEmail = request.PrivacyEmail.Trim();
var trimmedPrivacyContent = request.PrivacyPolicyContent.Trim();
settings.PrivacyEmail = validatedRequest.PrivacyEmail.Trim();
var trimmedPrivacyContent = validatedRequest.PrivacyPolicyContent.Trim();
var privacyChanged = !string.Equals(settings.PrivacyPolicyContent, trimmedPrivacyContent, StringComparison.Ordinal);
settings.PrivacyPolicyContent = trimmedPrivacyContent;
if (privacyChanged)
@@ -190,32 +197,32 @@ public static class AdminSiteSettingsEndpoints
}
settings.ImprintUrl = normalizedUrls.ImprintUrl;
settings.ImprintContent = request.ImprintContent.Trim();
settings.ImprintContent = validatedRequest.ImprintContent.Trim();
settings.ContactUrl = normalizedUrls.ContactUrl;
settings.ContactContent = request.ContactContent.Trim();
settings.ContactContent = validatedRequest.ContactContent.Trim();
settings.SponsorsUrl = normalizedUrls.SponsorsUrl;
settings.SponsorsContent = request.SponsorsContent.Trim();
settings.SponsorsContent = validatedRequest.SponsorsContent.Trim();
settings.ShowactsUrl = normalizedUrls.ShowactsUrl;
settings.ShowactsContent = request.ShowactsContent.Trim();
settings.StreamBannerEyebrow = SeasonMappings.NormalizePlainTextContent(request.StreamBannerEyebrow);
settings.StreamBannerTitle = SeasonMappings.NormalizePlainTextContent(request.StreamBannerTitle);
settings.StreamBannerText = SeasonMappings.NormalizePlainTextContent(request.StreamBannerText);
settings.StreamBannerLiveButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerLiveButtonLabel);
settings.ShowactsContent = validatedRequest.ShowactsContent.Trim();
settings.StreamBannerEyebrow = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerEyebrow);
settings.StreamBannerTitle = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerTitle);
settings.StreamBannerText = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerText);
settings.StreamBannerLiveButtonLabel = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerLiveButtonLabel);
settings.StreamBannerLiveButtonUrl = normalizedUrls.StreamBannerLiveButtonUrl;
settings.StreamBannerLockedButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerLockedButtonLabel);
settings.StreamBannerUseCompletedContent = request.StreamBannerUseCompletedContent;
settings.StreamBannerCompletedEyebrow = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedEyebrow);
settings.StreamBannerCompletedTitle = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedTitle);
settings.StreamBannerCompletedText = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedText);
settings.StreamBannerCompletedButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedButtonLabel);
settings.StreamBannerLockedButtonLabel = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerLockedButtonLabel);
settings.StreamBannerUseCompletedContent = validatedRequest.StreamBannerUseCompletedContent;
settings.StreamBannerCompletedEyebrow = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerCompletedEyebrow);
settings.StreamBannerCompletedTitle = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerCompletedTitle);
settings.StreamBannerCompletedText = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerCompletedText);
settings.StreamBannerCompletedButtonLabel = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerCompletedButtonLabel);
settings.StreamBannerCompletedButtonUrl = normalizedUrls.StreamBannerCompletedButtonUrl;
settings.AwardsSectionTitle = SeasonMappings.NormalizePlainTextContent(request.AwardsSectionTitle);
settings.AwardsSectionDescription = SeasonMappings.NormalizePlainTextContent(request.AwardsSectionDescription);
settings.SubcategoriesSectionTitle = SeasonMappings.NormalizePlainTextContent(request.SubcategoriesSectionTitle);
settings.SubcategoriesSectionDescription = SeasonMappings.NormalizePlainTextContent(request.SubcategoriesSectionDescription);
settings.AwardsSectionTitle = SeasonMappings.NormalizePlainTextContent(validatedRequest.AwardsSectionTitle);
settings.AwardsSectionDescription = SeasonMappings.NormalizePlainTextContent(validatedRequest.AwardsSectionDescription);
settings.SubcategoriesSectionTitle = SeasonMappings.NormalizePlainTextContent(validatedRequest.SubcategoriesSectionTitle);
settings.SubcategoriesSectionDescription = SeasonMappings.NormalizePlainTextContent(validatedRequest.SubcategoriesSectionDescription);
settings.SocialLinksJson = JsonSerializer.Serialize(socialLinks);
settings.FaqJson = JsonSerializer.Serialize(request.Faq ?? []);
settings.ShowactFormSchemaJson = request.ShowactFormSchemaJson ?? "[]";
settings.FaqJson = JsonSerializer.Serialize(validatedRequest.Faq);
settings.ShowactFormSchemaJson = validatedRequest.ShowactFormSchemaJson ?? "[]";
adminAuditService.AddEntry(
session.TwitchUserId,
@@ -228,7 +235,7 @@ public static class AdminSiteSettingsEndpoints
settings.HostDisplayName,
privacyChanged,
socialLinkCount = socialLinks.Length,
faqCount = request.Faq?.Length ?? 0,
faqCount = validatedRequest.Faq.Length,
},
RequestMetadataReader.Read(context));
@@ -340,6 +347,58 @@ public static class AdminSiteSettingsEndpoints
return null;
}
private static IResult? ValidateSiteSettingsRequest(UpdateSiteSettingsRequest? request)
{
if (request is null)
{
return Results.BadRequest(new { message = "Public Site Settings brauchen einen Request-Body." });
}
if (request.SocialLinks is null || request.Faq is null)
{
return Results.BadRequest(new { message = "Public Site Settings brauchen Social Links und FAQ im Request-Body." });
}
string?[] requiredTextFields =
[
request.HostDisplayName,
request.HostTagline,
request.HostArtistName,
request.NewsletterUrl,
request.ShareXUrl,
request.ShareDiscordUrl,
request.PrivacyEmail,
request.PrivacyPolicyContent,
request.ImprintUrl,
request.ImprintContent,
request.ContactUrl,
request.ContactContent,
request.SponsorsUrl,
request.SponsorsContent,
request.ShowactsUrl,
request.ShowactsContent,
request.StreamBannerEyebrow,
request.StreamBannerTitle,
request.StreamBannerText,
request.StreamBannerLiveButtonLabel,
request.StreamBannerLiveButtonUrl,
request.StreamBannerLockedButtonLabel,
request.StreamBannerCompletedEyebrow,
request.StreamBannerCompletedTitle,
request.StreamBannerCompletedText,
request.StreamBannerCompletedButtonLabel,
request.StreamBannerCompletedButtonUrl,
request.AwardsSectionTitle,
request.AwardsSectionDescription,
request.SubcategoriesSectionTitle,
request.SubcategoriesSectionDescription,
];
return requiredTextFields.Any(field => field is null)
? Results.BadRequest(new { message = "Public Site Settings brauchen alle Pflichtfelder im Request-Body." })
: null;
}
private static bool TryNormalizePublicUrl(string? value, string fieldName, out string normalizedUrl, out string errorMessage)
{
normalizedUrl = (value ?? string.Empty).Trim();