Require admin password for destructive admin actions
This commit is contained in:
@@ -19,7 +19,14 @@ public static class AdminEndpoints
|
||||
|
||||
admin.MapPost("/player-phase", async ([FromBody] SetPlayerPhaseRequest request, AdminWorkflowService service) => await service.SetPlayerPhaseAsync(request.PlayerId, request.Phase));
|
||||
|
||||
admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, AdminWorkflowService service) => await service.DeletePlayerAsync(playerId));
|
||||
admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, [FromBody] AdminPasswordRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
|
||||
{
|
||||
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||
if (player is null)
|
||||
return EndpointHelpers.UnauthorizedError();
|
||||
|
||||
return await service.DeletePlayerAsync(playerId, player.Id, request.Password);
|
||||
});
|
||||
|
||||
admin.MapPost("/link-suggestions", async ([FromBody] LinkSuggestionsRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
|
||||
{
|
||||
@@ -39,9 +46,23 @@ public static class AdminEndpoints
|
||||
return await service.UnlinkSuggestionsAsync(player.Id, request.SuggestionId);
|
||||
});
|
||||
|
||||
admin.MapPost("/reset", async (AdminWorkflowService service) => await service.ResetAsync());
|
||||
admin.MapPost("/reset", async ([FromBody] AdminPasswordRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
|
||||
{
|
||||
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||
if (player is null)
|
||||
return EndpointHelpers.UnauthorizedError();
|
||||
|
||||
admin.MapPost("/factory-reset", async (AdminWorkflowService service) => await service.FactoryResetAsync());
|
||||
return await service.ResetAsync(player.Id, request.Password);
|
||||
});
|
||||
|
||||
admin.MapPost("/factory-reset", async ([FromBody] AdminPasswordRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
|
||||
{
|
||||
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||
if (player is null)
|
||||
return EndpointHelpers.UnauthorizedError();
|
||||
|
||||
return await service.FactoryResetAsync(player.Id, request.Password);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user