Upload feature (a): BFF endpoints + category config + tests
- Domain/Documents: server-owned category config per wizard + authoritative
type/size validation (DocumentRules).
- Data/DocumentStore: in-memory metadata store (no file bytes/PII) + audit log;
user delete (owner-scoped, 409 once linked), admin delete (role seam via
X-Admin header), link-on-submit, poll-by-localId status.
- Program.cs: GET /uploads/categories, POST /uploads (multipart, excluded from
OpenAPI — hand-written on FE), GET /uploads/status, DELETE /uploads/{id},
DELETE /admin/uploads/{id}. Submit links digital docs + records post-delivery.
- Contracts extended (DocumentRefDto on registratie/herregistratie submit);
regenerated NSwag client + swagger.json (drift check stays green).
- Tests: 16 new (endpoints + DocumentRules); dotnet test 44/44.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -46,11 +46,28 @@ public sealed record DuoLookupDto(IReadOnlyList<DuoDiplomaDto> Diplomas, ManualD
|
|||||||
|
|
||||||
public sealed record IntakePolicyDto(int ScholingThreshold);
|
public sealed record IntakePolicyDto(int ScholingThreshold);
|
||||||
|
|
||||||
|
// --- Document upload contracts ---
|
||||||
|
|
||||||
|
public sealed record DocumentCategoryDto(
|
||||||
|
string CategoryId, string Label, string Description, bool Required,
|
||||||
|
IReadOnlyList<string> AcceptedTypes, int MaxSizeMb, bool Multiple, bool AllowPostDelivery);
|
||||||
|
|
||||||
|
public sealed record UploadCategoriesDto(IReadOnlyList<DocumentCategoryDto> Categories);
|
||||||
|
|
||||||
|
public sealed record UploadResponse(string DocumentId, string LocalId);
|
||||||
|
|
||||||
|
public sealed record UploadStatusItemDto(string LocalId, string Status, string? DocumentId);
|
||||||
|
public sealed record UploadStatusDto(IReadOnlyList<UploadStatusItemDto> Results);
|
||||||
|
|
||||||
|
// Per-category delivery intent on submit: digital categories carry their DocumentId,
|
||||||
|
// post-delivery categories carry Channel="post".
|
||||||
|
public sealed record DocumentRefDto(string CategoryId, string Channel, string? DocumentId = null);
|
||||||
|
|
||||||
// Submit requests carry only the fields the server re-validates (UX-only fields
|
// Submit requests carry only the fields the server re-validates (UX-only fields
|
||||||
// stay on the client). ponytail: a real submit would carry the full application.
|
// stay on the client). ponytail: a real submit would carry the full application.
|
||||||
public sealed record RegistratieRequest(string DiplomaHerkomst);
|
public sealed record RegistratieRequest(string DiplomaHerkomst, IReadOnlyList<DocumentRefDto>? Documents = null);
|
||||||
public sealed record IntakeRequest(int Uren);
|
public sealed record IntakeRequest(int Uren);
|
||||||
public sealed record HerregistratieRequest(int Uren);
|
public sealed record HerregistratieRequest(int Uren, IReadOnlyList<DocumentRefDto>? Documents = null);
|
||||||
public sealed record ChangeRequestRequest(string Straat, string Postcode, string Woonplaats);
|
public sealed record ChangeRequestRequest(string Straat, string Postcode, string Woonplaats);
|
||||||
|
|
||||||
public sealed record ReferentieResponse(string Referentie);
|
public sealed record ReferentieResponse(string Referentie);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using BigRegister.Domain.Diplomas;
|
using BigRegister.Domain.Diplomas;
|
||||||
|
using BigRegister.Domain.Documents;
|
||||||
using BigRegister.Domain.People;
|
using BigRegister.Domain.People;
|
||||||
using BigRegister.Domain.Registrations;
|
using BigRegister.Domain.Registrations;
|
||||||
|
|
||||||
@@ -30,4 +31,7 @@ public static class Mappers
|
|||||||
d.Id, d.Naam, d.Instelling, d.Jaar,
|
d.Id, d.Naam, d.Instelling, d.Jaar,
|
||||||
DiplomaRules.ProfessionFor(d),
|
DiplomaRules.ProfessionFor(d),
|
||||||
DiplomaRules.QuestionsFor(d).Select(q => q.ToDto()).ToList());
|
DiplomaRules.QuestionsFor(d).Select(q => q.ToDto()).ToList());
|
||||||
|
|
||||||
|
public static DocumentCategoryDto ToDto(this DocumentCategory c) => new(
|
||||||
|
c.CategoryId, c.Label, c.Description, c.Required, c.AcceptedTypes, c.MaxSizeMb, c.Multiple, c.AllowPostDelivery);
|
||||||
}
|
}
|
||||||
|
|||||||
101
backend/src/BigRegister.Api/Data/DocumentStore.cs
Normal file
101
backend/src/BigRegister.Api/Data/DocumentStore.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
namespace BigRegister.Api.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stored document metadata. The demo deliberately keeps NO file content — only
|
||||||
|
/// metadata — which is enough for status/delete/link and avoids holding PII bytes
|
||||||
|
/// in memory. A real backend persists the bytes to blob storage keyed by DocumentId.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record StoredDocument(
|
||||||
|
string DocumentId, string LocalId, string CategoryId, string WizardId,
|
||||||
|
string FileName, long SizeBytes, string Owner, DateTimeOffset UploadedAt)
|
||||||
|
{
|
||||||
|
public bool Linked { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record AuditEntry(DateTimeOffset At, string Action, string DocumentId, string CategoryId, string Actor);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-memory document store + audit log (no DB). ponytail: one global lock — fine
|
||||||
|
/// for a single-process demo store; swap for per-key locks if it ever serves load.
|
||||||
|
/// The audit log holds metadata only (never file content or other PII).
|
||||||
|
/// </summary>
|
||||||
|
public static class DocumentStore
|
||||||
|
{
|
||||||
|
/// The single seeded user (the demo has no real auth; ownership = this id).
|
||||||
|
public const string DemoOwner = "19012345601";
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, StoredDocument> _docs = new();
|
||||||
|
private static readonly List<AuditEntry> _audit = new();
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
|
||||||
|
public static StoredDocument Add(string localId, string categoryId, string wizardId, string fileName, long sizeBytes, string owner)
|
||||||
|
{
|
||||||
|
var doc = new StoredDocument(Guid.NewGuid().ToString(), localId, categoryId, wizardId, fileName, sizeBytes, owner, DateTimeOffset.UtcNow);
|
||||||
|
lock (_gate) _docs[doc.DocumentId] = doc;
|
||||||
|
Audit("upload", doc.DocumentId, categoryId, owner);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoredDocument? Get(string documentId)
|
||||||
|
{
|
||||||
|
lock (_gate) return _docs.TryGetValue(documentId, out var d) ? d : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Status for the poll-on-return pattern: a known localId is "complete" (it
|
||||||
|
/// arrived), an unknown one is still in flight / never started.
|
||||||
|
public static IReadOnlyList<StoredDocument> ByLocalIds(IEnumerable<string> localIds)
|
||||||
|
{
|
||||||
|
var set = localIds.ToHashSet();
|
||||||
|
lock (_gate) return _docs.Values.Where(d => set.Contains(d.LocalId)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark digital documents as linked to a finalised submission (blocks user delete).
|
||||||
|
public static void Link(IEnumerable<string> documentIds)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
foreach (var id in documentIds)
|
||||||
|
if (_docs.TryGetValue(id, out var d)) d.Linked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DeleteResult { Ok, NotFound, Linked }
|
||||||
|
|
||||||
|
/// User delete: owner-scoped; blocked once linked to a finalised submission.
|
||||||
|
public static DeleteResult DeleteOwned(string documentId, string owner)
|
||||||
|
{
|
||||||
|
string categoryId;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_docs.TryGetValue(documentId, out var d) || d.Owner != owner) return DeleteResult.NotFound;
|
||||||
|
if (d.Linked) return DeleteResult.Linked;
|
||||||
|
categoryId = d.CategoryId;
|
||||||
|
_docs.Remove(documentId);
|
||||||
|
}
|
||||||
|
Audit("delete-user", documentId, categoryId, owner);
|
||||||
|
return DeleteResult.Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Admin delete: bypasses ownership, unlinks, and (seam) flags the submission for
|
||||||
|
/// review so a caseworker is notified. Returns false if the document is unknown.
|
||||||
|
public static bool AdminDelete(string documentId, string actor)
|
||||||
|
{
|
||||||
|
string categoryId;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_docs.TryGetValue(documentId, out var d)) return false;
|
||||||
|
categoryId = d.CategoryId;
|
||||||
|
_docs.Remove(documentId);
|
||||||
|
}
|
||||||
|
Audit("delete-admin", documentId, categoryId, actor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Audit(string action, string documentId, string categoryId, string actor)
|
||||||
|
{
|
||||||
|
lock (_gate) _audit.Add(new AuditEntry(DateTimeOffset.UtcNow, action, documentId, categoryId, actor));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<AuditEntry> AuditLog
|
||||||
|
{
|
||||||
|
get { lock (_gate) return _audit.ToList(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
namespace BigRegister.Domain.Documents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SERVER-OWNED document category config. The frontend renders these; it never
|
||||||
|
/// hardcodes file types, size limits, labels, required-ness or the post-delivery
|
||||||
|
/// flag. Adding a new category or changing a rule is a backend-only change.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record DocumentCategory(
|
||||||
|
string CategoryId,
|
||||||
|
string Label,
|
||||||
|
string Description,
|
||||||
|
bool Required,
|
||||||
|
IReadOnlyList<string> AcceptedTypes,
|
||||||
|
int MaxSizeMb,
|
||||||
|
bool Multiple,
|
||||||
|
bool AllowPostDelivery);
|
||||||
|
|
||||||
|
public static class DocumentRules
|
||||||
|
{
|
||||||
|
private static readonly string[] Pdf = { "application/pdf" };
|
||||||
|
private static readonly string[] PdfImage = { "application/pdf", "image/jpeg", "image/png" };
|
||||||
|
|
||||||
|
/// <summary>The categories that apply to a given wizard/step.</summary>
|
||||||
|
public static IReadOnlyList<DocumentCategory> CategoriesFor(string wizardId) => wizardId switch
|
||||||
|
{
|
||||||
|
"registratie" => new[]
|
||||||
|
{
|
||||||
|
new DocumentCategory("diploma", "Diplomabewijs",
|
||||||
|
"Upload uw diploma als PDF-bestand.", true, Pdf, 10, false, false),
|
||||||
|
new DocumentCategory("identiteit", "Identiteitsbewijs",
|
||||||
|
"Upload een kopie van uw paspoort of ID-kaart.", true, PdfImage, 10, false, true),
|
||||||
|
},
|
||||||
|
"herregistratie" => new[]
|
||||||
|
{
|
||||||
|
new DocumentCategory("werkervaring", "Bewijs van werkervaring",
|
||||||
|
"Upload bewijs van uw gewerkte uren (bijv. een werkgeversverklaring).", true, Pdf, 10, true, true),
|
||||||
|
new DocumentCategory("nascholing", "Nascholingscertificaten",
|
||||||
|
"Upload uw nascholingscertificaten (optioneel).", false, PdfImage, 10, true, true),
|
||||||
|
},
|
||||||
|
_ => Array.Empty<DocumentCategory>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static DocumentCategory? Find(string wizardId, string categoryId) =>
|
||||||
|
CategoriesFor(wizardId).FirstOrDefault(c => c.CategoryId == categoryId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authoritative upload validation (the client check is UX-only). Returns a
|
||||||
|
/// rejection reason, or null when the file is acceptable.
|
||||||
|
/// </summary>
|
||||||
|
public static string? RejectUpload(DocumentCategory? category, string contentType, long sizeBytes)
|
||||||
|
{
|
||||||
|
if (category is null) return "Onbekende documentcategorie.";
|
||||||
|
if (!category.AcceptedTypes.Contains(contentType))
|
||||||
|
return $"Bestandstype niet toegestaan voor {category.Label}.";
|
||||||
|
if (sizeBytes > (long)category.MaxSizeMb * 1024 * 1024)
|
||||||
|
return $"Bestand is groter dan {category.MaxSizeMb} MB.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
|
|||||||
using BigRegister.Api.Contracts;
|
using BigRegister.Api.Contracts;
|
||||||
using BigRegister.Api.Data;
|
using BigRegister.Api.Data;
|
||||||
using BigRegister.Domain.Diplomas;
|
using BigRegister.Domain.Diplomas;
|
||||||
|
using BigRegister.Domain.Documents;
|
||||||
using BigRegister.Domain.Intake;
|
using BigRegister.Domain.Intake;
|
||||||
using BigRegister.Domain.Registrations;
|
using BigRegister.Domain.Registrations;
|
||||||
using BigRegister.Domain.Submissions;
|
using BigRegister.Domain.Submissions;
|
||||||
@@ -65,12 +66,12 @@ api.MapGet("/intake/policy", () => new IntakePolicyDto(IntakePolicy.ScholingThre
|
|||||||
// --- POST: submits. The server is the authority; it re-validates and decides. ---
|
// --- POST: submits. The server is the authority; it re-validates and decides. ---
|
||||||
|
|
||||||
api.MapPost("/registrations", (RegistratieRequest req, HttpContext ctx) =>
|
api.MapPost("/registrations", (RegistratieRequest req, HttpContext ctx) =>
|
||||||
Submit(ctx, "registratie", SubmissionRules.RejectRegistratie(req.DiplomaHerkomst)))
|
Submit(ctx, "registratie", SubmissionRules.RejectRegistratie(req.DiplomaHerkomst), req.Documents))
|
||||||
.Produces<ReferentieResponse>()
|
.Produces<ReferentieResponse>()
|
||||||
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
||||||
|
|
||||||
api.MapPost("/herregistraties", (HerregistratieRequest req, HttpContext ctx) =>
|
api.MapPost("/herregistraties", (HerregistratieRequest req, HttpContext ctx) =>
|
||||||
Submit(ctx, "herregistratie", SubmissionRules.RejectZeroUren(req.Uren)))
|
Submit(ctx, "herregistratie", SubmissionRules.RejectZeroUren(req.Uren), req.Documents))
|
||||||
.Produces<ReferentieResponse>()
|
.Produces<ReferentieResponse>()
|
||||||
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
||||||
|
|
||||||
@@ -84,12 +85,75 @@ api.MapPost("/change-requests", (ChangeRequestRequest req, HttpContext ctx) =>
|
|||||||
.Produces<ReferentieResponse>()
|
.Produces<ReferentieResponse>()
|
||||||
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
||||||
|
|
||||||
|
// --- Document upload ---
|
||||||
|
|
||||||
|
// Server-owned category config per wizard. The FE renders these; it never hardcodes.
|
||||||
|
api.MapGet("/uploads/categories", (string wizardId) =>
|
||||||
|
new UploadCategoriesDto(DocumentRules.CategoriesFor(wizardId).Select(c => c.ToDto()).ToList()));
|
||||||
|
|
||||||
|
// Multipart upload. Hand-written on the FE (XHR for progress), so it is excluded
|
||||||
|
// from the OpenAPI doc to keep the NSwag-generated client JSON-only. Validates type
|
||||||
|
// and size authoritatively; stores metadata only (no file bytes / PII held).
|
||||||
|
api.MapPost("/uploads", async (HttpRequest request) =>
|
||||||
|
{
|
||||||
|
if (!request.HasFormContentType) return Results.Problem(detail: "Verwacht multipart/form-data.", statusCode: 400);
|
||||||
|
var form = await request.ReadFormAsync();
|
||||||
|
var file = form.Files.GetFile("file");
|
||||||
|
string categoryId = form["categoryId"].ToString(), localId = form["localId"].ToString(), wizardId = form["wizardId"].ToString();
|
||||||
|
if (file is null || categoryId == "" || localId == "" || wizardId == "")
|
||||||
|
return Results.Problem(detail: "Onvolledige upload.", statusCode: 400);
|
||||||
|
|
||||||
|
var category = DocumentRules.Find(wizardId, categoryId);
|
||||||
|
var reject = DocumentRules.RejectUpload(category, file.ContentType, file.Length);
|
||||||
|
if (reject is not null) return Results.Problem(detail: reject, statusCode: 400);
|
||||||
|
|
||||||
|
var doc = DocumentStore.Add(localId, categoryId, wizardId, file.FileName, file.Length, DocumentStore.DemoOwner);
|
||||||
|
return Results.Created($"/api/v1/uploads/{doc.DocumentId}", new UploadResponse(doc.DocumentId, localId));
|
||||||
|
})
|
||||||
|
.ExcludeFromDescription();
|
||||||
|
|
||||||
|
// Poll-on-return: which of these client localIds have arrived at the BFF.
|
||||||
|
api.MapGet("/uploads/status", (string? localIds) =>
|
||||||
|
{
|
||||||
|
var ids = (localIds ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
var found = DocumentStore.ByLocalIds(ids).ToDictionary(d => d.LocalId);
|
||||||
|
var results = ids.Select(id => found.TryGetValue(id, out var d)
|
||||||
|
? new UploadStatusItemDto(id, "complete", d.DocumentId)
|
||||||
|
: new UploadStatusItemDto(id, "unknown", null)).ToList();
|
||||||
|
return new UploadStatusDto(results);
|
||||||
|
});
|
||||||
|
|
||||||
|
// User delete: owner-scoped; 409 once linked to a finalised submission.
|
||||||
|
api.MapDelete("/uploads/{documentId}", (string documentId) =>
|
||||||
|
DocumentStore.DeleteOwned(documentId, DocumentStore.DemoOwner) switch
|
||||||
|
{
|
||||||
|
DocumentStore.DeleteResult.Ok => Results.NoContent(),
|
||||||
|
DocumentStore.DeleteResult.Linked => Results.Problem(
|
||||||
|
detail: "Dit document is al gekoppeld aan een ingediende aanvraag en kan niet meer worden verwijderd.",
|
||||||
|
statusCode: StatusCodes.Status409Conflict),
|
||||||
|
_ => Results.NotFound(),
|
||||||
|
})
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.ProducesProblem(StatusCodes.Status409Conflict)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
// Admin delete (seam): a real system requires an admin role; here an X-Admin header
|
||||||
|
// stands in. Bypasses ownership, unlinks, and flags the submission for review.
|
||||||
|
api.MapDelete("/admin/uploads/{documentId}", (string documentId, HttpContext ctx) =>
|
||||||
|
!IsAdmin(ctx) ? Results.StatusCode(StatusCodes.Status403Forbidden)
|
||||||
|
: DocumentStore.AdminDelete(documentId, "admin") ? Results.NoContent() : Results.NotFound())
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status403Forbidden)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
static bool IsAdmin(HttpContext ctx) => ctx.Request.Headers["X-Admin"] == "true";
|
||||||
|
|
||||||
// Audit + outcome for a submit, with NO personal data: only kind, outcome,
|
// Audit + outcome for a submit, with NO personal data: only kind, outcome,
|
||||||
// generated reference and the caller's correlation id (the observability seam — a
|
// generated reference and the caller's correlation id (the observability seam — a
|
||||||
// real system ships this to structured logging / an audit store).
|
// real system ships this to structured logging / an audit store).
|
||||||
IResult Submit(HttpContext ctx, string kind, string? reject)
|
IResult Submit(HttpContext ctx, string kind, string? reject, IReadOnlyList<DocumentRefDto>? documents = null)
|
||||||
{
|
{
|
||||||
var cid = ctx.Request.Headers.TryGetValue("X-Correlation-Id", out var v) && !string.IsNullOrEmpty(v)
|
var cid = ctx.Request.Headers.TryGetValue("X-Correlation-Id", out var v) && !string.IsNullOrEmpty(v)
|
||||||
? v.ToString()
|
? v.ToString()
|
||||||
@@ -101,6 +165,15 @@ IResult Submit(HttpContext ctx, string kind, string? reject)
|
|||||||
return Results.Problem(detail: reject, statusCode: StatusCodes.Status422UnprocessableEntity);
|
return Results.Problem(detail: reject, statusCode: StatusCodes.Status422UnprocessableEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (documents is not null)
|
||||||
|
{
|
||||||
|
// Link digital documents (blocks later user delete) and record post-delivery
|
||||||
|
// intent so a caseworker knows to expect the physical document.
|
||||||
|
DocumentStore.Link(documents.Where(d => d.Channel == "digital" && d.DocumentId is not null).Select(d => d.DocumentId!));
|
||||||
|
foreach (var d in documents.Where(d => d.Channel == "post"))
|
||||||
|
DocumentStore.Audit("post-delivery", d.DocumentId ?? "-", d.CategoryId, cid);
|
||||||
|
}
|
||||||
|
|
||||||
var reference = SubmissionRules.NewReference();
|
var reference = SubmissionRules.NewReference();
|
||||||
app.Logger.LogInformation(
|
app.Logger.LogInformation(
|
||||||
"submit kind={Kind} outcome=accepted reference={Reference} correlationId={Cid} at={At:o}",
|
"submit kind={Kind} outcome=accepted reference={Reference} correlationId={Cid} at={At:o}",
|
||||||
|
|||||||
@@ -282,6 +282,126 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/uploads/categories": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "wizardId",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UploadCategoriesDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/uploads/status": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "localIds",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UploadStatusDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/uploads/{documentId}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "documentId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Conflict",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/admin/uploads/{documentId}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "documentId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@@ -367,6 +487,62 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"DocumentCategoryDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"categoryId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"acceptedTypes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"maxSizeMb": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"multiple": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"allowPostDelivery": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"DocumentRefDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"categoryId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"channel": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"DuoDiplomaDto": {
|
"DuoDiplomaDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -435,6 +611,13 @@
|
|||||||
"uren": {
|
"uren": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"documents": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/DocumentRefDto"
|
||||||
|
},
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -557,6 +740,13 @@
|
|||||||
"diplomaHerkomst": {
|
"diplomaHerkomst": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"documents": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/DocumentRefDto"
|
||||||
|
},
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -615,6 +805,50 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"UploadCategoriesDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"categories": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/DocumentCategoryDto"
|
||||||
|
},
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"UploadStatusDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"results": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UploadStatusItemDto"
|
||||||
|
},
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"UploadStatusItemDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"localId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using BigRegister.Api.Contracts;
|
using BigRegister.Api.Contracts;
|
||||||
|
using BigRegister.Api.Data;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
@@ -122,4 +124,89 @@ public class EndpointTests(WebApplicationFactory<Program> factory) : IClassFixtu
|
|||||||
var res = await _client.GetAsync("/health");
|
var res = await _client.GetAsync("/health");
|
||||||
res.EnsureSuccessStatusCode();
|
res.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Document upload ---
|
||||||
|
|
||||||
|
private static MultipartFormDataContent UploadForm(string localId, string categoryId, string wizardId, string fileName, string contentType)
|
||||||
|
{
|
||||||
|
var content = new MultipartFormDataContent();
|
||||||
|
var file = new ByteArrayContent(new byte[] { 1, 2, 3 });
|
||||||
|
file.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
||||||
|
content.Add(file, "file", fileName);
|
||||||
|
content.Add(new StringContent(categoryId), "categoryId");
|
||||||
|
content.Add(new StringContent(localId), "localId");
|
||||||
|
content.Add(new StringContent(wizardId), "wizardId");
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<UploadResponse> Upload(string localId, string categoryId = "diploma", string type = "application/pdf", string file = "d.pdf")
|
||||||
|
{
|
||||||
|
var res = await _client.PostAsync("/api/v1/uploads", UploadForm(localId, categoryId, "registratie", file, type));
|
||||||
|
Assert.Equal(HttpStatusCode.Created, res.StatusCode);
|
||||||
|
return (await res.Content.ReadFromJsonAsync<UploadResponse>())!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Categories_are_server_owned_config()
|
||||||
|
{
|
||||||
|
var dto = await _client.GetFromJsonAsync<UploadCategoriesDto>("/api/v1/uploads/categories?wizardId=registratie");
|
||||||
|
Assert.Contains(dto!.Categories, c => c.CategoryId == "diploma" && c.Required && !c.AllowPostDelivery);
|
||||||
|
Assert.Contains(dto.Categories, c => c.CategoryId == "identiteit" && c.AllowPostDelivery);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Upload_then_status_reports_complete_for_known_localId()
|
||||||
|
{
|
||||||
|
var localId = Guid.NewGuid().ToString();
|
||||||
|
var doc = await Upload(localId);
|
||||||
|
var status = await _client.GetFromJsonAsync<UploadStatusDto>($"/api/v1/uploads/status?localIds={localId},onbekend");
|
||||||
|
Assert.Contains(status!.Results, r => r.LocalId == localId && r.Status == "complete" && r.DocumentId == doc.DocumentId);
|
||||||
|
Assert.Contains(status.Results, r => r.LocalId == "onbekend" && r.Status == "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Upload_rejects_wrong_type()
|
||||||
|
{
|
||||||
|
var res = await _client.PostAsync("/api/v1/uploads",
|
||||||
|
UploadForm(Guid.NewGuid().ToString(), "diploma", "registratie", "d.txt", "text/plain"));
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task User_delete_succeeds_then_404()
|
||||||
|
{
|
||||||
|
var doc = await Upload(Guid.NewGuid().ToString());
|
||||||
|
Assert.Equal(HttpStatusCode.NoContent, (await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}")).StatusCode);
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}")).StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task User_delete_blocked_with_409_once_linked_to_submission()
|
||||||
|
{
|
||||||
|
var doc = await Upload(Guid.NewGuid().ToString());
|
||||||
|
var submit = await _client.PostAsJsonAsync("/api/v1/registrations",
|
||||||
|
new RegistratieRequest("duo", new[] { new DocumentRefDto("diploma", "digital", doc.DocumentId) }));
|
||||||
|
submit.EnsureSuccessStatusCode();
|
||||||
|
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}")).StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Admin_delete_requires_admin_role()
|
||||||
|
{
|
||||||
|
var doc = await Upload(Guid.NewGuid().ToString());
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, (await _client.DeleteAsync($"/api/v1/admin/uploads/{doc.DocumentId}")).StatusCode);
|
||||||
|
|
||||||
|
var req = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/admin/uploads/{doc.DocumentId}");
|
||||||
|
req.Headers.Add("X-Admin", "true");
|
||||||
|
Assert.Equal(HttpStatusCode.NoContent, (await _client.SendAsync(req)).StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Audit_log_records_upload_and_delete_metadata_only()
|
||||||
|
{
|
||||||
|
var doc = await Upload(Guid.NewGuid().ToString());
|
||||||
|
await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}");
|
||||||
|
Assert.Contains(DocumentStore.AuditLog, a => a.DocumentId == doc.DocumentId && a.Action == "upload");
|
||||||
|
Assert.Contains(DocumentStore.AuditLog, a => a.DocumentId == doc.DocumentId && a.Action == "delete-user");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,38 @@
|
|||||||
using BigRegister.Domain.Diplomas;
|
using BigRegister.Domain.Diplomas;
|
||||||
|
using BigRegister.Domain.Documents;
|
||||||
using BigRegister.Domain.Registrations;
|
using BigRegister.Domain.Registrations;
|
||||||
using BigRegister.Domain.Submissions;
|
using BigRegister.Domain.Submissions;
|
||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
|
public class DocumentRuleTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Rejects_unknown_category() =>
|
||||||
|
Assert.NotNull(DocumentRules.RejectUpload(null, "application/pdf", 1));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rejects_disallowed_type()
|
||||||
|
{
|
||||||
|
var c = DocumentRules.Find("registratie", "diploma");
|
||||||
|
Assert.NotNull(DocumentRules.RejectUpload(c, "text/plain", 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rejects_oversized_file()
|
||||||
|
{
|
||||||
|
var c = DocumentRules.Find("registratie", "diploma");
|
||||||
|
Assert.NotNull(DocumentRules.RejectUpload(c, "application/pdf", 11L * 1024 * 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Accepts_valid_file()
|
||||||
|
{
|
||||||
|
var c = DocumentRules.Find("registratie", "diploma");
|
||||||
|
Assert.Null(DocumentRules.RejectUpload(c, "application/pdf", 5L * 1024 * 1024));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class HerregistratieRuleTests
|
public class HerregistratieRuleTests
|
||||||
{
|
{
|
||||||
private static Registration Active(DateOnly deadline) => new(
|
private static Registration Active(DateOnly deadline) => new(
|
||||||
|
|||||||
@@ -446,6 +446,177 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
return Promise.resolve<ReferentieResponse>(null as any);
|
return Promise.resolve<ReferentieResponse>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return OK
|
||||||
|
*/
|
||||||
|
categories(wizardId: string): Promise<UploadCategoriesDto> {
|
||||||
|
let url_ = this.baseUrl + "/api/v1/uploads/categories?";
|
||||||
|
if (wizardId === undefined || wizardId === null)
|
||||||
|
throw new globalThis.Error("The parameter 'wizardId' must be defined and cannot be null.");
|
||||||
|
else
|
||||||
|
url_ += "wizardId=" + encodeURIComponent("" + wizardId) + "&";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||||
|
return this.processCategories(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processCategories(response: Response): Promise<UploadCategoriesDto> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as UploadCategoriesDto;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<UploadCategoriesDto>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param localIds (optional)
|
||||||
|
* @return OK
|
||||||
|
*/
|
||||||
|
status(localIds?: string | undefined): Promise<UploadStatusDto> {
|
||||||
|
let url_ = this.baseUrl + "/api/v1/uploads/status?";
|
||||||
|
if (localIds === null)
|
||||||
|
throw new globalThis.Error("The parameter 'localIds' cannot be null.");
|
||||||
|
else if (localIds !== undefined)
|
||||||
|
url_ += "localIds=" + encodeURIComponent("" + localIds) + "&";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||||
|
return this.processStatus(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processStatus(response: Response): Promise<UploadStatusDto> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as UploadStatusDto;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<UploadStatusDto>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return No Content
|
||||||
|
*/
|
||||||
|
uploads(documentId: string): Promise<void> {
|
||||||
|
let url_ = this.baseUrl + "/api/v1/uploads/{documentId}";
|
||||||
|
if (documentId === undefined || documentId === null)
|
||||||
|
throw new globalThis.Error("The parameter 'documentId' must be defined.");
|
||||||
|
url_ = url_.replace("{documentId}", encodeURIComponent("" + documentId));
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||||
|
return this.processUploads(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processUploads(response: Response): Promise<void> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
} else if (status === 404) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("Not Found", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
} else if (status === 409) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result409: any = null;
|
||||||
|
result409 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
|
||||||
|
return throwException("Conflict", status, _responseText, _headers, result409);
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<void>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return No Content
|
||||||
|
*/
|
||||||
|
uploads2(documentId: string): Promise<void> {
|
||||||
|
let url_ = this.baseUrl + "/api/v1/admin/uploads/{documentId}";
|
||||||
|
if (documentId === undefined || documentId === null)
|
||||||
|
throw new globalThis.Error("The parameter 'documentId' must be defined.");
|
||||||
|
url_ = url_.replace("{documentId}", encodeURIComponent("" + documentId));
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||||
|
return this.processUploads2(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processUploads2(response: Response): Promise<void> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
} else if (status === 403) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("Forbidden", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
} else if (status === 404) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("Not Found", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<void>(null as any);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AantekeningDto {
|
export interface AantekeningDto {
|
||||||
@@ -477,6 +648,23 @@ export interface DashboardViewDto {
|
|||||||
decisions?: HerregistratieDecisionsDto;
|
decisions?: HerregistratieDecisionsDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DocumentCategoryDto {
|
||||||
|
categoryId?: string | undefined;
|
||||||
|
label?: string | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
|
required?: boolean;
|
||||||
|
acceptedTypes?: string[] | undefined;
|
||||||
|
maxSizeMb?: number;
|
||||||
|
multiple?: boolean;
|
||||||
|
allowPostDelivery?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentRefDto {
|
||||||
|
categoryId?: string | undefined;
|
||||||
|
channel?: string | undefined;
|
||||||
|
documentId?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DuoDiplomaDto {
|
export interface DuoDiplomaDto {
|
||||||
id?: string | undefined;
|
id?: string | undefined;
|
||||||
naam?: string | undefined;
|
naam?: string | undefined;
|
||||||
@@ -498,6 +686,7 @@ export interface HerregistratieDecisionsDto {
|
|||||||
|
|
||||||
export interface HerregistratieRequest {
|
export interface HerregistratieRequest {
|
||||||
uren?: number;
|
uren?: number;
|
||||||
|
documents?: DocumentRefDto[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IntakePolicyDto {
|
export interface IntakePolicyDto {
|
||||||
@@ -541,6 +730,7 @@ export interface ReferentieResponse {
|
|||||||
|
|
||||||
export interface RegistratieRequest {
|
export interface RegistratieRequest {
|
||||||
diplomaHerkomst?: string | undefined;
|
diplomaHerkomst?: string | undefined;
|
||||||
|
documents?: DocumentRefDto[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegistrationDto {
|
export interface RegistrationDto {
|
||||||
@@ -560,6 +750,20 @@ export interface RegistrationStatusDto {
|
|||||||
doorgehaaldOp?: string | undefined;
|
doorgehaaldOp?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UploadCategoriesDto {
|
||||||
|
categories?: DocumentCategoryDto[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadStatusDto {
|
||||||
|
results?: UploadStatusItemDto[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadStatusItemDto {
|
||||||
|
localId?: string | undefined;
|
||||||
|
status?: string | undefined;
|
||||||
|
documentId?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export class SwaggerException extends Error {
|
export class SwaggerException extends Error {
|
||||||
override message: string;
|
override message: string;
|
||||||
status: number;
|
status: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user