266 lines
10 KiB
C#
266 lines
10 KiB
C#
using GameList.Contracts;
|
|
using GameList.Data;
|
|
using GameList.Domain;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace GameList.Endpoints;
|
|
|
|
public static class SuggestEndpoints
|
|
{
|
|
public static void MapSuggestEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
app.MapGet("/api/suggestions/mine", async (HttpContext ctx, AppDbContext db) =>
|
|
{
|
|
var phase = await EndpointHelpers.GetPhase(db);
|
|
if (phase != Phase.Suggest)
|
|
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
|
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null) return Results.Unauthorized();
|
|
var mine = await db.Suggestions.AsNoTracking()
|
|
.Where(s => s.PlayerId == player.Id)
|
|
.Select(s => new
|
|
{
|
|
s.Id,
|
|
s.Name,
|
|
s.Genre,
|
|
s.Description,
|
|
s.ScreenshotUrl,
|
|
s.YoutubeUrl,
|
|
s.GameUrl,
|
|
s.CreatedAt,
|
|
s.MinPlayers,
|
|
s.MaxPlayers
|
|
})
|
|
.ToListAsync();
|
|
|
|
var ordered = mine
|
|
.OrderBy(s => s.CreatedAt)
|
|
.Select(s => new SuggestionDto(s.Id, s.Name, s.Genre, s.Description, s.ScreenshotUrl, s.YoutubeUrl, s.GameUrl, s.MinPlayers, s.MaxPlayers));
|
|
|
|
return Results.Ok(ordered);
|
|
});
|
|
|
|
app.MapPost("/api/suggestions", async ([FromBody] SuggestionRequest request, HttpContext ctx, AppDbContext db, IHttpClientFactory http) =>
|
|
{
|
|
var phase = await EndpointHelpers.GetPhase(db);
|
|
if (phase != Phase.Suggest)
|
|
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
|
|
|
if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 100)
|
|
{
|
|
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." });
|
|
}
|
|
if (!await EndpointHelpers.IsReachableImageAsync(request.ScreenshotUrl, http))
|
|
{
|
|
return Results.BadRequest(new { error = "Screenshot URL could not be validated as an image." });
|
|
}
|
|
|
|
if (!ValidatePlayers(request.MinPlayers, request.MaxPlayers, out var playersError))
|
|
return Results.BadRequest(new { error = playersError });
|
|
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null) return Results.Unauthorized();
|
|
|
|
if (string.IsNullOrWhiteSpace(player.DisplayName))
|
|
{
|
|
return Results.BadRequest(new { error = "Set a display name before submitting suggestions." });
|
|
}
|
|
|
|
var existingCount = await db.Suggestions.CountAsync(s => s.PlayerId == player.Id);
|
|
if (existingCount >= 3)
|
|
{
|
|
return Results.BadRequest(new { error = "You have reached the 3 suggestion limit." });
|
|
}
|
|
|
|
var suggestion = new Suggestion
|
|
{
|
|
PlayerId = player.Id,
|
|
Name = request.Name.Trim(),
|
|
Genre = EndpointHelpers.TrimTo(request.Genre, 50),
|
|
Description = EndpointHelpers.TrimTo(request.Description, 500),
|
|
ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048),
|
|
YoutubeUrl = EndpointHelpers.TrimTo(request.YoutubeUrl, 2048),
|
|
GameUrl = EndpointHelpers.TrimTo(request.GameUrl, 2048),
|
|
MinPlayers = request.MinPlayers,
|
|
MaxPlayers = request.MaxPlayers
|
|
};
|
|
|
|
db.Suggestions.Add(suggestion);
|
|
await db.SaveChangesAsync();
|
|
|
|
return Results.Created($"/api/suggestions/{suggestion.Id}", new { suggestion.Id });
|
|
});
|
|
|
|
app.MapDelete("/api/suggestions/{id:int}", async (int id, HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
|
{
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null) return Results.Unauthorized();
|
|
var isAdmin = await EndpointHelpers.IsAdmin(ctx, db, config);
|
|
|
|
var phase = await EndpointHelpers.GetPhase(db);
|
|
if (!isAdmin && phase != Phase.Suggest)
|
|
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
|
|
|
var suggestion = isAdmin
|
|
? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id)
|
|
: await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id && s.PlayerId == player.Id);
|
|
if (suggestion == null)
|
|
return Results.NotFound(new { error = "Suggestion not found." });
|
|
|
|
db.Suggestions.Remove(suggestion);
|
|
await db.SaveChangesAsync();
|
|
return Results.NoContent();
|
|
});
|
|
|
|
app.MapPut("/api/suggestions/{id:int}", async (int id, [FromBody] SuggestionRequest request, HttpContext ctx, AppDbContext db, IConfiguration config, IHttpClientFactory http) =>
|
|
{
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
var isAdmin = await EndpointHelpers.IsAdmin(ctx, db, config);
|
|
|
|
if (!isAdmin)
|
|
{
|
|
if (player is null) return Results.Unauthorized();
|
|
|
|
var phase = await EndpointHelpers.GetPhase(db);
|
|
if (phase != Phase.Suggest)
|
|
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 100)
|
|
{
|
|
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." });
|
|
}
|
|
if (!await EndpointHelpers.IsReachableImageAsync(request.ScreenshotUrl, http))
|
|
{
|
|
return Results.BadRequest(new { error = "Screenshot URL could not be validated as an image." });
|
|
}
|
|
|
|
if (!ValidatePlayers(request.MinPlayers, request.MaxPlayers, out var playersError))
|
|
return Results.BadRequest(new { error = playersError });
|
|
|
|
var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id);
|
|
if (suggestion == null) return Results.NotFound(new { error = "Suggestion not found." });
|
|
|
|
if (!isAdmin)
|
|
{
|
|
if (suggestion.PlayerId != player!.Id)
|
|
return Results.Unauthorized();
|
|
}
|
|
|
|
suggestion.Name = request.Name.Trim();
|
|
suggestion.Genre = EndpointHelpers.TrimTo(request.Genre, 50);
|
|
suggestion.Description = EndpointHelpers.TrimTo(request.Description, 500);
|
|
suggestion.ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048);
|
|
suggestion.YoutubeUrl = EndpointHelpers.TrimTo(request.YoutubeUrl, 2048);
|
|
suggestion.GameUrl = EndpointHelpers.TrimTo(request.GameUrl, 2048);
|
|
suggestion.MinPlayers = request.MinPlayers;
|
|
suggestion.MaxPlayers = request.MaxPlayers;
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Results.Ok(new
|
|
{
|
|
suggestion.Id,
|
|
suggestion.Name,
|
|
suggestion.Genre,
|
|
suggestion.Description,
|
|
suggestion.ScreenshotUrl,
|
|
suggestion.YoutubeUrl,
|
|
suggestion.GameUrl,
|
|
suggestion.MinPlayers,
|
|
suggestion.MaxPlayers
|
|
});
|
|
});
|
|
|
|
app.MapGet("/api/suggestions/all", async (HttpContext ctx, AppDbContext db) =>
|
|
{
|
|
var phase = await EndpointHelpers.GetPhase(db);
|
|
if (phase < Phase.Reveal)
|
|
return EndpointHelpers.PhaseMismatch(Phase.Reveal, phase);
|
|
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null) return Results.Unauthorized();
|
|
|
|
var all = await db.Suggestions.AsNoTracking()
|
|
.Include(s => s.Player)
|
|
.Select(s => new
|
|
{
|
|
s.Id,
|
|
s.Name,
|
|
s.Genre,
|
|
s.Description,
|
|
s.ScreenshotUrl,
|
|
s.YoutubeUrl,
|
|
s.GameUrl,
|
|
s.MinPlayers,
|
|
s.MaxPlayers,
|
|
Author = s.Player!.DisplayName,
|
|
s.CreatedAt
|
|
})
|
|
.ToListAsync();
|
|
|
|
var ordered = all
|
|
.OrderBy(s => s.CreatedAt)
|
|
.Select(s => new
|
|
{
|
|
s.Id,
|
|
s.Name,
|
|
s.Genre,
|
|
s.Description,
|
|
s.ScreenshotUrl,
|
|
s.YoutubeUrl,
|
|
s.GameUrl,
|
|
s.MinPlayers,
|
|
s.MaxPlayers,
|
|
s.Author
|
|
});
|
|
|
|
return Results.Ok(ordered);
|
|
});
|
|
}
|
|
|
|
private static bool ValidatePlayers(int? minPlayers, int? maxPlayers, out string? error)
|
|
{
|
|
error = null;
|
|
if (minPlayers is null && maxPlayers is null) return true;
|
|
|
|
if (minPlayers is not null && (minPlayers < 1 || minPlayers > 32))
|
|
{
|
|
error = "Min players must be between 1 and 32.";
|
|
return false;
|
|
}
|
|
|
|
if (maxPlayers is not null && (maxPlayers < 1 || maxPlayers > 32))
|
|
{
|
|
error = "Max players must be between 1 and 32.";
|
|
return false;
|
|
}
|
|
|
|
if (minPlayers is null || maxPlayers is null)
|
|
{
|
|
error = "Provide both min and max players.";
|
|
return false;
|
|
}
|
|
|
|
if (minPlayers > maxPlayers)
|
|
{
|
|
error = "Min players cannot exceed max players.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|