Harden request validation and admin UX cues
This commit is contained in:
@@ -151,10 +151,15 @@ public static partial class AdminModerationEndpoints
|
||||
|
||||
private static async Task<IResult> 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"))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -15,16 +15,21 @@ public static partial class AuthEndpoints
|
||||
|
||||
private static async Task<IResult> 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();
|
||||
|
||||
@@ -11,13 +11,20 @@ public static partial class PublicEndpoints
|
||||
{
|
||||
private static async Task<IResult> 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,14 +11,21 @@ public static partial class PublicEndpoints
|
||||
{
|
||||
private static async Task<IResult> 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;
|
||||
|
||||
@@ -12,37 +12,39 @@ public static partial class PublicEndpoints
|
||||
{
|
||||
private static async Task<IResult> 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",
|
||||
|
||||
Reference in New Issue
Block a user