Reduce frontend polling load and clean stale UI hooks
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace GameList.Endpoints;
|
||||
|
||||
internal static class SuggestionValidator
|
||||
{
|
||||
public static async Task<string?> ValidateAsync(SuggestionInput input, IHttpClientFactory httpFactory)
|
||||
private static readonly ConcurrentDictionary<string, (bool Reachable, DateTimeOffset ExpiresAt)> ImageReachabilityCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly TimeSpan ReachableCacheTtl = TimeSpan.FromMinutes(15);
|
||||
private static readonly TimeSpan UnreachableCacheTtl = TimeSpan.FromMinutes(2);
|
||||
|
||||
public static async Task<string?> ValidateAsync(SuggestionInput input, IHttpClientFactory httpFactory, bool shouldValidateImageReachability = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.Name) || input.Name.Length > 100)
|
||||
return "Name is required and must be <= 100 characters.";
|
||||
@@ -10,7 +16,7 @@ internal static class SuggestionValidator
|
||||
if (!EndpointHelpers.IsValidImageUrl(input.ScreenshotUrl))
|
||||
return "Screenshot URL must be http(s) and end with an image file extension.";
|
||||
|
||||
if (!await EndpointHelpers.IsReachableImageAsync(input.ScreenshotUrl, httpFactory))
|
||||
if (shouldValidateImageReachability && !await IsReachableImageCachedAsync(input.ScreenshotUrl, httpFactory))
|
||||
return "Screenshot URL could not be validated as an image. Use a public image link (http/https, no redirects, max 5 MB).";
|
||||
|
||||
if (!EndpointHelpers.IsValidHttpUrl(input.GameUrl))
|
||||
@@ -22,6 +28,21 @@ internal static class SuggestionValidator
|
||||
return ValidatePlayers(input.MinPlayers, input.MaxPlayers);
|
||||
}
|
||||
|
||||
private static async Task<bool> IsReachableImageCachedAsync(string? url, IHttpClientFactory httpFactory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return true;
|
||||
|
||||
var normalized = url.Trim();
|
||||
if (ImageReachabilityCache.TryGetValue(normalized, out var cached) && cached.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
return cached.Reachable;
|
||||
|
||||
var reachable = await EndpointHelpers.IsReachableImageAsync(normalized, httpFactory);
|
||||
var ttl = reachable ? ReachableCacheTtl : UnreachableCacheTtl;
|
||||
ImageReachabilityCache[normalized] = (reachable, DateTimeOffset.UtcNow.Add(ttl));
|
||||
return reachable;
|
||||
}
|
||||
|
||||
private static string? ValidatePlayers(int? minPlayers, int? maxPlayers)
|
||||
{
|
||||
if (minPlayers is null && maxPlayers is null)
|
||||
|
||||
@@ -145,10 +145,6 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
||||
|
||||
public async Task<ServiceResult<SuggestionUpdatedResponse>> UpdateAsync(Guid playerId, int suggestionId, SuggestionInput input)
|
||||
{
|
||||
var validationError = await SuggestionValidator.ValidateAsync(input, httpFactory);
|
||||
if (validationError is not null)
|
||||
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.BadRequest(validationError));
|
||||
|
||||
var actor = await db.Players
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == playerId)
|
||||
@@ -162,6 +158,11 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
||||
if (suggestion == null)
|
||||
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.NotFound("Suggestion not found."));
|
||||
|
||||
var shouldValidateScreenshot = ShouldValidateScreenshotReachability(input.ScreenshotUrl, suggestion.ScreenshotUrl);
|
||||
var validationError = await SuggestionValidator.ValidateAsync(input, httpFactory, shouldValidateScreenshot);
|
||||
if (validationError is not null)
|
||||
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.BadRequest(validationError));
|
||||
|
||||
var isAdmin = actor.IsAdmin;
|
||||
if (!isAdmin)
|
||||
{
|
||||
@@ -269,4 +270,10 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
||||
suggestion.MinPlayers = input.MinPlayers;
|
||||
suggestion.MaxPlayers = input.MaxPlayers;
|
||||
}
|
||||
|
||||
private static bool ShouldValidateScreenshotReachability(string? requestedScreenshotUrl, string? existingScreenshotUrl)
|
||||
{
|
||||
var normalizedRequested = EndpointHelpers.TrimTo(requestedScreenshotUrl, 2048);
|
||||
return !string.Equals(normalizedRequested, existingScreenshotUrl, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user