feat(bff): BFF with one endpoint per portal + OIDC validation (closes #8) #64
@@ -27,7 +27,14 @@ builder.Services.AddHttpClient<IDomainClient, DomainClient>(c => c.BaseAddress =
|
|||||||
builder.Services.AddHttpClient<IProjectionClient, ProjectionClient>(c => c.BaseAddress = new Uri(projectionBaseUrl));
|
builder.Services.AddHttpClient<IProjectionClient, ProjectionClient>(c => c.BaseAddress = new Uri(projectionBaseUrl));
|
||||||
|
|
||||||
builder.Services.AddHealthChecks();
|
builder.Services.AddHealthChecks();
|
||||||
builder.Services.AddOpenApi();
|
// Clear the auto-populated `servers` block so the committed spec is stable regardless of the host
|
||||||
|
// the doc was generated from (the client sets its own base URL). Keeps the drift guard deterministic.
|
||||||
|
builder.Services.AddOpenApi(options =>
|
||||||
|
options.AddDocumentTransformer((document, _, _) =>
|
||||||
|
{
|
||||||
|
document.Servers?.Clear();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -47,14 +54,19 @@ app.MapPost("/self-service/registrations", async (ClaimsPrincipal user, IDomainC
|
|||||||
|
|
||||||
var accepted = await domain.SubmitRegistrationAsync(bsn, ct);
|
var accepted = await domain.SubmitRegistrationAsync(bsn, ct);
|
||||||
return Results.Accepted($"/self-service/registrations/{accepted.RegistrationId}", accepted);
|
return Results.Accepted($"/self-service/registrations/{accepted.RegistrationId}", accepted);
|
||||||
}).RequireAuthorization();
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces<SubmitAccepted>(StatusCodes.Status202Accepted)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized);
|
||||||
|
|
||||||
// Openbaar register: an anonymous public lookup that exposes only public-safe fields (S-09).
|
// Openbaar register: an anonymous public lookup that exposes only public-safe fields (S-09).
|
||||||
app.MapGet("/openbaar/register", async (string? q, IProjectionClient projection, CancellationToken ct) =>
|
app.MapGet("/openbaar/register", async (string? q, IProjectionClient projection, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var entries = await projection.GetRegisterAsync(ct);
|
var entries = await projection.GetRegisterAsync(ct);
|
||||||
return Results.Ok(OpenbaarProjection.PublicView(entries, q));
|
return Results.Ok(OpenbaarProjection.PublicView(entries, q));
|
||||||
});
|
})
|
||||||
|
.Produces<IReadOnlyList<OpenbaarEntry>>(StatusCodes.Status200OK);
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
|||||||
34
services/bff/Bff.Tests/OpenApiSpecTests.cs
Normal file
34
services/bff/Bff.Tests/OpenApiSpecTests.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Bff.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guards the committed OpenAPI contract (<c>services/bff/openapi.json</c>) against drift: it must
|
||||||
|
/// equal the document the running BFF serves. S-08's Angular client is generated from this file, so a
|
||||||
|
/// stale spec is a bug. To regenerate: run the BFF and save <c>/openapi/v1.json</c> over the file.
|
||||||
|
/// </summary>
|
||||||
|
public class OpenApiSpecTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Committed_openapi_spec_matches_the_served_document()
|
||||||
|
{
|
||||||
|
using var factory = new BffFactory();
|
||||||
|
|
||||||
|
var served = await factory.CreateClient().GetStringAsync("/openapi/v1.json");
|
||||||
|
var committed = await File.ReadAllTextAsync(SpecPath());
|
||||||
|
|
||||||
|
Assert.Equal(Canonical(committed), Canonical(served));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reformat both sides identically so the comparison is about content, not whitespace.
|
||||||
|
private static string Canonical(string json)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
return JsonSerializer.Serialize(doc.RootElement, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// The committed spec sits at services/bff/openapi.json — one level up from this test file.
|
||||||
|
private static string SpecPath([CallerFilePath] string thisFile = "")
|
||||||
|
=> Path.Combine(Path.GetDirectoryName(thisFile)!, "..", "openapi.json");
|
||||||
|
}
|
||||||
104
services/bff/openapi.json
Normal file
104
services/bff/openapi.json
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.1.1",
|
||||||
|
"info": {
|
||||||
|
"title": "Bff.Api | v1",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/self-service/registrations": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Bff.Api"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"202": {
|
||||||
|
"description": "Accepted",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SubmitAccepted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/openbaar/register": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Bff.Api"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "q",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/OpenbaarEntry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"OpenbaarEntry": {
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SubmitAccepted": {
|
||||||
|
"required": [
|
||||||
|
"registrationId",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"registrationId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Bff.Api"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user