Require admin password for destructive admin actions

This commit is contained in:
2026-02-08 15:05:10 +01:00
parent 96a47020d8
commit e666e7c603
13 changed files with 197 additions and 43 deletions

View File

@@ -1,6 +1,7 @@
using GameList.Contracts;
using GameList.Data;
using GameList.Domain;
using GameList.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace GameList.Endpoints;
@@ -86,8 +87,12 @@ internal sealed class AdminWorkflowService(AppDbContext db)
return Results.Ok(new AdminSetPlayerPhaseResponse(player.Id, player.CurrentPhase, player.VotesFinal));
}
public async Task<IResult> DeletePlayerAsync(Guid playerId)
public async Task<IResult> DeletePlayerAsync(Guid playerId, Guid adminPlayerId, string password)
{
var passwordError = await ValidateAdminPasswordAsync(adminPlayerId, password);
if (passwordError is not null)
return passwordError;
var player = await db.Players.Include(p => p.Suggestions).FirstOrDefaultAsync(p => p.Id == playerId);
if (player is null)
return EndpointHelpers.NotFoundError("Player not found.");
@@ -203,8 +208,12 @@ internal sealed class AdminWorkflowService(AppDbContext db)
return Results.Ok(new AdminUnlinkSuggestionsResponse(groupIds, await db.Players.CountAsync()));
}
public async Task<IResult> ResetAsync()
public async Task<IResult> ResetAsync(Guid adminPlayerId, string password)
{
var passwordError = await ValidateAdminPasswordAsync(adminPlayerId, password);
if (passwordError is not null)
return passwordError;
await using var tx = await db.Database.BeginTransactionAsync();
await db.Votes.ExecuteDeleteAsync();
@@ -220,8 +229,12 @@ internal sealed class AdminWorkflowService(AppDbContext db)
return Results.Ok(new AdminResetStateResponse(Phase.Suggest, state.ResultsOpen, state.UpdatedAt));
}
public async Task<IResult> FactoryResetAsync()
public async Task<IResult> FactoryResetAsync(Guid adminPlayerId, string password)
{
var passwordError = await ValidateAdminPasswordAsync(adminPlayerId, password);
if (passwordError is not null)
return passwordError;
await using var tx = await db.Database.BeginTransactionAsync();
await db.Votes.ExecuteDeleteAsync();
@@ -237,4 +250,18 @@ internal sealed class AdminWorkflowService(AppDbContext db)
return Results.Ok(new AdminResetStateResponse(Phase.Suggest, fresh.ResultsOpen, fresh.UpdatedAt));
}
private async Task<IResult?> ValidateAdminPasswordAsync(Guid adminPlayerId, string password)
{
if (string.IsNullOrWhiteSpace(password))
return EndpointHelpers.BadRequestError("Admin password is required.");
var admin = await db.Players.AsNoTracking().FirstOrDefaultAsync(p => p.Id == adminPlayerId && p.IsAdmin);
if (admin is null)
return EndpointHelpers.UnauthorizedError();
return PasswordHasher.Verify(password, admin.PasswordHash, admin.PasswordSalt)
? null
: EndpointHelpers.BadRequestError("Invalid admin password.");
}
}