Add owner role and admin management controls
This commit is contained in:
@@ -19,6 +19,7 @@ public static class AdminEndpoints
|
||||
admin.MapPost("/joker", async ([FromBody] GrantJokerRequest request, AdminWorkflowService service) => await service.GrantJokerAsync(request.PlayerId));
|
||||
|
||||
admin.MapPost("/player-phase", async ([FromBody] SetPlayerPhaseRequest request, AdminWorkflowService service) => await service.SetPlayerPhaseAsync(request.PlayerId, request.Phase));
|
||||
admin.MapPost("/player-admin", async ([FromBody] SetPlayerAdminRequest request, AdminWorkflowService service) => await service.SetPlayerAdminAsync(request.PlayerId, request.IsAdmin));
|
||||
|
||||
admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, [FromBody] AdminPasswordRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
|
||||
{
|
||||
|
||||
@@ -42,7 +42,17 @@ internal sealed class AdminWorkflowService(AppDbContext db)
|
||||
.AsNoTracking()
|
||||
.Include(p => p.Suggestions)
|
||||
.OrderBy(p => p.DisplayName ?? p.Username)
|
||||
.Select(p => new VoteStatusDto(p.Id, p.DisplayName ?? p.Username, p.Username, p.CurrentPhase, p.VotesFinal, p.HasJoker, p.Suggestions.Count, p.Suggestions.Select(s => s.Name).ToList()))
|
||||
.Select(p => new VoteStatusDto(
|
||||
p.Id,
|
||||
p.DisplayName ?? p.Username,
|
||||
p.Username,
|
||||
p.CurrentPhase,
|
||||
p.VotesFinal,
|
||||
p.HasJoker,
|
||||
p.IsAdmin,
|
||||
p.IsOwner,
|
||||
p.Suggestions.Count,
|
||||
p.Suggestions.Select(s => s.Name).ToList()))
|
||||
.ToListAsync();
|
||||
|
||||
var waiting = voters.Where(v => !v.Finalized).Select(v => v.Name).ToList();
|
||||
@@ -87,6 +97,21 @@ internal sealed class AdminWorkflowService(AppDbContext db)
|
||||
return Results.Ok(new AdminSetPlayerPhaseResponse(player.Id, player.CurrentPhase, player.VotesFinal));
|
||||
}
|
||||
|
||||
public async Task<IResult> SetPlayerAdminAsync(Guid playerId, bool isAdmin)
|
||||
{
|
||||
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == playerId);
|
||||
if (player is null)
|
||||
return EndpointHelpers.NotFoundError("Player not found.");
|
||||
|
||||
if (player.IsOwner)
|
||||
return EndpointHelpers.BadRequestError("Owner permissions cannot be changed.");
|
||||
|
||||
player.IsAdmin = isAdmin;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new AdminSetPlayerAdminResponse(player.Id, player.IsAdmin));
|
||||
}
|
||||
|
||||
public async Task<IResult> DeletePlayerAsync(Guid playerId, Guid adminPlayerId, string password, HttpContext ctx)
|
||||
{
|
||||
var passwordError = await ValidateAdminPasswordAsync(adminPlayerId, password, ctx);
|
||||
@@ -96,6 +121,8 @@ internal sealed class AdminWorkflowService(AppDbContext db)
|
||||
var player = await db.Players.Include(p => p.Suggestions).FirstOrDefaultAsync(p => p.Id == playerId);
|
||||
if (player is null)
|
||||
return EndpointHelpers.NotFoundError("Player not found.");
|
||||
if (player.IsOwner)
|
||||
return EndpointHelpers.BadRequestError("Owner account cannot be deleted.");
|
||||
|
||||
await using var tx = await db.Database.BeginTransactionAsync();
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ public static class AuthEndpoints
|
||||
{
|
||||
var group = app.MapGroup("/api/auth").RequireRateLimiting("auth-sensitive");
|
||||
|
||||
group.MapGet("/options", async (AppDbContext db) =>
|
||||
{
|
||||
var ownerExists = await db.Players.AsNoTracking().AnyAsync(p => p.IsOwner);
|
||||
return Results.Ok(new AuthOptionsResponse(ownerExists));
|
||||
});
|
||||
|
||||
group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db, IConfiguration config, AuthAttemptMonitor authAttemptMonitor) =>
|
||||
{
|
||||
if (!AuthValidator.TryValidateRegistration(request, out var validated, out var registrationError))
|
||||
@@ -37,15 +43,16 @@ public static class AuthEndpoints
|
||||
return EndpointHelpers.BadRequestError("Invalid admin key.");
|
||||
}
|
||||
|
||||
var adminExists = await db.Players.AnyAsync(p => p.IsAdmin);
|
||||
if (adminExists)
|
||||
var ownerExists = await db.Players.AsNoTracking().AnyAsync(p => p.IsOwner);
|
||||
if (ownerExists)
|
||||
{
|
||||
authAttemptMonitor.RecordFailure(ctx, "auth-register-admin", validated.NormalizedUsername, "bootstrap-admin-disabled");
|
||||
return EndpointHelpers.BadRequestError("Admin registration via admin key is disabled after the first admin account.");
|
||||
return EndpointHelpers.BadRequestError("Admin registration via admin key is disabled once an owner account exists.");
|
||||
}
|
||||
}
|
||||
|
||||
var isAdmin = wantsAdmin;
|
||||
var isOwner = wantsAdmin;
|
||||
|
||||
var player = new Player
|
||||
{
|
||||
@@ -56,6 +63,7 @@ public static class AuthEndpoints
|
||||
PasswordSalt = salt,
|
||||
DisplayName = validated.DisplayName,
|
||||
IsAdmin = isAdmin,
|
||||
IsOwner = isOwner,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
LastLoginAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ internal sealed class StateWorkflowService(AppDbContext db)
|
||||
player.Username,
|
||||
player.DisplayName,
|
||||
player.IsAdmin,
|
||||
player.IsOwner,
|
||||
phase,
|
||||
player.VotesFinal,
|
||||
player.HasJoker
|
||||
|
||||
Reference in New Issue
Block a user