Files
GameList/Endpoints/AdminEndpoints.cs

220 lines
9.1 KiB
C#

using GameList.Data;
using GameList.Domain;
using GameList.Contracts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
namespace GameList.Endpoints;
public static class AdminEndpoints
{
public static void MapAdminEndpoints(this IEndpointRouteBuilder app)
{
var admin = app.MapGroup("/api/admin");
admin.MapPost("/results", async ([FromBody] Contracts.ResultsOpenRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
{
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
var state = await db.AppState.FirstAsync();
state.ResultsOpen = request.ResultsOpen;
state.UpdatedAt = DateTimeOffset.UtcNow;
if (request.ResultsOpen)
{
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Results));
}
else
{
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Vote)
.SetProperty(x => x.VotesFinal, false));
}
await db.SaveChangesAsync();
var currentState = await db.AppState.AsNoTracking().FirstAsync();
return Results.Ok(new { currentState.ResultsOpen, currentState.UpdatedAt });
});
admin.MapGet("/vote-status", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
{
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
var voters = await db.Players
.AsNoTracking()
.Where(p => p.CurrentPhase == Phase.Vote || p.Suggestions.Any())
.OrderBy(p => p.DisplayName ?? p.Username)
.Select(p => new VoteStatusDto(p.Id, p.DisplayName ?? p.Username, p.VotesFinal))
.ToListAsync();
var waiting = voters.Where(v => !v.Finalized).Select(v => v.Name).ToList();
var ready = waiting.Count == 0;
return Results.Ok(new { voters, ready, waiting });
});
admin.MapPost("/link-suggestions", async ([FromBody] LinkSuggestionsRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
{
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
if (player is null || !await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
var phase = await EndpointHelpers.GetPhase(db, player.Id);
if (phase != Phase.Vote)
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
if (request.SourceSuggestionId == request.TargetSuggestionId)
return Results.BadRequest(new { error = "Pick two different games to link." });
var suggestions = await db.Suggestions.ToListAsync();
var source = suggestions.FirstOrDefault(s => s.Id == request.SourceSuggestionId);
var target = suggestions.FirstOrDefault(s => s.Id == request.TargetSuggestionId);
if (source is null || target is null)
return Results.NotFound(new { error = "Suggestion not found." });
var rootIndex = EndpointHelpers.BuildLinkRoots(suggestions.Select(s => (s.Id, s.ParentSuggestionId)));
if (!rootIndex.TryGetValue(source.Id, out var sourceRoot) || !rootIndex.TryGetValue(target.Id, out var targetRoot))
return Results.NotFound(new { error = "Suggestion not found." });
if (sourceRoot == targetRoot)
return Results.BadRequest(new { error = "These games are already linked." });
var affectedRootIds = new HashSet<int> { sourceRoot, targetRoot };
var affectedIds = rootIndex
.Where(kv => affectedRootIds.Contains(kv.Value))
.Select(kv => kv.Key)
.ToList();
await using var tx = await db.Database.BeginTransactionAsync();
foreach (var suggestion in suggestions)
{
var root = rootIndex.GetValueOrDefault(suggestion.Id);
if (root == targetRoot)
{
suggestion.ParentSuggestionId = suggestion.Id == targetRoot ? null : targetRoot;
}
else if (root == sourceRoot)
{
suggestion.ParentSuggestionId = targetRoot;
}
}
await db.SaveChangesAsync();
var affectedPlayerIds = await db.Votes
.Where(v => affectedIds.Contains(v.SuggestionId))
.Select(v => v.PlayerId)
.Distinct()
.ToListAsync();
await db.Votes.Where(v => affectedIds.Contains(v.SuggestionId)).ExecuteDeleteAsync();
if (affectedPlayerIds.Count > 0)
{
await db.Players.Where(p => affectedPlayerIds.Contains(p.Id))
.ExecuteUpdateAsync(p => p.SetProperty(x => x.VotesFinal, false));
}
await tx.CommitAsync();
return Results.Ok(new
{
RootId = targetRoot,
LinkedSuggestionIds = affectedIds,
UnfinalizedPlayers = affectedPlayerIds.Count
});
});
admin.MapPost("/unlink-suggestions", async ([FromBody] UnlinkSuggestionsRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
{
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
if (player is null || !await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
var phase = await EndpointHelpers.GetPhase(db, player.Id);
if (phase != Phase.Vote)
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
var suggestions = await db.Suggestions.ToListAsync();
var target = suggestions.FirstOrDefault(s => s.Id == request.SuggestionId);
if (target is null)
return Results.NotFound(new { error = "Suggestion not found." });
var rootIndex = EndpointHelpers.BuildLinkRoots(suggestions.Select(s => (s.Id, s.ParentSuggestionId)));
if (!rootIndex.TryGetValue(target.Id, out var rootId))
return Results.Ok(new { UnlinkedSuggestionIds = Array.Empty<int>(), UnfinalizedPlayers = 0 });
var groupIds = rootIndex
.Where(kv => kv.Value == rootId)
.Select(kv => kv.Key)
.ToList();
await using var tx = await db.Database.BeginTransactionAsync();
foreach (var suggestion in suggestions.Where(s => groupIds.Contains(s.Id)))
{
suggestion.ParentSuggestionId = null;
}
await db.SaveChangesAsync();
var affectedPlayerIds = await db.Votes
.Where(v => groupIds.Contains(v.SuggestionId))
.Select(v => v.PlayerId)
.Distinct()
.ToListAsync();
await db.Votes.Where(v => groupIds.Contains(v.SuggestionId)).ExecuteDeleteAsync();
if (affectedPlayerIds.Count > 0)
{
await db.Players.Where(p => affectedPlayerIds.Contains(p.Id))
.ExecuteUpdateAsync(p => p.SetProperty(x => x.VotesFinal, false));
}
await tx.CommitAsync();
return Results.Ok(new
{
UnlinkedSuggestionIds = groupIds,
UnfinalizedPlayers = affectedPlayerIds.Count
});
});
admin.MapPost("/reset", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
{
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
await db.Votes.ExecuteDeleteAsync();
await db.Suggestions.ExecuteDeleteAsync();
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Suggest)
.SetProperty(x => x.VotesFinal, false));
var state = await db.AppState.FirstAsync();
state.ResultsOpen = false;
state.UpdatedAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync();
return Results.Ok(new { Phase = Phase.Suggest, state.ResultsOpen, state.UpdatedAt });
});
admin.MapPost("/factory-reset", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
{
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
await using var tx = await db.Database.BeginTransactionAsync();
await db.Votes.ExecuteDeleteAsync();
await db.Suggestions.ExecuteDeleteAsync();
await db.Players.ExecuteDeleteAsync();
await db.AppState.ExecuteDeleteAsync();
var fresh = EndpointHelpers.NewAppState();
db.AppState.Add(fresh);
await db.SaveChangesAsync();
await tx.CommitAsync();
return Results.Ok(new { Phase = Phase.Suggest, fresh.ResultsOpen, fresh.UpdatedAt });
});
}
}