From 96f8bfd822ca8e8ec098e3e64cac590c3752aec6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 18 Apr 2026 13:46:25 +0200 Subject: [PATCH] feat(backend): add POST /api/documents/{id}/file endpoint to attach file to existing document Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 14 +++++++++ .../service/DocumentService.java | 17 +++++++++++ .../controller/DocumentControllerTest.java | 30 +++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index 6dfb81fe..cc8f6707 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -129,6 +129,20 @@ public class DocumentController { return ResponseEntity.noContent().build(); } + // --- ATTACH FILE --- + + @PostMapping(value = "/{id}/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @RequirePermission(Permission.WRITE_ALL) + public Document attachFile( + @PathVariable UUID id, + @RequestPart("file") MultipartFile file) { + try { + return documentService.attachFile(id, file); + } catch (IOException e) { + throw DomainException.internal(ErrorCode.FILE_UPLOAD_FAILED, "Failed to upload file: " + e.getMessage()); + } + } + // --- QUICK UPLOAD --- private static final Set ALLOWED_CONTENT_TYPES = Set.of( diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java index 2f074e48..fecf7117 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -286,6 +286,23 @@ public class DocumentService { return documentRepository.save(doc); } + @Transactional + public Document attachFile(UUID id, MultipartFile file) throws IOException { + Document doc = documentRepository.findById(id) + .orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id)); + FileService.UploadResult upload = fileService.uploadFile(file, file.getOriginalFilename()); + doc.setFilePath(upload.s3Key()); + doc.setFileHash(upload.fileHash()); + doc.setOriginalFilename(file.getOriginalFilename()); + doc.setContentType(file.getContentType()); + if (doc.getStatus() == DocumentStatus.PLACEHOLDER) { + doc.setStatus(DocumentStatus.UPLOADED); + } + Document saved = documentRepository.save(doc); + documentVersionService.recordVersion(saved); + return saved; + } + // 0. Zuletzt aktive Dokumente (sortiert nach updatedAt DESC) public List getRecentActivity(int size) { return documentRepository.findAll( diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java index dd73aa91..4153556e 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java @@ -563,6 +563,36 @@ class DocumentControllerTest { .andExpect(status().isBadRequest()); } + // ─── POST /api/documents/{id}/file ─────────────────────────────────────── + + @Test + @WithMockUser + void attachFile_returns403_whenMissingWritePermission() throws Exception { + org.springframework.mock.web.MockMultipartFile file = + new org.springframework.mock.web.MockMultipartFile("file", "brief.pdf", "application/pdf", new byte[]{1}); + + mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID() + "/file").file(file)) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void attachFile_returns200_withUpdatedDocument_whenHasWritePermission() throws Exception { + UUID id = UUID.randomUUID(); + Document doc = Document.builder() + .id(id).title("Brief").originalFilename("brief.pdf") + .filePath("docs/brief.pdf").status(DocumentStatus.UPLOADED).build(); + when(documentService.attachFile(eq(id), any())).thenReturn(doc); + + org.springframework.mock.web.MockMultipartFile file = + new org.springframework.mock.web.MockMultipartFile("file", "brief.pdf", "application/pdf", new byte[]{1}); + + mockMvc.perform(multipart("/api/documents/" + id + "/file").file(file)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(id.toString())) + .andExpect(jsonPath("$.status").value("UPLOADED")); + } + // ─── GET /api/documents/{id}/versions/{versionId} ──────────────────────── @Test