feat(acl): diploma upload stored in the ZGW Documenten API (S-10b, closes #103) #108
@@ -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<TData = void>(id: string, options?: HttpClientBodyOptions): Observable<TData>;
|
||||
postSelfServiceRegistrationsIdDocuments<TData = void>(id: string, options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||
postSelfServiceRegistrationsIdDocuments<TData = void>(id: string, options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||
postSelfServiceRegistrationsIdDocuments<TData = void>(id: string,
|
||||
provideDocumentsRequest: ProvideDocumentsRequest, options?: HttpClientBodyOptions): Observable<TData>;
|
||||
postSelfServiceRegistrationsIdDocuments<TData = void>(id: string,
|
||||
provideDocumentsRequest: ProvideDocumentsRequest, options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||
postSelfServiceRegistrationsIdDocuments<TData = void>(id: string,
|
||||
provideDocumentsRequest: ProvideDocumentsRequest, options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||
postSelfServiceRegistrationsIdDocuments<TData = void>(
|
||||
id: string, options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||
id: string,
|
||||
provideDocumentsRequest: ProvideDocumentsRequest, options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||
if (options?.observe === 'events') {
|
||||
return this.http.post<TData>(
|
||||
`/self-service/registrations/${id}/documents`,
|
||||
undefined,{
|
||||
provideDocumentsRequest,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'events',
|
||||
}
|
||||
@@ -244,7 +256,7 @@ export class BffApiV1Service {
|
||||
if (options?.observe === 'response') {
|
||||
return this.http.post<TData>(
|
||||
`/self-service/registrations/${id}/documents`,
|
||||
undefined,{
|
||||
provideDocumentsRequest,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'response',
|
||||
}
|
||||
@@ -253,7 +265,7 @@ export class BffApiV1Service {
|
||||
|
||||
return this.http.post<TData>(
|
||||
`/self-service/registrations/${id}/documents`,
|
||||
undefined,{
|
||||
provideDocumentsRequest,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'body',
|
||||
}
|
||||
|
||||
@@ -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.</summary>
|
||||
Task<bool> WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Provide the documents the caller's own registration is waiting for ("documenten
|
||||
/// aanleveren"). Owner-scoped by <paramref name="bsn"/>. Returns <c>false</c> when the domain
|
||||
/// reports the registration is unknown or not the caller's (404), so the BFF can relay a 404.</summary>
|
||||
Task<bool> ProvideDocumentsAsync(string registrationId, string bsn, CancellationToken ct = default);
|
||||
/// <summary>Provide (upload) the diploma the caller's own registration is waiting for ("documenten
|
||||
/// aanleveren"). The file is carried base64-encoded. Owner-scoped by <paramref name="bsn"/>. Returns
|
||||
/// <c>false</c> when the domain reports the registration is unknown or not the caller's (404).</summary>
|
||||
Task<bool> ProvideDocumentsAsync(
|
||||
string registrationId, string bsn, string contentBase64, string? fileName, string? contentType, CancellationToken ct = default);
|
||||
|
||||
/// <summary>The behandelaar's werkbak — registrations awaiting beoordeling.</summary>
|
||||
Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default);
|
||||
@@ -68,10 +69,12 @@ public sealed class DomainClient(HttpClient http) : IDomainClient
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ProvideDocumentsAsync(string registrationId, string bsn, CancellationToken ct = default)
|
||||
public async Task<bool> 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;
|
||||
|
||||
@@ -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();
|
||||
/// <summary>The behandelaar's decision on a registration.</summary>
|
||||
public sealed record DecideRequest(string Besluit);
|
||||
|
||||
/// <summary>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.</summary>
|
||||
public sealed record ProvideDocumentsRequest(string ContentBase64, string? FileName = null, string? ContentType = null);
|
||||
|
||||
// Behandel (medewerker-realm) authentication + authorization wiring (ADR-0013).
|
||||
internal static class BehandelAuth
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user