Validate screenshot URLs server- and client-side
This commit is contained in:
@@ -34,6 +34,16 @@ internal static class EndpointHelpers
|
|||||||
? t[..Math.Min(t.Length, max)]
|
? t[..Math.Min(t.Length, max)]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
public static bool IsValidImageUrl(string? url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url)) return true; // empty is acceptable
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
|
||||||
|
if (uri.Scheme is not ("http" or "https")) return false;
|
||||||
|
var path = uri.AbsolutePath.ToLowerInvariant();
|
||||||
|
return path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg")
|
||||||
|
|| path.EndsWith(".gif") || path.EndsWith(".webp") || path.EndsWith(".avif");
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<bool> IsAdmin(HttpContext ctx, AppDbContext db, IConfiguration config)
|
public static async Task<bool> IsAdmin(HttpContext ctx, AppDbContext db, IConfiguration config)
|
||||||
{
|
{
|
||||||
var player = await GetAuthenticatedPlayer(ctx, db);
|
var player = await GetAuthenticatedPlayer(ctx, db);
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ public static class SuggestEndpoints
|
|||||||
return Results.BadRequest(new { error = "Name is required and must be <= 100 characters." });
|
return Results.BadRequest(new { error = "Name is required and must be <= 100 characters." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!EndpointHelpers.IsValidImageUrl(request.ScreenshotUrl))
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new { error = "Screenshot URL must be http(s) and end with an image file extension." });
|
||||||
|
}
|
||||||
|
|
||||||
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||||
if (player is null) return Results.Unauthorized();
|
if (player is null) return Results.Unauthorized();
|
||||||
|
|
||||||
@@ -118,6 +123,11 @@ public static class SuggestEndpoints
|
|||||||
return Results.BadRequest(new { error = "Name is required and must be <= 100 characters." });
|
return Results.BadRequest(new { error = "Name is required and must be <= 100 characters." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!EndpointHelpers.IsValidImageUrl(request.ScreenshotUrl))
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new { error = "Screenshot URL must be http(s) and end with an image file extension." });
|
||||||
|
}
|
||||||
|
|
||||||
var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id);
|
var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id);
|
||||||
if (suggestion == null) return Results.NotFound(new { error = "Suggestion not found." });
|
if (suggestion == null) return Results.NotFound(new { error = "Suggestion not found." });
|
||||||
|
|
||||||
|
|||||||
@@ -296,6 +296,9 @@ function setupHandlers() {
|
|||||||
const form = e.target;
|
const form = e.target;
|
||||||
const data = Object.fromEntries(new FormData(form).entries());
|
const data = Object.fromEntries(new FormData(form).entries());
|
||||||
if (!data.name) return toast("Name required", true);
|
if (!data.name) return toast("Name required", true);
|
||||||
|
if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) {
|
||||||
|
return toast("Screenshot URL must be http(s) and end with an image file.", true);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await api.createSuggestion(data);
|
await api.createSuggestion(data);
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -468,6 +471,9 @@ function openEditModal(s) {
|
|||||||
form?.addEventListener("submit", async (e) => {
|
form?.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = Object.fromEntries(new FormData(form).entries());
|
const data = Object.fromEntries(new FormData(form).entries());
|
||||||
|
if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) {
|
||||||
|
return toast("Screenshot URL must be http(s) and end with an image file.", true);
|
||||||
|
}
|
||||||
if (!data.name?.trim()) return toast("Name required", true);
|
if (!data.name?.trim()) return toast("Name required", true);
|
||||||
try {
|
try {
|
||||||
await api.updateSuggestion(s.id, data);
|
await api.updateSuggestion(s.id, data);
|
||||||
@@ -516,3 +522,16 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
||||||
|
function isValidImageUrl(url) {
|
||||||
|
if (!url) return true;
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
const allowed = ["http:", "https:"];
|
||||||
|
if (!allowed.includes(u.protocol)) return false;
|
||||||
|
const path = u.pathname.toLowerCase();
|
||||||
|
return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif"].some(ext => path.endsWith(ext));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user