From 33ba00a682234109a49d524cc17571662397d445 Mon Sep 17 00:00:00 2001 From: AzuTear Date: Tue, 30 Jun 2026 08:45:28 +0200 Subject: [PATCH] Harden request validation and admin UX cues --- Backend/Endpoints/AdminRiskFlagEndpoints.cs | 9 +- .../Endpoints/AdminSiteSettingsEndpoints.cs | 115 +++++++++++++----- Backend/Endpoints/AuthTeamLoginEndpoints.cs | 15 ++- Backend/Endpoints/PublicClipEndpoints.cs | 27 ++-- Backend/Endpoints/PublicEndpointSupport.cs | 71 +++++++++++ Backend/Endpoints/PublicHostImageEndpoints.cs | 2 +- .../Endpoints/PublicNominationEndpoints.cs | 17 ++- Backend/Endpoints/PublicVoteEndpoints.cs | 36 +++--- .../views/admin/AdminAccessSettingsView.vue | 10 +- frontend/src/views/admin/AdminRiskView.vue | 6 + .../src/views/admin/AdminSettingsView.vue | 10 +- 11 files changed, 248 insertions(+), 70 deletions(-) diff --git a/Backend/Endpoints/AdminRiskFlagEndpoints.cs b/Backend/Endpoints/AdminRiskFlagEndpoints.cs index 844c7e3..eda9579 100644 --- a/Backend/Endpoints/AdminRiskFlagEndpoints.cs +++ b/Backend/Endpoints/AdminRiskFlagEndpoints.cs @@ -151,10 +151,15 @@ public static partial class AdminModerationEndpoints private static async Task BulkResolveRiskFlags( HttpContext context, - BulkResolveRiskFlagsRequest request, + BulkResolveRiskFlagsRequest? request, AwardsDbContext db, IAdminAuditService adminAuditService) { + if (request is null || request.RiskFlagIds is null) + { + return Results.BadRequest(new { message = "Bulk-Verarbeitung braucht mindestens einen Risikohinweis." }); + } + var session = AdminEndpointConventions.CurrentSession(context); var normalizedIds = request.RiskFlagIds .Distinct() @@ -188,7 +193,7 @@ public static partial class AdminModerationEndpoints if (riskFlags.Any(item => item.Severity != "low")) { - return Results.BadRequest(new { message = "Bulk-Verarbeitung ist nur fuer Low-Severity-Hinweise erlaubt." }); + return Results.BadRequest(new { message = "Bulk-Verarbeitung ist nur fuer offene Low-Severity-Hinweise erlaubt." }); } if (riskFlags.Any(item => item.Status != "open")) diff --git a/Backend/Endpoints/AdminSiteSettingsEndpoints.cs b/Backend/Endpoints/AdminSiteSettingsEndpoints.cs index 06cba79..8582214 100644 --- a/Backend/Endpoints/AdminSiteSettingsEndpoints.cs +++ b/Backend/Endpoints/AdminSiteSettingsEndpoints.cs @@ -155,10 +155,17 @@ public static class AdminSiteSettingsEndpoints private static async Task 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(); diff --git a/Backend/Endpoints/AuthTeamLoginEndpoints.cs b/Backend/Endpoints/AuthTeamLoginEndpoints.cs index fe03b91..3c6bede 100644 --- a/Backend/Endpoints/AuthTeamLoginEndpoints.cs +++ b/Backend/Endpoints/AuthTeamLoginEndpoints.cs @@ -15,16 +15,21 @@ public static partial class AuthEndpoints private static async Task TeamLogin( HttpContext context, - TeamLoginRequest request, + TeamLoginRequest? request, AwardsDbContext db, IUserSessionService userSessionService) { - var login = NormalizeTeamLogin(request.Login); + if (request is null) + { + return Results.BadRequest(new { message = "Login und Passwort sind erforderlich." }); + } + + var login = NormalizeTeamLogin(request.Login ?? string.Empty); var password = request.Password ?? string.Empty; if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password)) { - return Results.Unauthorized(); + return Results.BadRequest(new { message = "Login und Passwort sind erforderlich." }); } var member = await db.TeamMembers.FirstOrDefaultAsync(item => item.Login == login, context.RequestAborted); @@ -120,8 +125,8 @@ public static partial class AuthEndpoints : await db.TeamMembers.FirstOrDefaultAsync(item => item.BoundTwitchUserId == twitchUserId, cancellationToken); } - private static string NormalizeTeamLogin(string value) => - value.Trim().TrimStart('@').ToLowerInvariant(); + private static string NormalizeTeamLogin(string? value) => + (value ?? string.Empty).Trim().TrimStart('@').ToLowerInvariant(); private static string NormalizeTwitchUserId(string? value) => (value ?? string.Empty).Trim().TrimStart('@').ToLowerInvariant(); diff --git a/Backend/Endpoints/PublicClipEndpoints.cs b/Backend/Endpoints/PublicClipEndpoints.cs index eb96113..bde1866 100644 --- a/Backend/Endpoints/PublicClipEndpoints.cs +++ b/Backend/Endpoints/PublicClipEndpoints.cs @@ -11,13 +11,20 @@ public static partial class PublicEndpoints { private static async Task CreateClip( HttpContext context, - CreateClipRequest request, + CreateClipRequest? request, AwardsDbContext db, IUserSessionService userSessionService, IRiskFlagService riskFlagService, IRiskRuleService riskRuleService) { - if (!TryNormalizeExternalUrl(request.ClipUrl, out var clipUrl)) + var validationError = ValidateCreateClipRequest(request); + if (validationError is not null) + { + return validationError; + } + + var validatedRequest = request!; + if (!TryNormalizeExternalUrl(validatedRequest.ClipUrl, out var clipUrl)) { return Results.BadRequest(new { message = "A valid http(s) clip link is required." }); } @@ -44,7 +51,7 @@ public static partial class PublicEndpoints return Results.BadRequest(new { message }); } - var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == request.Year); + var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == validatedRequest.Year); var clipSeasonResolution = EnsurePublicWriteSeason(season, "nomination"); if (clipSeasonResolution.Result is not null) { @@ -53,26 +60,26 @@ public static partial class PublicEndpoints season = clipSeasonResolution.Season!; - var selectedCandidate = request.CandidateId is int candidateId + var selectedCandidate = validatedRequest.CandidateId is int candidateId ? await db.Candidates .AsNoTracking() .FirstOrDefaultAsync(item => item.Id == candidateId && item.SeasonId == season.Id) : null; - if (request.CandidateId is not null && selectedCandidate is null) + if (validatedRequest.CandidateId is not null && selectedCandidate is null) { return Results.BadRequest(new { message = "The selected candidate does not exist for this season." }); } - if (request.CategoryId is int requestedCategoryId + if (validatedRequest.CategoryId is int requestedCategoryId && selectedCandidate is not null && selectedCandidate.CategoryId != requestedCategoryId) { return Results.BadRequest(new { message = "The selected candidate does not belong to the selected category." }); } - var resolvedCategoryId = request.CategoryId ?? selectedCandidate?.CategoryId; - var normalizedTitle = request.Title?.Trim() ?? string.Empty; - var submittedCreator = request.Creator?.Trim(); + var resolvedCategoryId = validatedRequest.CategoryId ?? selectedCandidate?.CategoryId; + var normalizedTitle = validatedRequest.Title?.Trim() ?? string.Empty; + var submittedCreator = validatedRequest.Creator?.Trim(); var normalizedCreator = string.IsNullOrWhiteSpace(submittedCreator) ? selectedCandidate?.DisplayName ?? string.Empty : submittedCreator; @@ -96,7 +103,7 @@ public static partial class PublicEndpoints } } - var submitterIdResult = await ResolveSubmitterIdAsync(context, request.TwitchUserId, userSessionService); + var submitterIdResult = await ResolveSubmitterIdAsync(context, validatedRequest.TwitchUserId, userSessionService); if (submitterIdResult.Result is not null) { return submitterIdResult.Result; diff --git a/Backend/Endpoints/PublicEndpointSupport.cs b/Backend/Endpoints/PublicEndpointSupport.cs index 37ea201..392b132 100644 --- a/Backend/Endpoints/PublicEndpointSupport.cs +++ b/Backend/Endpoints/PublicEndpointSupport.cs @@ -1,4 +1,5 @@ using Backend.Common; +using Backend.Contracts; using Backend.Domain; using Backend.Services; using Microsoft.AspNetCore.Http.Extensions; @@ -205,4 +206,74 @@ public static partial class PublicEndpoints "show" => "show", _ => "current", }; + + private static IResult? ValidateCreateNominationRequest(CreateNominationRequest? request) + { + if (request is null) + { + return Results.BadRequest(new { message = "A nomination request body is required." }); + } + + if (request.Year <= 0) + { + return Results.BadRequest(new { message = "A valid season year is required." }); + } + + if ((request.Nominations?.Length ?? 0) == 0 && (request.Nominees?.Length ?? 0) == 0) + { + return Results.BadRequest(new { message = "A nomination request must include at least one stream link." }); + } + + return null; + } + + private static IResult? ValidateCreateVoteRequest(CreateVoteRequest? request) + { + if (request is null) + { + return Results.BadRequest(new { message = "A vote request body is required." }); + } + + if (request.SeasonId <= 0) + { + return Results.BadRequest(new { message = "A valid season id is required." }); + } + + if (request.Entries is null || request.Entries.Length == 0) + { + return Results.BadRequest(new { message = "At least one vote entry is required." }); + } + + if (request.Entries.Any(item => item.CategoryId <= 0 || item.CandidateId <= 0)) + { + return Results.BadRequest(new { message = "Each vote entry requires a valid category and candidate." }); + } + + return null; + } + + private static IResult? ValidateCreateClipRequest(CreateClipRequest? request) + { + if (request is null) + { + return Results.BadRequest(new { message = "A clip request body is required." }); + } + + if (request.Year <= 0) + { + return Results.BadRequest(new { message = "A valid season year is required." }); + } + + if (request.CategoryId is <= 0 || request.CandidateId is <= 0) + { + return Results.BadRequest(new { message = "Category and candidate ids must be positive when provided." }); + } + + if (string.IsNullOrWhiteSpace(request.ClipUrl)) + { + return Results.BadRequest(new { message = "A valid http(s) clip link is required." }); + } + + return null; + } } diff --git a/Backend/Endpoints/PublicHostImageEndpoints.cs b/Backend/Endpoints/PublicHostImageEndpoints.cs index 2e5b5a7..a54c1fd 100644 --- a/Backend/Endpoints/PublicHostImageEndpoints.cs +++ b/Backend/Endpoints/PublicHostImageEndpoints.cs @@ -21,7 +21,7 @@ public static partial class PublicEndpoints if (settings?.HostImageData is not { Length: > 0 } imageData || string.IsNullOrWhiteSpace(settings.HostImageContentType)) { - return Results.NotFound(); + return Results.Redirect("/assets/amaterasu2sei_2.png", permanent: false); } var entityTag = settings.HostImageUpdatedAt?.ToUnixTimeSeconds().ToString(System.Globalization.CultureInfo.InvariantCulture) diff --git a/Backend/Endpoints/PublicNominationEndpoints.cs b/Backend/Endpoints/PublicNominationEndpoints.cs index cbb5206..c7d76cc 100644 --- a/Backend/Endpoints/PublicNominationEndpoints.cs +++ b/Backend/Endpoints/PublicNominationEndpoints.cs @@ -11,14 +11,21 @@ public static partial class PublicEndpoints { private static async Task CreateNomination( HttpContext context, - CreateNominationRequest request, + CreateNominationRequest? request, AwardsDbContext db, IUserSessionService userSessionService, IRiskFlagService riskFlagService, IRiskRuleService riskRuleService, NominationEnrichmentService nominationEnrichmentService) { - var submittedNominations = NormalizeSubmittedNominations(request); + var validationError = ValidateCreateNominationRequest(request); + if (validationError is not null) + { + return validationError; + } + + var validatedRequest = request!; + var submittedNominations = NormalizeSubmittedNominations(validatedRequest); if (submittedNominations.Length == 0) { @@ -66,14 +73,14 @@ public static partial class PublicEndpoints return Results.BadRequest(new { message = "Dieser Link kann nicht nominiert werden. Bitte reiche einen direkten Kanal- oder Profil-Link ein." }); } - var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == request.Year); + var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == validatedRequest.Year); if (season is null) { return Results.BadRequest(new { message = "The selected season does not exist." }); } - var categoryGroupName = await ResolveCategoryGroupNameAsync(db, season.Id, request, context.RequestAborted); + var categoryGroupName = await ResolveCategoryGroupNameAsync(db, season.Id, validatedRequest, context.RequestAborted); if (string.IsNullOrWhiteSpace(categoryGroupName)) { return Results.BadRequest(new { message = "The selected category group does not exist for this season." }); @@ -102,7 +109,7 @@ public static partial class PublicEndpoints return nominationSeasonResolution.Result; } - var submitterIdResult = await ResolveSubmitterIdAsync(context, request.TwitchUserId, userSessionService); + var submitterIdResult = await ResolveSubmitterIdAsync(context, validatedRequest.TwitchUserId, userSessionService); if (submitterIdResult.Result is not null) { return submitterIdResult.Result; diff --git a/Backend/Endpoints/PublicVoteEndpoints.cs b/Backend/Endpoints/PublicVoteEndpoints.cs index 3648a20..3d0beb4 100644 --- a/Backend/Endpoints/PublicVoteEndpoints.cs +++ b/Backend/Endpoints/PublicVoteEndpoints.cs @@ -12,37 +12,39 @@ public static partial class PublicEndpoints { private static async Task CreateVote( HttpContext context, - CreateVoteRequest request, + CreateVoteRequest? request, AwardsDbContext db, IUserSessionService userSessionService, IRiskFlagService riskFlagService, IRiskRuleService riskRuleService) { - if (request.Entries.Length == 0) + var validationError = ValidateCreateVoteRequest(request); + if (validationError is not null) { - return Results.BadRequest(new { message = "At least one vote entry is required." }); + return validationError; } - var distinctCategoryCount = request.Entries + var validatedRequest = request!; + var distinctCategoryCount = validatedRequest.Entries .Select(item => item.CategoryId) .Distinct() .Count(); - if (distinctCategoryCount != request.Entries.Length) + if (distinctCategoryCount != validatedRequest.Entries.Length) { return Results.BadRequest(new { message = "Only one vote entry per category is allowed." }); } var season = await db.Seasons .AsNoTracking() - .FirstOrDefaultAsync(item => item.Id == request.SeasonId); + .FirstOrDefaultAsync(item => item.Id == validatedRequest.SeasonId); var voteSeasonResolution = EnsurePublicWriteSeason(season, "voting"); if (voteSeasonResolution.Result is not null) { return voteSeasonResolution.Result; } - var submitterIdResult = await ResolveSubmitterIdAsync(context, request.TwitchUserId, userSessionService); + var submitterIdResult = await ResolveSubmitterIdAsync(context, validatedRequest.TwitchUserId, userSessionService); if (submitterIdResult.Result is not null) { return submitterIdResult.Result; @@ -50,10 +52,10 @@ public static partial class PublicEndpoints var submitterId = submitterIdResult.SubmitterId!; var requestMetadata = RequestMetadataReader.Read(context); - var candidateIds = request.Entries.Select(item => item.CandidateId).Distinct().ToArray(); + var candidateIds = validatedRequest.Entries.Select(item => item.CandidateId).Distinct().ToArray(); var validCandidates = await db.Candidates .AsNoTracking() - .Where(item => item.SeasonId == request.SeasonId && candidateIds.Contains(item.Id)) + .Where(item => item.SeasonId == validatedRequest.SeasonId && candidateIds.Contains(item.Id)) .Select(item => new { item.Id, item.CategoryId }) .ToArrayAsync(); @@ -63,17 +65,17 @@ public static partial class PublicEndpoints } var candidateCategoryMap = validCandidates.ToDictionary(item => item.Id, item => item.CategoryId); - if (request.Entries.Any(item => candidateCategoryMap[item.CandidateId] != item.CategoryId)) + if (validatedRequest.Entries.Any(item => candidateCategoryMap[item.CandidateId] != item.CategoryId)) { return Results.BadRequest(new { message = "A selected candidate does not match the submitted category." }); } var ballot = await db.VoteBallots .Include(item => item.Entries) - .FirstOrDefaultAsync(item => item.SeasonId == request.SeasonId && item.SubmittedByTwitchId == submitterId); + .FirstOrDefaultAsync(item => item.SeasonId == validatedRequest.SeasonId && item.SubmittedByTwitchId == submitterId); var isResubmission = ballot is not null; - ballot = await ApplyVoteEntriesAsync(db, ballot, request.SeasonId, submitterId, request.Entries, context.RequestAborted); + ballot = await ApplyVoteEntriesAsync(db, ballot, validatedRequest.SeasonId, submitterId, validatedRequest.Entries, context.RequestAborted); var resubmittedBallotRule = await riskRuleService.GetRuleAsync("resubmitted_ballot", context.RequestAborted); var rapidVoteUpdatesRule = await riskRuleService.GetRuleAsync("rapid_vote_updates", context.RequestAborted); @@ -90,9 +92,9 @@ public static partial class PublicEndpoints db.ChangeTracker.Clear(); ballot = await db.VoteBallots .Include(item => item.Entries) - .FirstAsync(item => item.SeasonId == request.SeasonId && item.SubmittedByTwitchId == submitterId, context.RequestAborted); + .FirstAsync(item => item.SeasonId == validatedRequest.SeasonId && item.SubmittedByTwitchId == submitterId, context.RequestAborted); isResubmission = true; - ballot = await ApplyVoteEntriesAsync(db, ballot, request.SeasonId, submitterId, request.Entries, context.RequestAborted); + ballot = await ApplyVoteEntriesAsync(db, ballot, validatedRequest.SeasonId, submitterId, validatedRequest.Entries, context.RequestAborted); await db.SaveChangesAsync(context.RequestAborted); } @@ -107,21 +109,21 @@ public static partial class PublicEndpoints if (isResubmission && resubmittedBallotRule.Enabled) { await riskFlagService.AddIfMissingAsync( - request.SeasonId, + validatedRequest.SeasonId, submitterId, "vote", "resubmitted_ballot", resubmittedBallotRule.Severity, "Ein User hat sein Ballot erneut gespeichert oder aktualisiert.", requestMetadata, - new { ballotId = ballot.Id, entryCount = request.Entries.Length, entityLinks = new[] { ballotLink } }, + new { ballotId = ballot.Id, entryCount = validatedRequest.Entries.Length, entityLinks = new[] { ballotLink } }, context.RequestAborted); } if (rapidVoteUpdatesRule.Enabled && recentVoteSubmissions >= rapidVoteUpdatesRule.Threshold) { await riskFlagService.AddIfMissingAsync( - request.SeasonId, + validatedRequest.SeasonId, submitterId, "vote", "rapid_vote_updates", diff --git a/frontend/src/views/admin/AdminAccessSettingsView.vue b/frontend/src/views/admin/AdminAccessSettingsView.vue index 3dcb455..213b731 100644 --- a/frontend/src/views/admin/AdminAccessSettingsView.vue +++ b/frontend/src/views/admin/AdminAccessSettingsView.vue @@ -1,5 +1,5 @@