Centralize admin auth with endpoint filter

This commit is contained in:
2026-02-05 17:11:17 +01:00
parent c03cee1777
commit 8176940d18
3 changed files with 34 additions and 18 deletions

View File

@@ -4,6 +4,7 @@ using GameList.Contracts;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic; using System.Collections.Generic;
using GameList.Infrastructure;
namespace GameList.Endpoints; namespace GameList.Endpoints;
@@ -11,12 +12,12 @@ public static class AdminEndpoints
{ {
public static void MapAdminEndpoints(this IEndpointRouteBuilder app) public static void MapAdminEndpoints(this IEndpointRouteBuilder app)
{ {
var admin = app.MapGroup("/api/admin"); var admin = app.MapGroup("/api/admin")
.RequireAuthorization()
.AddEndpointFilter<AdminOnlyFilter>();
admin.MapPost("/results", async ([FromBody] Contracts.ResultsOpenRequest request, HttpContext ctx, AppDbContext db) => admin.MapPost("/results", async ([FromBody] Contracts.ResultsOpenRequest request, HttpContext ctx, AppDbContext db) =>
{ {
if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized();
var state = await db.AppState.FirstAsync(); var state = await db.AppState.FirstAsync();
state.ResultsOpen = request.ResultsOpen; state.ResultsOpen = request.ResultsOpen;
state.UpdatedAt = DateTimeOffset.UtcNow; state.UpdatedAt = DateTimeOffset.UtcNow;
@@ -38,8 +39,6 @@ public static class AdminEndpoints
admin.MapGet("/vote-status", async (HttpContext ctx, AppDbContext db) => admin.MapGet("/vote-status", async (HttpContext ctx, AppDbContext db) =>
{ {
if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized();
var voters = await db.Players var voters = await db.Players
.AsNoTracking() .AsNoTracking()
.Include(p => p.Suggestions) .Include(p => p.Suggestions)
@@ -61,7 +60,6 @@ public static class AdminEndpoints
admin.MapPost("/joker", async ([FromBody] GrantJokerRequest request, HttpContext ctx, AppDbContext db) => admin.MapPost("/joker", async ([FromBody] GrantJokerRequest request, HttpContext ctx, AppDbContext db) =>
{ {
if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized();
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == request.PlayerId); var player = await db.Players.FirstOrDefaultAsync(p => p.Id == request.PlayerId);
if (player is null) return Results.NotFound(new { error = "Player not found." }); if (player is null) return Results.NotFound(new { error = "Player not found." });
@@ -78,8 +76,6 @@ public static class AdminEndpoints
admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, HttpContext ctx, AppDbContext db) => admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, HttpContext ctx, AppDbContext db) =>
{ {
if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized();
var player = await db.Players var player = await db.Players
.Include(p => p.Suggestions) .Include(p => p.Suggestions)
.FirstOrDefaultAsync(p => p.Id == playerId); .FirstOrDefaultAsync(p => p.Id == playerId);
@@ -114,7 +110,7 @@ public static class AdminEndpoints
admin.MapPost("/link-suggestions", async ([FromBody] LinkSuggestionsRequest request, HttpContext ctx, AppDbContext db) => admin.MapPost("/link-suggestions", async ([FromBody] LinkSuggestionsRequest request, HttpContext ctx, AppDbContext db) =>
{ {
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db); var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
if (player is null || !await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized(); if (player is null) return Results.Unauthorized();
var phase = await EndpointHelpers.GetPhase(db, player.Id); var phase = await EndpointHelpers.GetPhase(db, player.Id);
if (phase != Phase.Vote) if (phase != Phase.Vote)
@@ -186,7 +182,7 @@ public static class AdminEndpoints
admin.MapPost("/unlink-suggestions", async ([FromBody] UnlinkSuggestionsRequest request, HttpContext ctx, AppDbContext db) => admin.MapPost("/unlink-suggestions", async ([FromBody] UnlinkSuggestionsRequest request, HttpContext ctx, AppDbContext db) =>
{ {
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db); var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
if (player is null || !await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized(); if (player is null) return Results.Unauthorized();
var phase = await EndpointHelpers.GetPhase(db, player.Id); var phase = await EndpointHelpers.GetPhase(db, player.Id);
if (phase != Phase.Vote) if (phase != Phase.Vote)
@@ -240,8 +236,6 @@ public static class AdminEndpoints
admin.MapPost("/reset", async (HttpContext ctx, AppDbContext db) => admin.MapPost("/reset", async (HttpContext ctx, AppDbContext db) =>
{ {
if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized();
await db.Votes.ExecuteDeleteAsync(); await db.Votes.ExecuteDeleteAsync();
await db.Suggestions.ExecuteDeleteAsync(); await db.Suggestions.ExecuteDeleteAsync();
@@ -258,8 +252,6 @@ public static class AdminEndpoints
admin.MapPost("/factory-reset", async (HttpContext ctx, AppDbContext db) => admin.MapPost("/factory-reset", async (HttpContext ctx, AppDbContext db) =>
{ {
if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized();
await using var tx = await db.Database.BeginTransactionAsync(); await using var tx = await db.Database.BeginTransactionAsync();
await db.Votes.ExecuteDeleteAsync(); await db.Votes.ExecuteDeleteAsync();

View File

@@ -11,10 +11,10 @@ internal static class EndpointHelpers
{ {
public static async Task<Player?> GetAuthenticatedPlayer(HttpContext ctx, AppDbContext db) public static async Task<Player?> GetAuthenticatedPlayer(HttpContext ctx, AppDbContext db)
{ {
if (ctx?.User?.Identity?.IsAuthenticated != true) if (ctx?.User?.Identity?.IsAuthenticated != true) return null;
{
return null; if (ctx.Items.TryGetValue(nameof(Player), out var cached) && cached is Player cachedPlayer)
} return cachedPlayer;
var idValue = ctx.User.FindFirstValue(ClaimTypes.NameIdentifier); var idValue = ctx.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(idValue) || !Guid.TryParse(idValue, out var playerId)) if (string.IsNullOrWhiteSpace(idValue) || !Guid.TryParse(idValue, out var playerId))
@@ -28,7 +28,10 @@ internal static class EndpointHelpers
if (existing is null) if (existing is null)
{ {
await Infrastructure.PlayerIdentityExtensions.SignOutPlayerAsync(ctx); await Infrastructure.PlayerIdentityExtensions.SignOutPlayerAsync(ctx);
return null;
} }
ctx.Items[nameof(Player)] = existing;
return existing; return existing;
} }

View File

@@ -0,0 +1,21 @@
using GameList.Data;
using GameList.Endpoints;
using Microsoft.AspNetCore.Authorization;
namespace GameList.Infrastructure;
public class AdminOnlyFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var httpContext = context.HttpContext;
var db = httpContext.RequestServices.GetRequiredService<AppDbContext>();
var player = await EndpointHelpers.GetAuthenticatedPlayer(httpContext, db);
if (player?.IsAdmin != true)
{
return Results.Unauthorized();
}
return await next(context);
}
}