From 4c516cdad34144ae4550e0dd8d54b454f88966af Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Mon, 20 Jul 2026 12:13:10 +0200 Subject: [PATCH] feat(bff): documents endpoint accepts the base64 file and forwards it to the domain (refs #103) The self-service documents endpoint takes { contentBase64, fileName, contentType } as JSON (bsn from the token) and forwards it via IDomainClient.ProvideDocumentsAsync. Regenerates openapi.json + the Angular client (postSelfServiceRegistrationsIdDocuments now takes a ProvideDocumentsRequest body). Co-Authored-By: Claude Opus 4.8 (1M context) --- libs/api-client/src/lib/generated/bff-api.ts | 26 ++++++++++----- services/bff/Bff.Api/DownstreamClients.cs | 15 +++++---- services/bff/Bff.Api/Program.cs | 10 ++++-- services/bff/openapi.json | 33 ++++++++++++++++++++ 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/libs/api-client/src/lib/generated/bff-api.ts b/libs/api-client/src/lib/generated/bff-api.ts index f5d35d7..3ac28e9 100644 --- a/libs/api-client/src/lib/generated/bff-api.ts +++ b/libs/api-client/src/lib/generated/bff-api.ts @@ -35,6 +35,14 @@ export interface OpenbaarEntry { reference: string | null; } +export interface ProvideDocumentsRequest { + contentBase64: string; + /** @nullable */ + fileName?: string | null; + /** @nullable */ + contentType?: string | null; +} + export interface SubmitAccepted { registrationId: string; status: string; @@ -226,15 +234,19 @@ export class BffApiV1Service { ); } - postSelfServiceRegistrationsIdDocuments(id: string, options?: HttpClientBodyOptions): Observable; - postSelfServiceRegistrationsIdDocuments(id: string, options?: HttpClientEventOptions): Observable>; - postSelfServiceRegistrationsIdDocuments(id: string, options?: HttpClientResponseOptions): Observable>; + postSelfServiceRegistrationsIdDocuments(id: string, + provideDocumentsRequest: ProvideDocumentsRequest, options?: HttpClientBodyOptions): Observable; + postSelfServiceRegistrationsIdDocuments(id: string, + provideDocumentsRequest: ProvideDocumentsRequest, options?: HttpClientEventOptions): Observable>; + postSelfServiceRegistrationsIdDocuments(id: string, + provideDocumentsRequest: ProvideDocumentsRequest, options?: HttpClientResponseOptions): Observable>; postSelfServiceRegistrationsIdDocuments( - id: string, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { + id: string, + provideDocumentsRequest: ProvideDocumentsRequest, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { if (options?.observe === 'events') { return this.http.post( `/self-service/registrations/${id}/documents`, - undefined,{ + provideDocumentsRequest,{ ...(options as Omit, 'observe'>), observe: 'events', } @@ -244,7 +256,7 @@ export class BffApiV1Service { if (options?.observe === 'response') { return this.http.post( `/self-service/registrations/${id}/documents`, - undefined,{ + provideDocumentsRequest,{ ...(options as Omit, 'observe'>), observe: 'response', } @@ -253,7 +265,7 @@ export class BffApiV1Service { return this.http.post( `/self-service/registrations/${id}/documents`, - undefined,{ + provideDocumentsRequest,{ ...(options as Omit, 'observe'>), observe: 'body', } diff --git a/services/bff/Bff.Api/DownstreamClients.cs b/services/bff/Bff.Api/DownstreamClients.cs index f935247..cf0e448 100644 --- a/services/bff/Bff.Api/DownstreamClients.cs +++ b/services/bff/Bff.Api/DownstreamClients.cs @@ -27,10 +27,11 @@ public interface IDomainClient /// unknown or not the caller's (404), so the BFF can relay a 404 rather than a 500. Task WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default); - /// Provide the documents the caller's own registration is waiting for ("documenten - /// aanleveren"). Owner-scoped by . Returns false when the domain - /// reports the registration is unknown or not the caller's (404), so the BFF can relay a 404. - Task ProvideDocumentsAsync(string registrationId, string bsn, CancellationToken ct = default); + /// Provide (upload) the diploma the caller's own registration is waiting for ("documenten + /// aanleveren"). The file is carried base64-encoded. Owner-scoped by . Returns + /// false when the domain reports the registration is unknown or not the caller's (404). + Task ProvideDocumentsAsync( + string registrationId, string bsn, string contentBase64, string? fileName, string? contentType, CancellationToken ct = default); /// The behandelaar's werkbak — registrations awaiting beoordeling. Task> GetWerkbakAsync(CancellationToken ct = default); @@ -68,10 +69,12 @@ public sealed class DomainClient(HttpClient http) : IDomainClient return true; } - public async Task ProvideDocumentsAsync(string registrationId, string bsn, CancellationToken ct = default) + public async Task ProvideDocumentsAsync( + string registrationId, string bsn, string contentBase64, string? fileName, string? contentType, CancellationToken ct = default) { using var response = await http.PostAsJsonAsync( - $"registrations/{registrationId}/documents", new { bsn }, ct); + $"registrations/{registrationId}/documents", + new { bsn, contentBase64, fileName, contentType }, ct); // The domain 404s an unknown or not-owned registration; relay that rather than fail hard. if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return false; diff --git a/services/bff/Bff.Api/Program.cs b/services/bff/Bff.Api/Program.cs index 940d5f2..f2a660a 100644 --- a/services/bff/Bff.Api/Program.cs +++ b/services/bff/Bff.Api/Program.cs @@ -109,13 +109,15 @@ app.MapPost("/self-service/registrations/{id}/withdraw", async (string id, Claim // forwarded to the domain, which owner-scopes the action and completes the WachtOpDocumenten task; a // registration that is unknown or not the caller's comes back 404. The real file upload + ZGW storage // is S-10b — this is the trigger that unblocks the process. -app.MapPost("/self-service/registrations/{id}/documents", async (string id, ClaimsPrincipal user, IDomainClient domain, CancellationToken ct) => +app.MapPost("/self-service/registrations/{id}/documents", async (string id, ProvideDocumentsRequest body, ClaimsPrincipal user, IDomainClient domain, CancellationToken ct) => { var bsn = user.FindFirstValue("bsn"); if (string.IsNullOrWhiteSpace(bsn)) return Results.BadRequest("The token carries no bsn claim."); + if (string.IsNullOrWhiteSpace(body?.ContentBase64)) + return Results.BadRequest("A document is required."); - var provided = await domain.ProvideDocumentsAsync(id, bsn, ct); + var provided = await domain.ProvideDocumentsAsync(id, bsn, body.ContentBase64, body.FileName, body.ContentType, ct); return provided ? Results.NoContent() : Results.NotFound(); }) .RequireAuthorization() @@ -163,6 +165,10 @@ app.Run(); /// The behandelaar's decision on a registration. public sealed record DecideRequest(string Besluit); +/// A diploma upload from the self-service portal — the file base64-encoded client-side, with +/// its name and MIME type. The bsn is taken from the DigiD token, not this body. +public sealed record ProvideDocumentsRequest(string ContentBase64, string? FileName = null, string? ContentType = null); + // Behandel (medewerker-realm) authentication + authorization wiring (ADR-0013). internal static class BehandelAuth { diff --git a/services/bff/openapi.json b/services/bff/openapi.json index fd2101a..02359e1 100644 --- a/services/bff/openapi.json +++ b/services/bff/openapi.json @@ -76,6 +76,16 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProvideDocumentsRequest" + } + } + }, + "required": true + }, "responses": { "204": { "description": "No Content" @@ -228,6 +238,29 @@ } } }, + "ProvideDocumentsRequest": { + "required": [ + "contentBase64" + ], + "type": "object", + "properties": { + "contentBase64": { + "type": "string" + }, + "fileName": { + "type": [ + "null", + "string" + ] + }, + "contentType": { + "type": [ + "null", + "string" + ] + } + } + }, "SubmitAccepted": { "required": [ "registrationId",