Add OpenAPI contract and generated frontend client

This commit is contained in:
2026-02-18 21:25:07 +01:00
parent e55a1b01f4
commit 1802fd6607
19 changed files with 1509 additions and 126 deletions

View File

@@ -9,36 +9,36 @@ public static class AdminEndpoints
{
public static void MapAdminEndpoints(this IEndpointRouteBuilder app)
{
var admin = app.MapGroup("/api/admin").RequireAuthorization().RequireRateLimiting("admin-sensitive").AddEndpointFilter<AdminOnlyFilter>();
var admin = app.MapGroup("/api/admin").WithTags("Admin").RequireAuthorization().RequireRateLimiting("admin-sensitive").AddEndpointFilter<AdminOnlyFilter>();
admin.MapPost("/results", async ([FromBody] ResultsOpenRequest request, AdminWorkflowService service) =>
{
var result = await service.SetResultsOpenAsync(request.ResultsOpen);
return result.ToHttpResult(Results.Ok);
});
}).WithName("SetResultsOpen");
admin.MapGet("/vote-status", async (AdminWorkflowService service) =>
{
var result = await service.GetVoteStatusAsync();
return result.ToHttpResult(Results.Ok);
});
}).WithName("GetVoteStatus");
admin.MapPost("/joker", async ([FromBody] GrantJokerRequest request, AdminWorkflowService service) =>
{
var result = await service.GrantJokerAsync(request.PlayerId);
return result.ToHttpResult(Results.Ok);
});
}).WithName("GrantJoker");
admin.MapPost("/player-phase", async ([FromBody] SetPlayerPhaseRequest request, AdminWorkflowService service) =>
{
var result = await service.SetPlayerPhaseAsync(request.PlayerId, request.Phase);
return result.ToHttpResult(Results.Ok);
});
}).WithName("SetPlayerPhase");
admin.MapPost("/player-admin", async ([FromBody] SetPlayerAdminRequest request, AdminWorkflowService service) =>
{
var result = await service.SetPlayerAdminAsync(request.PlayerId, request.IsAdmin);
return result.ToHttpResult(Results.Ok);
});
}).WithName("SetPlayerAdmin");
admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, [FromBody] AdminPasswordRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
{
@@ -48,7 +48,7 @@ public static class AdminEndpoints
var result = await service.DeletePlayerAsync(playerId, player.Id, request.Password, ctx);
return result.ToHttpResult(Results.Ok);
});
}).WithName("DeletePlayer");
admin.MapPost("/link-suggestions", async ([FromBody] LinkSuggestionsRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
{
@@ -58,7 +58,7 @@ public static class AdminEndpoints
var result = await service.LinkSuggestionsAsync(player.Id, request.SourceSuggestionId, request.TargetSuggestionId);
return result.ToHttpResult(Results.Ok);
});
}).WithName("LinkSuggestions");
admin.MapPost("/unlink-suggestions", async ([FromBody] UnlinkSuggestionsRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
{
@@ -68,7 +68,7 @@ public static class AdminEndpoints
var result = await service.UnlinkSuggestionsAsync(player.Id, request.SuggestionId);
return result.ToHttpResult(Results.Ok);
});
}).WithName("UnlinkSuggestions");
admin.MapPost("/reset", async ([FromBody] AdminPasswordRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
{
@@ -78,7 +78,7 @@ public static class AdminEndpoints
var result = await service.ResetAsync(player.Id, request.Password, ctx);
return result.ToHttpResult(Results.Ok);
});
}).WithName("Reset");
admin.MapPost("/factory-reset", async ([FromBody] AdminPasswordRequest request, HttpContext ctx, AppDbContext db, AdminWorkflowService service) =>
{
@@ -88,7 +88,7 @@ public static class AdminEndpoints
var result = await service.FactoryResetAsync(player.Id, request.Password, ctx);
return result.ToHttpResult(Results.Ok);
});
}).WithName("FactoryReset");
}
}

View File

@@ -11,13 +11,13 @@ public static class AuthEndpoints
{
public static void MapAuthEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/auth").RequireRateLimiting("auth-sensitive");
var group = app.MapGroup("/api/auth").WithTags("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));
});
}).WithName("GetAuthOptions");
group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db, IConfiguration config, AuthAttemptMonitor authAttemptMonitor) =>
{
@@ -94,7 +94,7 @@ public static class AuthEndpoints
player.DisplayName,
player.IsAdmin
));
});
}).WithName("Register");
group.MapPost("/login", async ([FromBody] LoginRequest request, HttpContext ctx, AppDbContext db, AuthAttemptMonitor authAttemptMonitor) =>
{
@@ -137,13 +137,13 @@ public static class AuthEndpoints
player.DisplayName,
player.IsAdmin
));
});
}).WithName("Login");
group.MapPost("/logout", async (HttpContext ctx) =>
{
await PlayerIdentityExtensions.SignOutPlayerAsync(ctx);
return Results.NoContent();
});
}).WithName("Logout");
}
private static string NormalizeActor(string? username) => string.IsNullOrWhiteSpace(username) ? "(missing)" : username.Trim();

View File

@@ -9,6 +9,7 @@ public static class ResultsEndpoints
public static void MapResultsEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/results")
.WithTags("Results")
.RequireAuthorization()
.AddEndpointFilter(new PhaseRequirementFilter(Phase.Results));
@@ -20,7 +21,7 @@ public static class ResultsEndpoints
var result = await service.GetResultsAsync(player.Id);
return result.ToHttpResult(Results.Ok);
});
}).WithName("GetResults");
}
}

