using System.Text.Json; using EventSubscriber.Application; using Projection.ReadModel; var builder = WebApplication.CreateBuilder(args); var connectionString = builder.Configuration.GetConnectionString("Projection") ?? throw new InvalidOperationException("Missing connection string 'ConnectionStrings:Projection'"); // The exact Authorization header value Open Notificaties sends on each abonnement callback. // NRC probes the callback during registration and refuses it unless it returns 401 without // this value (ADR-0007), so the webhook enforces it. var webhookToken = builder.Configuration["EventSubscriber:Webhook:AuthToken"] ?? throw new InvalidOperationException("Missing configuration 'EventSubscriber:Webhook:AuthToken'"); builder.Services.AddProjectionReadModel(connectionString); builder.Services.AddProjectionWriteSide(); var app = builder.Build(); // Apply migrations on start so a fresh stack reaches a usable schema unattended. await app.Services.MigrateProjectionAsync(); app.MapGet("/health", () => "Healthy"); // The NRC abonnement callback. Open Notificaties POSTs a notification here; we project it. // Auth-on-callback is mandatory: the auth check runs *before* the body is read, so NRC's // registration probe (a POST without the configured Authorization, and without a valid // notification body) gets the 401 it requires rather than a 400 (ADR-0007). app.MapPost("/notifications", async ( HttpRequest request, NotificationProjector projector, CancellationToken ct) => { if (request.Headers.Authorization != webhookToken) return Results.Unauthorized(); var dto = await JsonSerializer.DeserializeAsync( request.Body, SerializerOptions, ct); if (dto is null) return Results.BadRequest(); await projector.HandleAsync(dto.ToNotification(), ct); return Results.NoContent(); }); // Admin: rebuild the projection from the durable notification log (PRD §8.4 — rebuildable). app.MapPost("/admin/rebuild", async (NotificationProjector projector, CancellationToken ct) => { await projector.RebuildAsync(ct); return Results.NoContent(); }); await app.RunAsync(); /// The NRC notification body, as Open Notificaties POSTs it. Only the fields the /// projection needs are bound; aanmaakdatum/kenmerken are ignored for the minimal slice. public sealed record NotificationDto(string Kanaal, string Resource, string Actie, Uri ResourceUrl) { public Notification ToNotification() => new(Kanaal, Resource, Actie, ResourceUrl); } public partial class Program { // NRC sends camelCase JSON; match it case-insensitively. private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); }