Add admin accounts and streamlined header UI
This commit is contained in:
@@ -13,7 +13,7 @@ public static class AdminEndpoints
|
||||
|
||||
admin.MapPost("/phase", async ([FromBody] Contracts.PhaseRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
if (!EndpointHelpers.IsAuthorized(ctx, config)) return Results.Unauthorized();
|
||||
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
|
||||
|
||||
var state = await db.AppState.FirstAsync();
|
||||
state.CurrentPhase = request.Phase;
|
||||
@@ -24,7 +24,7 @@ public static class AdminEndpoints
|
||||
|
||||
admin.MapPost("/reset", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
if (!EndpointHelpers.IsAuthorized(ctx, config)) return Results.Unauthorized();
|
||||
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
|
||||
|
||||
await db.Votes.ExecuteDeleteAsync();
|
||||
await db.Suggestions.ExecuteDeleteAsync();
|
||||
@@ -39,7 +39,7 @@ public static class AdminEndpoints
|
||||
|
||||
admin.MapPost("/factory-reset", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
if (!EndpointHelpers.IsAuthorized(ctx, config)) return Results.Unauthorized();
|
||||
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
|
||||
|
||||
await using var tx = await db.Database.BeginTransactionAsync();
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public static class AuthEndpoints
|
||||
{
|
||||
var group = app.MapGroup("/api/auth");
|
||||
|
||||
group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db) =>
|
||||
group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var username = request.Username?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(username) || username.Length > 64)
|
||||
@@ -24,6 +24,8 @@ public static class AuthEndpoints
|
||||
return Results.BadRequest(new { error = "Password is required." });
|
||||
|
||||
var displayName = EndpointHelpers.TrimTo(request.DisplayName, 64);
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
return Results.BadRequest(new { error = "Display name is required." });
|
||||
var normalized = username.ToLowerInvariant();
|
||||
|
||||
var exists = await db.Players.AnyAsync(p => p.NormalizedUsername == normalized);
|
||||
@@ -31,6 +33,10 @@ public static class AuthEndpoints
|
||||
return Results.Conflict(new { error = "Username already taken." });
|
||||
|
||||
var (hash, salt) = PasswordHasher.HashPassword(request.Password);
|
||||
var adminKey = EndpointHelpers.TrimTo(request.AdminKey, 128);
|
||||
var expectedAdminKey = config["ADMIN_PASSWORD"];
|
||||
var isAdmin = !string.IsNullOrWhiteSpace(expectedAdminKey) && adminKey == expectedAdminKey;
|
||||
|
||||
var player = new Player
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
@@ -39,6 +45,7 @@ public static class AuthEndpoints
|
||||
PasswordHash = hash,
|
||||
PasswordSalt = salt,
|
||||
DisplayName = displayName,
|
||||
IsAdmin = isAdmin,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
LastLoginAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
@@ -48,7 +55,7 @@ public static class AuthEndpoints
|
||||
|
||||
PlayerIdentityExtensions.IssuePlayerCookie(ctx, player.Id);
|
||||
|
||||
return Results.Ok(new { player.Id, player.Username, player.DisplayName });
|
||||
return Results.Ok(new { player.Id, player.Username, player.DisplayName, player.IsAdmin });
|
||||
});
|
||||
|
||||
group.MapPost("/login", async ([FromBody] LoginRequest request, HttpContext ctx, AppDbContext db) =>
|
||||
@@ -62,12 +69,16 @@ public static class AuthEndpoints
|
||||
if (player == null || !PasswordHasher.Verify(request.Password, player.PasswordHash, player.PasswordSalt))
|
||||
return Results.Json(new { error = "Invalid username or password." }, statusCode: StatusCodes.Status401Unauthorized);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(player.DisplayName))
|
||||
{
|
||||
player.DisplayName = player.Username;
|
||||
}
|
||||
player.LastLoginAt = DateTimeOffset.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
PlayerIdentityExtensions.IssuePlayerCookie(ctx, player.Id);
|
||||
|
||||
return Results.Ok(new { player.Id, player.Username, player.DisplayName });
|
||||
return Results.Ok(new { player.Id, player.Username, player.DisplayName, player.IsAdmin });
|
||||
});
|
||||
|
||||
group.MapPost("/logout", (HttpContext ctx) =>
|
||||
|
||||
@@ -34,8 +34,11 @@ internal static class EndpointHelpers
|
||||
? t[..Math.Min(t.Length, max)]
|
||||
: null;
|
||||
|
||||
public static bool IsAuthorized(HttpContext ctx, IConfiguration config)
|
||||
public static async Task<bool> IsAdmin(HttpContext ctx, AppDbContext db, IConfiguration config)
|
||||
{
|
||||
var player = await GetAuthenticatedPlayer(ctx, db);
|
||||
if (player?.IsAdmin == true) return true;
|
||||
|
||||
var provided = ctx.Request.Headers["X-Admin-Key"].FirstOrDefault()
|
||||
?? ctx.Request.Query["key"].FirstOrDefault();
|
||||
var expected = config["ADMIN_PASSWORD"];
|
||||
|
||||
@@ -27,7 +27,7 @@ public static class StateEndpoints
|
||||
{
|
||||
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||
if (player is null) return Results.Unauthorized();
|
||||
return Results.Ok(new { player.Id, player.DisplayName });
|
||||
return Results.Ok(new { player.Id, player.DisplayName, player.Username, player.IsAdmin });
|
||||
});
|
||||
|
||||
app.MapPost("/api/me/name", async ([FromBody] SetNameRequest request, HttpContext ctx, AppDbContext db) =>
|
||||
|
||||
Reference in New Issue
Block a user