Harden request validation and admin UX cues
This commit is contained in:
@@ -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