feat(bff): POST /behandel/registrations/{id}/decide behind the behandelaar policy (refs #13)
Forwards a behandelaar's goedkeuren/afwijzen to the domain (which applies the decision and completes the workflow task). Validates the besluit vocabulary (400 on unknown) without troubling the domain. openapi.json + api-client regenerated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,10 @@ import {
|
|||||||
Observable
|
Observable
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
export interface DecideRequest {
|
||||||
|
besluit: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OpenbaarEntry {
|
export interface OpenbaarEntry {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -252,4 +256,42 @@ export class BffApiV1Service {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postBehandelRegistrationsIdDecide<TData = void>(id: string,
|
||||||
|
decideRequest: DecideRequest, options?: HttpClientBodyOptions): Observable<TData>;
|
||||||
|
postBehandelRegistrationsIdDecide<TData = void>(id: string,
|
||||||
|
decideRequest: DecideRequest, options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||||
|
postBehandelRegistrationsIdDecide<TData = void>(id: string,
|
||||||
|
decideRequest: DecideRequest, options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||||
|
postBehandelRegistrationsIdDecide<TData = void>(
|
||||||
|
id: string,
|
||||||
|
decideRequest: DecideRequest, options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||||
|
if (options?.observe === 'events') {
|
||||||
|
return this.http.post<TData>(
|
||||||
|
`/behandel/registrations/${id}/decide`,
|
||||||
|
decideRequest,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'events',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.observe === 'response') {
|
||||||
|
return this.http.post<TData>(
|
||||||
|
`/behandel/registrations/${id}/decide`,
|
||||||
|
decideRequest,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'response',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.post<TData>(
|
||||||
|
`/behandel/registrations/${id}/decide`,
|
||||||
|
decideRequest,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'body',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,8 +103,28 @@ app.MapGet("/behandel/werkbak", async (IDomainClient domain, CancellationToken c
|
|||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.Produces(StatusCodes.Status403Forbidden);
|
.Produces(StatusCodes.Status403Forbidden);
|
||||||
|
|
||||||
|
// A behandelaar's beoordeling on a registration (goedkeuren/afwijzen). Forwarded to the domain, which
|
||||||
|
// applies the decision and completes the workflow task (ADR-0013). Same medewerker/behandelaar gate.
|
||||||
|
app.MapPost("/behandel/registrations/{id}/decide",
|
||||||
|
async (string id, DecideRequest body, IDomainClient domain, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
if (!BehandelAuth.IsKnownBesluit(body.Besluit))
|
||||||
|
return Results.BadRequest(new { error = $"Unknown besluit '{body.Besluit}'. Expected 'goedkeuren' or 'afwijzen'." });
|
||||||
|
|
||||||
|
await domain.DecideAsync(id, body.Besluit, ct);
|
||||||
|
return Results.NoContent();
|
||||||
|
})
|
||||||
|
.RequireAuthorization(BehandelAuth.Policy)
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status403Forbidden);
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
/// <summary>The behandelaar's decision on a registration.</summary>
|
||||||
|
public sealed record DecideRequest(string Besluit);
|
||||||
|
|
||||||
// Behandel (medewerker-realm) authentication + authorization wiring (ADR-0013).
|
// Behandel (medewerker-realm) authentication + authorization wiring (ADR-0013).
|
||||||
internal static class BehandelAuth
|
internal static class BehandelAuth
|
||||||
{
|
{
|
||||||
@@ -112,6 +132,12 @@ internal static class BehandelAuth
|
|||||||
public const string Policy = "behandelaar";
|
public const string Policy = "behandelaar";
|
||||||
public const string BehandelaarRole = "behandelaar";
|
public const string BehandelaarRole = "behandelaar";
|
||||||
|
|
||||||
|
/// <summary>The beoordeling vocabulary the BFF accepts (case-insensitive); an unknown besluit is a
|
||||||
|
/// 400 without troubling the domain. Mirrors the domain's <c>BeoordelingsBesluit</c>.</summary>
|
||||||
|
public static bool IsKnownBesluit(string? besluit) =>
|
||||||
|
string.Equals(besluit, "goedkeuren", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(besluit, "afwijzen", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>Lift Keycloak's realm roles (the nested <c>realm_access.roles</c> claim) onto the
|
/// <summary>Lift Keycloak's realm roles (the nested <c>realm_access.roles</c> claim) onto the
|
||||||
/// principal as role claims, so <c>RequireRole</c> can authorize on them.</summary>
|
/// principal as role claims, so <c>RequireRole</c> can authorize on them.</summary>
|
||||||
public static void AddRealmRoles(ClaimsPrincipal? principal)
|
public static void AddRealmRoles(ClaimsPrincipal? principal)
|
||||||
|
|||||||
@@ -88,10 +88,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/behandel/registrations/{id}/decide": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Bff.Api"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DecideRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"DecideRequest": {
|
||||||
|
"required": [
|
||||||
|
"besluit"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"besluit": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"OpenbaarEntry": {
|
"OpenbaarEntry": {
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
|
|||||||
Reference in New Issue
Block a user