Refactor endpoint services to accept narrow inputs
This commit is contained in:
@@ -14,7 +14,7 @@ public static class AdminEndpoints
|
|||||||
|
|
||||||
admin.MapPost("/results", async ([FromBody] ResultsOpenRequest request, AdminWorkflowService service) =>
|
admin.MapPost("/results", async ([FromBody] ResultsOpenRequest request, AdminWorkflowService service) =>
|
||||||
{
|
{
|
||||||
return await service.SetResultsOpenAsync(request);
|
return await service.SetResultsOpenAsync(request.ResultsOpen);
|
||||||
});
|
});
|
||||||
|
|
||||||
admin.MapGet("/vote-status", async (AdminWorkflowService service) =>
|
admin.MapGet("/vote-status", async (AdminWorkflowService service) =>
|
||||||
@@ -24,7 +24,7 @@ public static class AdminEndpoints
|
|||||||
|
|
||||||
admin.MapPost("/joker", async ([FromBody] GrantJokerRequest request, AdminWorkflowService service) =>
|
admin.MapPost("/joker", async ([FromBody] GrantJokerRequest request, AdminWorkflowService service) =>
|
||||||
{
|
{
|
||||||
return await service.GrantJokerAsync(request);
|
return await service.GrantJokerAsync(request.PlayerId);
|
||||||
});
|
});
|
||||||
|
|
||||||
admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, AdminWorkflowService service) =>
|
admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, AdminWorkflowService service) =>
|
||||||
@@ -38,7 +38,7 @@ public static class AdminEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
return await service.LinkSuggestionsAsync(player, request);
|
return await service.LinkSuggestionsAsync(player.Id, request.SourceSuggestionId, request.TargetSuggestionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
admin.MapPost("/unlink-suggestions", async ([FromBody] UnlinkSuggestionsRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
|
admin.MapPost("/unlink-suggestions", async ([FromBody] UnlinkSuggestionsRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
|
||||||
@@ -47,7 +47,7 @@ public static class AdminEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
return await service.UnlinkSuggestionsAsync(player, request);
|
return await service.UnlinkSuggestionsAsync(player.Id, request.SuggestionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
admin.MapPost("/reset", async (AdminWorkflowService service) =>
|
admin.MapPost("/reset", async (AdminWorkflowService service) =>
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ namespace GameList.Endpoints;
|
|||||||
|
|
||||||
internal sealed class AdminWorkflowService(AppDbContext db)
|
internal sealed class AdminWorkflowService(AppDbContext db)
|
||||||
{
|
{
|
||||||
public async Task<IResult> SetResultsOpenAsync(ResultsOpenRequest request)
|
public async Task<IResult> SetResultsOpenAsync(bool resultsOpen)
|
||||||
{
|
{
|
||||||
var state = await db.AppState.FirstAsync();
|
var state = await db.AppState.FirstAsync();
|
||||||
state.ResultsOpen = request.ResultsOpen;
|
state.ResultsOpen = resultsOpen;
|
||||||
state.UpdatedAt = DateTimeOffset.UtcNow;
|
state.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
await using var tx = await db.Database.BeginTransactionAsync();
|
await using var tx = await db.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
if (request.ResultsOpen)
|
if (resultsOpen)
|
||||||
{
|
{
|
||||||
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Results));
|
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Results));
|
||||||
}
|
}
|
||||||
@@ -44,9 +44,9 @@ internal sealed class AdminWorkflowService(AppDbContext db)
|
|||||||
return Results.Ok(new VoteStatusResponse(voters, ready, waiting));
|
return Results.Ok(new VoteStatusResponse(voters, ready, waiting));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> GrantJokerAsync(GrantJokerRequest request)
|
public async Task<IResult> GrantJokerAsync(Guid playerId)
|
||||||
{
|
{
|
||||||
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == request.PlayerId);
|
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == playerId);
|
||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.NotFoundError("Player not found.");
|
return EndpointHelpers.NotFoundError("Player not found.");
|
||||||
|
|
||||||
@@ -88,18 +88,18 @@ internal sealed class AdminWorkflowService(AppDbContext db)
|
|||||||
return Results.Ok(new AdminDeletePlayerResponse(playerId));
|
return Results.Ok(new AdminDeletePlayerResponse(playerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> LinkSuggestionsAsync(Player adminPlayer, LinkSuggestionsRequest request)
|
public async Task<IResult> LinkSuggestionsAsync(Guid adminPlayerId, int sourceSuggestionId, int targetSuggestionId)
|
||||||
{
|
{
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, adminPlayer.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, adminPlayerId);
|
||||||
if (phase != Phase.Vote)
|
if (phase != Phase.Vote)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
||||||
|
|
||||||
if (request.SourceSuggestionId == request.TargetSuggestionId)
|
if (sourceSuggestionId == targetSuggestionId)
|
||||||
return EndpointHelpers.BadRequestError("Pick two different games to link.");
|
return EndpointHelpers.BadRequestError("Pick two different games to link.");
|
||||||
|
|
||||||
var suggestions = await db.Suggestions.ToListAsync();
|
var suggestions = await db.Suggestions.ToListAsync();
|
||||||
var source = suggestions.FirstOrDefault(s => s.Id == request.SourceSuggestionId);
|
var source = suggestions.FirstOrDefault(s => s.Id == sourceSuggestionId);
|
||||||
var target = suggestions.FirstOrDefault(s => s.Id == request.TargetSuggestionId);
|
var target = suggestions.FirstOrDefault(s => s.Id == targetSuggestionId);
|
||||||
if (source is null || target is null)
|
if (source is null || target is null)
|
||||||
return EndpointHelpers.NotFoundError("Suggestion not found.");
|
return EndpointHelpers.NotFoundError("Suggestion not found.");
|
||||||
|
|
||||||
@@ -143,14 +143,14 @@ internal sealed class AdminWorkflowService(AppDbContext db)
|
|||||||
return Results.Ok(new AdminLinkSuggestionsResponse(targetRoot, affectedIds, await db.Players.CountAsync()));
|
return Results.Ok(new AdminLinkSuggestionsResponse(targetRoot, affectedIds, await db.Players.CountAsync()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> UnlinkSuggestionsAsync(Player adminPlayer, UnlinkSuggestionsRequest request)
|
public async Task<IResult> UnlinkSuggestionsAsync(Guid adminPlayerId, int suggestionId)
|
||||||
{
|
{
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, adminPlayer.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, adminPlayerId);
|
||||||
if (phase != Phase.Vote)
|
if (phase != Phase.Vote)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
||||||
|
|
||||||
var suggestions = await db.Suggestions.ToListAsync();
|
var suggestions = await db.Suggestions.ToListAsync();
|
||||||
var target = suggestions.FirstOrDefault(s => s.Id == request.SuggestionId);
|
var target = suggestions.FirstOrDefault(s => s.Id == suggestionId);
|
||||||
if (target is null)
|
if (target is null)
|
||||||
return Results.Ok(new AdminUnlinkSuggestionsResponse(Array.Empty<int>(), 0));
|
return Results.Ok(new AdminUnlinkSuggestionsResponse(Array.Empty<int>(), 0));
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public static class ResultsEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
return await service.GetResultsAsync(player);
|
return await service.GetResultsAsync(player.Id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ namespace GameList.Endpoints;
|
|||||||
|
|
||||||
internal sealed class ResultsWorkflowService(AppDbContext db)
|
internal sealed class ResultsWorkflowService(AppDbContext db)
|
||||||
{
|
{
|
||||||
public async Task<IResult> GetResultsAsync(Player player)
|
public async Task<IResult> GetResultsAsync(Guid playerId)
|
||||||
{
|
{
|
||||||
var appState = await db.AppState.AsNoTracking().FirstAsync();
|
var appState = await db.AppState.AsNoTracking().FirstAsync();
|
||||||
if (!appState.ResultsOpen)
|
if (!appState.ResultsOpen)
|
||||||
return EndpointHelpers.BadRequestError("Results are locked until the admin enables them.");
|
return EndpointHelpers.BadRequestError("Results are locked until the admin enables them.");
|
||||||
|
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
||||||
if (phase != Phase.Results)
|
if (phase != Phase.Results)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Results, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Results, phase);
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ internal sealed class ResultsWorkflowService(AppDbContext db)
|
|||||||
Average = s.Votes.Count == 0 ? 0 : s.Votes.Average(v => v.Score),
|
Average = s.Votes.Count == 0 ? 0 : s.Votes.Average(v => v.Score),
|
||||||
Votes = s.Votes.Select(v => v.Score).ToList(),
|
Votes = s.Votes.Select(v => v.Score).ToList(),
|
||||||
MyVote = s.Votes
|
MyVote = s.Votes
|
||||||
.Where(v => v.PlayerId == player.Id)
|
.Where(v => v.PlayerId == playerId)
|
||||||
.Select(v => (int?)v.Score)
|
.Select(v => (int?)v.Score)
|
||||||
.FirstOrDefault(),
|
.FirstOrDefault(),
|
||||||
s.ScreenshotUrl,
|
s.ScreenshotUrl,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public static class SuggestEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
return await service.GetMineAsync(player);
|
return await service.GetMineAsync(player.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPost("/", async ([FromBody] SuggestionRequest request, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
|
group.MapPost("/", async ([FromBody] SuggestionRequest request, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
|
||||||
@@ -26,7 +26,19 @@ public static class SuggestEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
return await service.CreateAsync(player, request);
|
return await service.CreateAsync(
|
||||||
|
player.Id,
|
||||||
|
new SuggestionInput(
|
||||||
|
request.Name,
|
||||||
|
request.Genre,
|
||||||
|
request.Description,
|
||||||
|
request.ScreenshotUrl,
|
||||||
|
request.YoutubeUrl,
|
||||||
|
request.GameUrl,
|
||||||
|
request.MinPlayers,
|
||||||
|
request.MaxPlayers
|
||||||
|
)
|
||||||
|
);
|
||||||
}).AddEndpointFilter(new PhaseOrJokerFilter());
|
}).AddEndpointFilter(new PhaseOrJokerFilter());
|
||||||
|
|
||||||
group.MapDelete("/{id:int}", async (int id, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
|
group.MapDelete("/{id:int}", async (int id, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
|
||||||
@@ -35,8 +47,7 @@ public static class SuggestEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
var isAdmin = await EndpointHelpers.IsAdmin(ctx, db);
|
return await service.DeleteAsync(player.Id, id);
|
||||||
return await service.DeleteAsync(player, isAdmin, id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPut("/{id:int}", async (int id, [FromBody] SuggestionRequest request, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
|
group.MapPut("/{id:int}", async (int id, [FromBody] SuggestionRequest request, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
|
||||||
@@ -45,8 +56,20 @@ public static class SuggestEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
var isAdmin = player.IsAdmin;
|
return await service.UpdateAsync(
|
||||||
return await service.UpdateAsync(player, isAdmin, id, request);
|
player.Id,
|
||||||
|
id,
|
||||||
|
new SuggestionInput(
|
||||||
|
request.Name,
|
||||||
|
request.Genre,
|
||||||
|
request.Description,
|
||||||
|
request.ScreenshotUrl,
|
||||||
|
request.YoutubeUrl,
|
||||||
|
request.GameUrl,
|
||||||
|
request.MinPlayers,
|
||||||
|
request.MaxPlayers
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapGet("/all", async (HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
|
group.MapGet("/all", async (HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
|
||||||
@@ -55,7 +78,7 @@ public static class SuggestEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
return await service.GetAllAsync(player);
|
return await service.GetAllAsync(player.Id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
Endpoints/SuggestionInput.cs
Normal file
12
Endpoints/SuggestionInput.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace GameList.Endpoints;
|
||||||
|
|
||||||
|
internal readonly record struct SuggestionInput(
|
||||||
|
string Name,
|
||||||
|
string? Genre,
|
||||||
|
string? Description,
|
||||||
|
string? ScreenshotUrl,
|
||||||
|
string? YoutubeUrl,
|
||||||
|
string? GameUrl,
|
||||||
|
int? MinPlayers,
|
||||||
|
int? MaxPlayers
|
||||||
|
);
|
||||||
@@ -1,27 +1,25 @@
|
|||||||
using GameList.Contracts;
|
|
||||||
|
|
||||||
namespace GameList.Endpoints;
|
namespace GameList.Endpoints;
|
||||||
|
|
||||||
internal static class SuggestionValidator
|
internal static class SuggestionValidator
|
||||||
{
|
{
|
||||||
public static async Task<string?> ValidateAsync(SuggestionRequest request, IHttpClientFactory httpFactory)
|
public static async Task<string?> ValidateAsync(SuggestionInput input, IHttpClientFactory httpFactory)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 100)
|
if (string.IsNullOrWhiteSpace(input.Name) || input.Name.Length > 100)
|
||||||
return "Name is required and must be <= 100 characters.";
|
return "Name is required and must be <= 100 characters.";
|
||||||
|
|
||||||
if (!EndpointHelpers.IsValidImageUrl(request.ScreenshotUrl))
|
if (!EndpointHelpers.IsValidImageUrl(input.ScreenshotUrl))
|
||||||
return "Screenshot URL must be http(s) and end with an image file extension.";
|
return "Screenshot URL must be http(s) and end with an image file extension.";
|
||||||
|
|
||||||
if (!await EndpointHelpers.IsReachableImageAsync(request.ScreenshotUrl, httpFactory))
|
if (!await EndpointHelpers.IsReachableImageAsync(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).";
|
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(request.GameUrl))
|
if (!EndpointHelpers.IsValidHttpUrl(input.GameUrl))
|
||||||
return "Game URL must be http or https.";
|
return "Game URL must be http or https.";
|
||||||
|
|
||||||
if (!EndpointHelpers.IsValidHttpUrl(request.YoutubeUrl))
|
if (!EndpointHelpers.IsValidHttpUrl(input.YoutubeUrl))
|
||||||
return "YouTube URL must be http or https.";
|
return "YouTube URL must be http or https.";
|
||||||
|
|
||||||
return ValidatePlayers(request.MinPlayers, request.MaxPlayers);
|
return ValidatePlayers(input.MinPlayers, input.MaxPlayers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ValidatePlayers(int? minPlayers, int? maxPlayers)
|
private static string? ValidatePlayers(int? minPlayers, int? maxPlayers)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ namespace GameList.Endpoints;
|
|||||||
|
|
||||||
internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFactory httpFactory)
|
internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFactory httpFactory)
|
||||||
{
|
{
|
||||||
public async Task<IResult> GetMineAsync(Player player)
|
public async Task<IResult> GetMineAsync(Guid playerId)
|
||||||
{
|
{
|
||||||
var mine = await db.Suggestions
|
var mine = await db.Suggestions
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(s => s.PlayerId == player.Id)
|
.Where(s => s.PlayerId == playerId)
|
||||||
.Select(s => new
|
.Select(s => new
|
||||||
{
|
{
|
||||||
s.Id,
|
s.Id,
|
||||||
@@ -36,35 +36,45 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
|||||||
return Results.Ok(ordered);
|
return Results.Ok(ordered);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> CreateAsync(Player player, SuggestionRequest request)
|
public async Task<IResult> CreateAsync(Guid playerId, SuggestionInput input)
|
||||||
{
|
{
|
||||||
var validationError = await SuggestionValidator.ValidateAsync(request, httpFactory);
|
var validationError = await SuggestionValidator.ValidateAsync(input, httpFactory);
|
||||||
if (validationError is not null)
|
if (validationError is not null)
|
||||||
return EndpointHelpers.BadRequestError(validationError);
|
return EndpointHelpers.BadRequestError(validationError);
|
||||||
|
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
|
var playerState = await db.Players
|
||||||
var usingJoker = phase == Phase.Vote && player.HasJoker;
|
.AsNoTracking()
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.Select(p => new
|
||||||
|
{
|
||||||
|
p.DisplayName,
|
||||||
|
p.HasJoker
|
||||||
|
})
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
||||||
|
var usingJoker = phase == Phase.Vote && playerState.HasJoker;
|
||||||
if (phase != Phase.Suggest && !usingJoker)
|
if (phase != Phase.Suggest && !usingJoker)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(player.DisplayName))
|
if (string.IsNullOrWhiteSpace(playerState.DisplayName))
|
||||||
return EndpointHelpers.BadRequestError("Set a display name before submitting suggestions.");
|
return EndpointHelpers.BadRequestError("Set a display name before submitting suggestions.");
|
||||||
|
|
||||||
var existingCount = await db.Suggestions.CountAsync(s => s.PlayerId == player.Id);
|
var existingCount = await db.Suggestions.CountAsync(s => s.PlayerId == playerId);
|
||||||
if (!usingJoker && existingCount >= 5)
|
if (!usingJoker && existingCount >= 5)
|
||||||
return EndpointHelpers.BadRequestError("You have reached the 5 suggestion limit.");
|
return EndpointHelpers.BadRequestError("You have reached the 5 suggestion limit.");
|
||||||
|
|
||||||
var suggestion = new Suggestion
|
var suggestion = new Suggestion
|
||||||
{
|
{
|
||||||
PlayerId = player.Id,
|
PlayerId = playerId,
|
||||||
Name = request.Name.Trim(),
|
Name = input.Name.Trim(),
|
||||||
Genre = EndpointHelpers.TrimTo(request.Genre, 50),
|
Genre = EndpointHelpers.TrimTo(input.Genre, 50),
|
||||||
Description = EndpointHelpers.TrimTo(request.Description, 500),
|
Description = EndpointHelpers.TrimTo(input.Description, 500),
|
||||||
ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048),
|
ScreenshotUrl = EndpointHelpers.TrimTo(input.ScreenshotUrl, 2048),
|
||||||
YoutubeUrl = EndpointHelpers.TrimTo(request.YoutubeUrl, 2048),
|
YoutubeUrl = EndpointHelpers.TrimTo(input.YoutubeUrl, 2048),
|
||||||
GameUrl = EndpointHelpers.TrimTo(request.GameUrl, 2048),
|
GameUrl = EndpointHelpers.TrimTo(input.GameUrl, 2048),
|
||||||
MinPlayers = request.MinPlayers,
|
MinPlayers = input.MinPlayers,
|
||||||
MaxPlayers = request.MaxPlayers
|
MaxPlayers = input.MaxPlayers
|
||||||
};
|
};
|
||||||
|
|
||||||
await using var tx = await db.Database.BeginTransactionAsync();
|
await using var tx = await db.Database.BeginTransactionAsync();
|
||||||
@@ -73,7 +83,9 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
|||||||
|
|
||||||
if (usingJoker)
|
if (usingJoker)
|
||||||
{
|
{
|
||||||
player.HasJoker = false;
|
await db.Players
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.ExecuteUpdateAsync(p => p.SetProperty(x => x.HasJoker, false));
|
||||||
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.VotesFinal, false));
|
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.VotesFinal, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,18 +95,28 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
|||||||
return Results.Created($"/api/suggestions/{suggestion.Id}", new SuggestionCreatedResponse(suggestion.Id));
|
return Results.Created($"/api/suggestions/{suggestion.Id}", new SuggestionCreatedResponse(suggestion.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> DeleteAsync(Player player, bool isAdmin, int suggestionId)
|
public async Task<IResult> DeleteAsync(Guid playerId, int suggestionId)
|
||||||
{
|
{
|
||||||
|
var actor = await db.Players
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.Select(p => new
|
||||||
|
{
|
||||||
|
p.IsAdmin
|
||||||
|
})
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
var isAdmin = actor.IsAdmin;
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
{
|
{
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
||||||
if (phase != Phase.Suggest)
|
if (phase != Phase.Suggest)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
||||||
}
|
}
|
||||||
|
|
||||||
var suggestion = isAdmin
|
var suggestion = isAdmin
|
||||||
? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId)
|
? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId)
|
||||||
: await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId && s.PlayerId == player.Id);
|
: await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId && s.PlayerId == playerId);
|
||||||
if (suggestion == null)
|
if (suggestion == null)
|
||||||
return EndpointHelpers.NotFoundError("Suggestion not found.");
|
return EndpointHelpers.NotFoundError("Suggestion not found.");
|
||||||
|
|
||||||
@@ -112,40 +134,50 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
|||||||
return Results.NoContent();
|
return Results.NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> UpdateAsync(Player player, bool isAdmin, int suggestionId, SuggestionRequest request)
|
public async Task<IResult> UpdateAsync(Guid playerId, int suggestionId, SuggestionInput input)
|
||||||
{
|
{
|
||||||
var validationError = await SuggestionValidator.ValidateAsync(request, httpFactory);
|
var validationError = await SuggestionValidator.ValidateAsync(input, httpFactory);
|
||||||
if (validationError is not null)
|
if (validationError is not null)
|
||||||
return EndpointHelpers.BadRequestError(validationError);
|
return EndpointHelpers.BadRequestError(validationError);
|
||||||
|
|
||||||
|
var actor = await db.Players
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.Select(p => new
|
||||||
|
{
|
||||||
|
p.IsAdmin
|
||||||
|
})
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId);
|
var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId);
|
||||||
if (suggestion == null)
|
if (suggestion == null)
|
||||||
return EndpointHelpers.NotFoundError("Suggestion not found.");
|
return EndpointHelpers.NotFoundError("Suggestion not found.");
|
||||||
|
|
||||||
|
var isAdmin = actor.IsAdmin;
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
{
|
{
|
||||||
if (suggestion.PlayerId != player.Id)
|
if (suggestion.PlayerId != playerId)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
||||||
if (phase == Phase.Results)
|
if (phase == Phase.Results)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
||||||
|
|
||||||
if (phase == Phase.Suggest)
|
if (phase == Phase.Suggest)
|
||||||
{
|
{
|
||||||
suggestion.Name = request.Name.Trim();
|
suggestion.Name = input.Name.Trim();
|
||||||
}
|
}
|
||||||
else if (phase != Phase.Vote)
|
else if (phase != Phase.Vote)
|
||||||
{
|
{
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyEditableFields(suggestion, request);
|
ApplyEditableFields(suggestion, input);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
suggestion.Name = request.Name.Trim();
|
suggestion.Name = input.Name.Trim();
|
||||||
ApplyEditableFields(suggestion, request);
|
ApplyEditableFields(suggestion, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@@ -163,9 +195,9 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> GetAllAsync(Player player)
|
public async Task<IResult> GetAllAsync(Guid playerId)
|
||||||
{
|
{
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
||||||
if (phase < Phase.Vote)
|
if (phase < Phase.Vote)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
||||||
|
|
||||||
@@ -186,7 +218,7 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
|||||||
Author = s.Player!.DisplayName,
|
Author = s.Player!.DisplayName,
|
||||||
s.CreatedAt,
|
s.CreatedAt,
|
||||||
s.ParentSuggestionId,
|
s.ParentSuggestionId,
|
||||||
IsOwner = s.PlayerId == player.Id
|
IsOwner = s.PlayerId == playerId
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -219,14 +251,14 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
|
|||||||
return Results.Ok(ordered);
|
return Results.Ok(ordered);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyEditableFields(Suggestion suggestion, SuggestionRequest request)
|
private static void ApplyEditableFields(Suggestion suggestion, SuggestionInput input)
|
||||||
{
|
{
|
||||||
suggestion.Genre = EndpointHelpers.TrimTo(request.Genre, 50);
|
suggestion.Genre = EndpointHelpers.TrimTo(input.Genre, 50);
|
||||||
suggestion.Description = EndpointHelpers.TrimTo(request.Description, 500);
|
suggestion.Description = EndpointHelpers.TrimTo(input.Description, 500);
|
||||||
suggestion.ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048);
|
suggestion.ScreenshotUrl = EndpointHelpers.TrimTo(input.ScreenshotUrl, 2048);
|
||||||
suggestion.YoutubeUrl = EndpointHelpers.TrimTo(request.YoutubeUrl, 2048);
|
suggestion.YoutubeUrl = EndpointHelpers.TrimTo(input.YoutubeUrl, 2048);
|
||||||
suggestion.GameUrl = EndpointHelpers.TrimTo(request.GameUrl, 2048);
|
suggestion.GameUrl = EndpointHelpers.TrimTo(input.GameUrl, 2048);
|
||||||
suggestion.MinPlayers = request.MinPlayers;
|
suggestion.MinPlayers = input.MinPlayers;
|
||||||
suggestion.MaxPlayers = request.MaxPlayers;
|
suggestion.MaxPlayers = input.MaxPlayers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public static class VoteEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
return await service.GetMineAsync(player);
|
return await service.GetMineAsync(player.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPost("/", async (VoteRequest request, HttpContext ctx, AppDbContext db, VoteWorkflowService service) =>
|
group.MapPost("/", async (VoteRequest request, HttpContext ctx, AppDbContext db, VoteWorkflowService service) =>
|
||||||
@@ -25,7 +25,7 @@ public static class VoteEndpoints
|
|||||||
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
return await service.UpsertAsync(player, request);
|
return await service.UpsertAsync(player.Id, request.SuggestionId, request.Score);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPost("/finalize", async (VoteFinalizeRequest request, HttpContext ctx, AppDbContext db, VoteWorkflowService service) =>
|
group.MapPost("/finalize", async (VoteFinalizeRequest request, HttpContext ctx, AppDbContext db, VoteWorkflowService service) =>
|
||||||
@@ -34,7 +34,7 @@ public static class VoteEndpoints
|
|||||||
if (player is null)
|
if (player is null)
|
||||||
return EndpointHelpers.UnauthorizedError();
|
return EndpointHelpers.UnauthorizedError();
|
||||||
|
|
||||||
return await service.SetFinalizeAsync(player, request);
|
return await service.SetFinalizeAsync(player.Id, request.Final);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ namespace GameList.Endpoints;
|
|||||||
|
|
||||||
internal sealed class VoteWorkflowService(AppDbContext db)
|
internal sealed class VoteWorkflowService(AppDbContext db)
|
||||||
{
|
{
|
||||||
public async Task<IResult> GetMineAsync(Player player)
|
public async Task<IResult> GetMineAsync(Guid playerId)
|
||||||
{
|
{
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
||||||
if (phase != Phase.Vote)
|
if (phase != Phase.Vote)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
||||||
|
|
||||||
var votes = await db.Votes
|
var votes = await db.Votes
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(v => v.PlayerId == player.Id)
|
.Where(v => v.PlayerId == playerId)
|
||||||
.Select(v => new
|
.Select(v => new
|
||||||
{
|
{
|
||||||
v.SuggestionId,
|
v.SuggestionId,
|
||||||
@@ -26,19 +26,29 @@ internal sealed class VoteWorkflowService(AppDbContext db)
|
|||||||
return Results.Ok(votes);
|
return Results.Ok(votes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> UpsertAsync(Player player, VoteRequest request)
|
public async Task<IResult> UpsertAsync(Guid playerId, int suggestionId, int score)
|
||||||
{
|
{
|
||||||
if (request.Score is < 0 or > 10)
|
if (score is < 0 or > 10)
|
||||||
return EndpointHelpers.BadRequestError("Score must be between 0 and 10.");
|
return EndpointHelpers.BadRequestError("Score must be between 0 and 10.");
|
||||||
|
|
||||||
if (player.VotesFinal)
|
var playerState = await db.Players
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.Select(p => new
|
||||||
|
{
|
||||||
|
p.VotesFinal,
|
||||||
|
p.DisplayName
|
||||||
|
})
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
if (playerState.VotesFinal)
|
||||||
return EndpointHelpers.BadRequestError("Votes are finalized. Unfinalize before changing scores.");
|
return EndpointHelpers.BadRequestError("Votes are finalized. Unfinalize before changing scores.");
|
||||||
|
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
||||||
if (phase != Phase.Vote)
|
if (phase != Phase.Vote)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(player.DisplayName))
|
if (string.IsNullOrWhiteSpace(playerState.DisplayName))
|
||||||
return EndpointHelpers.BadRequestError("Set a display name before voting.");
|
return EndpointHelpers.BadRequestError("Set a display name before voting.");
|
||||||
|
|
||||||
var linkMap = await db.Suggestions
|
var linkMap = await db.Suggestions
|
||||||
@@ -50,46 +60,48 @@ internal sealed class VoteWorkflowService(AppDbContext db)
|
|||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var rootIndex = EndpointHelpers.BuildLinkRoots(linkMap.Select(s => (s.Id, s.ParentSuggestionId)));
|
var rootIndex = EndpointHelpers.BuildLinkRoots(linkMap.Select(s => (s.Id, s.ParentSuggestionId)));
|
||||||
if (!rootIndex.ContainsKey(request.SuggestionId))
|
if (!rootIndex.ContainsKey(suggestionId))
|
||||||
return EndpointHelpers.BadRequestError("Suggestion not found.");
|
return EndpointHelpers.BadRequestError("Suggestion not found.");
|
||||||
|
|
||||||
var linkedIds = EndpointHelpers.LinkedIdsFor(request.SuggestionId, rootIndex);
|
var linkedIds = EndpointHelpers.LinkedIdsFor(suggestionId, rootIndex);
|
||||||
if (linkedIds.Count == 0)
|
if (linkedIds.Count == 0)
|
||||||
linkedIds.Add(request.SuggestionId);
|
linkedIds.Add(suggestionId);
|
||||||
|
|
||||||
var existingVotes = await db.Votes
|
var existingVotes = await db.Votes
|
||||||
.Where(v => v.PlayerId == player.Id && linkedIds.Contains(v.SuggestionId))
|
.Where(v => v.PlayerId == playerId && linkedIds.Contains(v.SuggestionId))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
foreach (var suggestionId in linkedIds)
|
foreach (var linkedSuggestionId in linkedIds)
|
||||||
{
|
{
|
||||||
var vote = existingVotes.FirstOrDefault(v => v.SuggestionId == suggestionId);
|
var vote = existingVotes.FirstOrDefault(v => v.SuggestionId == linkedSuggestionId);
|
||||||
if (vote == null)
|
if (vote == null)
|
||||||
{
|
{
|
||||||
db.Votes.Add(new Vote
|
db.Votes.Add(new Vote
|
||||||
{
|
{
|
||||||
PlayerId = player.Id,
|
PlayerId = playerId,
|
||||||
SuggestionId = suggestionId,
|
SuggestionId = linkedSuggestionId,
|
||||||
Score = request.Score
|
Score = score
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
vote.Score = request.Score;
|
vote.Score = score;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return Results.Ok(new VoteUpsertResponse(linkedIds, request.Score));
|
return Results.Ok(new VoteUpsertResponse(linkedIds, score));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> SetFinalizeAsync(Player player, VoteFinalizeRequest request)
|
public async Task<IResult> SetFinalizeAsync(Guid playerId, bool final)
|
||||||
{
|
{
|
||||||
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id);
|
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
||||||
if (phase != Phase.Vote)
|
if (phase != Phase.Vote)
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
||||||
|
|
||||||
player.VotesFinal = request.Final;
|
var player = await db.Players.FirstAsync(p => p.Id == playerId);
|
||||||
|
|
||||||
|
player.VotesFinal = final;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return Results.Ok(new VoteFinalizeResponse(player.VotesFinal));
|
return Results.Ok(new VoteFinalizeResponse(player.VotesFinal));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user