View File

@@ -7,7 +7,7 @@ public static class StateEndpoints
{
public static void MapStateEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api").RequireAuthorization();
var group = app.MapGroup("/api").WithTags("State").RequireAuthorization();
group.MapGet("/state", async (HttpContext ctx, AppDbContext db, StateWorkflowService service, StateChangeNotifier notifier) =>
{
@@ -28,7 +28,7 @@ public static class StateEndpoints
ctx.Response.Headers.ETag = notifier.CurrentEtag;
return Results.Ok(payload);
});
});
}).WithName("GetState");
group.MapGet("/events/state", async (HttpContext ctx, AppDbContext db, StateChangeNotifier notifier) =>
{
@@ -73,7 +73,7 @@ public static class StateEndpoints
}
return Results.Empty;
});
}).WithName("GetStateEvents");
group.MapGet("/me", async (HttpContext ctx, AppDbContext db, StateWorkflowService service) =>
{
@@ -83,7 +83,7 @@ public static class StateEndpoints
var result = await service.GetMeAsync(player);
return result.ToHttpResult(Results.Ok);
});
}).WithName("GetMe");
group.MapPost("/me/phase/next", async (HttpContext ctx, AppDbContext db, StateWorkflowService service) =>
{
@@ -93,7 +93,7 @@ public static class StateEndpoints
var result = await service.NextPhaseAsync(player);
return result.ToHttpResult(Results.Ok);
});
}).WithName("NextPhase");
group.MapPost("/me/phase/prev", async (HttpContext ctx, AppDbContext db, StateWorkflowService service) =>
{
@@ -103,7 +103,7 @@ public static class StateEndpoints
var result = await service.PrevPhaseAsync(player);
return result.ToHttpResult(Results.Ok);
});
}).WithName("PrevPhase");
}

View File

@@ -9,7 +9,7 @@ public static class SuggestEndpoints
{
public static void MapSuggestEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/suggestions").RequireAuthorization();
var group = app.MapGroup("/api/suggestions").WithTags("Suggestions").RequireAuthorization();
group.MapGet("/mine", async (HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
{
@@ -19,7 +19,7 @@ public static class SuggestEndpoints
var result = await service.GetMineAsync(player.Id);
return result.ToHttpResult(Results.Ok);
});
}).WithName("GetMySuggestions");
group.MapPost("/", async ([FromBody] SuggestionRequest request, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
{
@@ -42,7 +42,7 @@ public static class SuggestEndpoints
);
return result.ToHttpResult(payload => Results.Created($"/api/suggestions/{payload.Id}", payload));
}).AddEndpointFilter(new PhaseOrJokerFilter());
}).AddEndpointFilter(new PhaseOrJokerFilter()).WithName("CreateSuggestion");
group.MapDelete("/{id:int}", async (int id, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
{
@@ -52,7 +52,7 @@ public static class SuggestEndpoints
var result = await service.DeleteAsync(player.Id, id);
return result.ToHttpResult(Results.NoContent);
});
}).WithName("DeleteSuggestion");
group.MapPut("/{id:int}", async (int id, [FromBody] SuggestionRequest request, HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
{
@@ -76,7 +76,7 @@ public static class SuggestEndpoints
);
return result.ToHttpResult(Results.Ok);
});
}).WithName("UpdateSuggestion");
group.MapGet("/all", async (HttpContext ctx, AppDbContext db, SuggestionWorkflowService service) =>
{
@@ -86,7 +86,7 @@ public static class SuggestEndpoints
var result = await service.GetAllAsync(player.Id);
return result.ToHttpResult(Results.Ok);
});
}).WithName("GetAllSuggestions");
}
}

View File

@@ -9,7 +9,7 @@ public static class VoteEndpoints
{
public static void MapVoteEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/votes").RequireAuthorization().AddEndpointFilter(new PhaseRequirementFilter(Phase.Vote));
var group = app.MapGroup("/api/votes").WithTags("Votes").RequireAuthorization().AddEndpointFilter(new PhaseRequirementFilter(Phase.Vote));
group.MapGet("/mine", async (HttpContext ctx, AppDbContext db, VoteWorkflowService service) =>
{
@@ -19,7 +19,7 @@ public static class VoteEndpoints
var result = await service.GetMineAsync(player.Id);
return result.ToHttpResult(Results.Ok);
});
}).WithName("GetMyVotes");
group.MapPost("/", async (VoteRequest request, HttpContext ctx, AppDbContext db, VoteWorkflowService service) =>
{
@@ -29,7 +29,7 @@ public static class VoteEndpoints
var result = await service.UpsertAsync(player.Id, request.SuggestionId, request.Score);
return result.ToHttpResult(Results.Ok);
});
}).WithName("UpsertVote");
group.MapPost("/finalize", async (VoteFinalizeRequest request, HttpContext ctx, AppDbContext db, VoteWorkflowService service) =>
{
@@ -39,7 +39,7 @@ public static class VoteEndpoints
var result = await service.SetFinalizeAsync(player.Id, request.Final);
return result.ToHttpResult(Results.Ok);
});
}).WithName("SetVotesFinalized");
}
}