diff --git a/backend/src/BigRegister.Api/Data/DocumentStore.cs b/backend/src/BigRegister.Api/Data/DocumentStore.cs
index 876afe6..f452b88 100644
--- a/backend/src/BigRegister.Api/Data/DocumentStore.cs
+++ b/backend/src/BigRegister.Api/Data/DocumentStore.cs
@@ -1,13 +1,14 @@
namespace BigRegister.Api.Data;
///
-/// 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.
+/// Stored document: metadata + bytes. The demo holds bytes IN-MEMORY (reset on
+/// restart) purely so re-opened wizards can preview/download what was uploaded — a
+/// real backend persists them to blob storage keyed by DocumentId. Bytes are never
+/// serialized into a JSON response; only the dedicated content endpoint streams them.
///
public sealed record StoredDocument(
string DocumentId, string LocalId, string CategoryId, string WizardId,
- string FileName, long SizeBytes, string Owner, DateTimeOffset UploadedAt)
+ string FileName, long SizeBytes, string ContentType, byte[] Content, string Owner, DateTimeOffset UploadedAt)
{
public bool Linked { get; set; }
}
@@ -28,9 +29,9 @@ public static class DocumentStore
private static readonly List _audit = new();
private static readonly object _gate = new();
- public static StoredDocument Add(string localId, string categoryId, string wizardId, string fileName, long sizeBytes, string owner)
+ public static StoredDocument Add(string localId, string categoryId, string wizardId, string fileName, string contentType, byte[] content, string owner)
{
- var doc = new StoredDocument(Guid.NewGuid().ToString(), localId, categoryId, wizardId, fileName, sizeBytes, owner, DateTimeOffset.UtcNow);
+ var doc = new StoredDocument(Guid.NewGuid().ToString(), localId, categoryId, wizardId, fileName, content.LongLength, contentType, content, owner, DateTimeOffset.UtcNow);
lock (_gate) _docs[doc.DocumentId] = doc;
Audit("upload", doc.DocumentId, categoryId, owner);
return doc;
diff --git a/backend/src/BigRegister.Api/Program.cs b/backend/src/BigRegister.Api/Program.cs
index 8807d49..ff4b273 100644
--- a/backend/src/BigRegister.Api/Program.cs
+++ b/backend/src/BigRegister.Api/Program.cs
@@ -107,11 +107,25 @@ api.MapPost("/uploads", async (HttpRequest request) =>
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);
+ using var ms = new MemoryStream();
+ await file.CopyToAsync(ms);
+ var doc = DocumentStore.Add(localId, categoryId, wizardId, file.FileName, file.ContentType, ms.ToArray(), DocumentStore.DemoOwner);
return Results.Created($"/api/v1/uploads/{doc.DocumentId}", new UploadResponse(doc.DocumentId, localId));
})
.ExcludeFromDescription();
+// Serve stored bytes so a re-opened wizard can preview/download an upload. Inline
+// for pdf/image (browser renders it), attachment otherwise (download).
+api.MapGet("/uploads/{documentId}/content", (string documentId) =>
+{
+ var doc = DocumentStore.Get(documentId);
+ if (doc is null) return Results.NotFound();
+ var inline = doc.ContentType == "application/pdf" || doc.ContentType.StartsWith("image/");
+ return Results.File(doc.Content, doc.ContentType, fileDownloadName: inline ? null : doc.FileName);
+})
+.Produces(StatusCodes.Status200OK)
+.Produces(StatusCodes.Status404NotFound);
+
// Poll-on-return: which of these client localIds have arrived at the BFF.
api.MapGet("/uploads/status", (string? localIds) =>
{
diff --git a/backend/tests/BigRegister.Tests/EndpointTests.cs b/backend/tests/BigRegister.Tests/EndpointTests.cs
index ed37877..68a265c 100644
--- a/backend/tests/BigRegister.Tests/EndpointTests.cs
+++ b/backend/tests/BigRegister.Tests/EndpointTests.cs
@@ -164,6 +164,24 @@ public class EndpointTests(WebApplicationFactory factory) : IClassFixtu
Assert.Contains(status.Results, r => r.LocalId == "onbekend" && r.Status == "unknown");
}
+ [Fact]
+ public async Task Upload_content_is_served_back_with_its_type_inline_for_pdf()
+ {
+ var doc = await Upload(Guid.NewGuid().ToString());
+ var res = await _client.GetAsync($"/api/v1/uploads/{doc.DocumentId}/content");
+ res.EnsureSuccessStatusCode();
+ Assert.Equal("application/pdf", res.Content.Headers.ContentType!.MediaType);
+ Assert.Equal(new byte[] { 1, 2, 3 }, await res.Content.ReadAsByteArrayAsync());
+ // pdf/image → inline (no attachment disposition) so the browser previews it
+ Assert.NotEqual("attachment", res.Content.Headers.ContentDisposition?.DispositionType);
+ }
+
+ [Fact]
+ public async Task Upload_content_404_for_unknown_document()
+ {
+ Assert.Equal(HttpStatusCode.NotFound, (await _client.GetAsync("/api/v1/uploads/demo-nope/content")).StatusCode);
+ }
+
[Fact]
public async Task Upload_rejects_wrong_type()
{