From 8176940d18eed2b5677e249279b3b6aef5173dac Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Thu, 5 Feb 2026 17:11:17 +0100 Subject: [PATCH] Centralize admin auth with endpoint filter --- Endpoints/AdminEndpoints.cs | 20 ++++++-------------- Endpoints/EndpointHelpers.cs | 11 +++++++---- Infrastructure/AdminOnlyFilter.cs | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 Infrastructure/AdminOnlyFilter.cs diff --git a/Endpoints/AdminEndpoints.cs b/Endpoints/AdminEndpoints.cs index 2c24a83..086e3a0 100644 --- a/Endpoints/AdminEndpoints.cs +++ b/Endpoints/AdminEndpoints.cs @@ -4,6 +4,7 @@ using GameList.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; +using GameList.Infrastructure; namespace GameList.Endpoints; @@ -11,12 +12,12 @@ public static class AdminEndpoints { public static void MapAdminEndpoints(this IEndpointRouteBuilder app) { - var admin = app.MapGroup("/api/admin"); + var admin = app.MapGroup("/api/admin") + .RequireAuthorization() + .AddEndpointFilter(); 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(); state.ResultsOpen = request.ResultsOpen; state.UpdatedAt = DateTimeOffset.UtcNow; @@ -38,8 +39,6 @@ public static class AdminEndpoints admin.MapGet("/vote-status", async (HttpContext ctx, AppDbContext db) => { - if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized(); - var voters = await db.Players .AsNoTracking() .Include(p => p.Suggestions) @@ -61,7 +60,6 @@ public static class AdminEndpoints 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); 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) => { - if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized(); - var player = await db.Players .Include(p => p.Suggestions) .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) => { 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); if (phase != Phase.Vote) @@ -186,7 +182,7 @@ public static class AdminEndpoints admin.MapPost("/unlink-suggestions", async ([FromBody] UnlinkSuggestionsRequest request, HttpContext ctx, AppDbContext 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); if (phase != Phase.Vote) @@ -240,8 +236,6 @@ public static class AdminEndpoints admin.MapPost("/reset", async (HttpContext ctx, AppDbContext db) => { - if (!await EndpointHelpers.IsAdmin(ctx, db)) return Results.Unauthorized(); - await db.Votes.ExecuteDeleteAsync(); await db.Suggestions.ExecuteDeleteAsync(); @@ -258,8 +252,6 @@ public static class AdminEndpoints 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 db.Votes.ExecuteDeleteAsync(); diff --git a/Endpoints/EndpointHelpers.cs b/Endpoints/EndpointHelpers.cs index 598344c..d15c13e 100644 --- a/Endpoints/EndpointHelpers.cs +++ b/Endpoints/EndpointHelpers.cs @@ -11,10 +11,10 @@ internal static class EndpointHelpers { public static async Task GetAuthenticatedPlayer(HttpContext ctx, AppDbContext db) { - if (ctx?.User?.Identity?.IsAuthenticated != true) - { - return null; - } + if (ctx?.User?.Identity?.IsAuthenticated != true) return null; + + if (ctx.Items.TryGetValue(nameof(Player), out var cached) && cached is Player cachedPlayer) + return cachedPlayer; var idValue = ctx.User.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrWhiteSpace(idValue) || !Guid.TryParse(idValue, out var playerId)) @@ -28,7 +28,10 @@ internal static class EndpointHelpers if (existing is null) { await Infrastructure.PlayerIdentityExtensions.SignOutPlayerAsync(ctx); + return null; } + + ctx.Items[nameof(Player)] = existing; return existing; } diff --git a/Infrastructure/AdminOnlyFilter.cs b/Infrastructure/AdminOnlyFilter.cs new file mode 100644 index 0000000..be76265 --- /dev/null +++ b/Infrastructure/AdminOnlyFilter.cs @@ -0,0 +1,21 @@ +using GameList.Data; +using GameList.Endpoints; +using Microsoft.AspNetCore.Authorization; + +namespace GameList.Infrastructure; + +public class AdminOnlyFilter : IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var httpContext = context.HttpContext; + var db = httpContext.RequestServices.GetRequiredService(); + var player = await EndpointHelpers.GetAuthenticatedPlayer(httpContext, db); + if (player?.IsAdmin != true) + { + return Results.Unauthorized(); + } + + return await next(context); + } +}