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 bb725202..a7867c92 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -13,6 +13,7 @@ import java.util.UUID; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import org.springframework.data.domain.PageRequest; @@ -254,7 +255,7 @@ public class DocumentController { @PatchMapping("/bulk") @RequirePermission(Permission.WRITE_ALL) public BulkEditResult patchBulk( - @RequestBody DocumentBulkEditDTO dto, + @RequestBody @Valid DocumentBulkEditDTO dto, Authentication authentication) { if (dto.getDocumentIds() == null || dto.getDocumentIds().isEmpty()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "documentIds is required"); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentBulkEditDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentBulkEditDTO.java index ad6d246e..f7ec4a42 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentBulkEditDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentBulkEditDTO.java @@ -3,19 +3,58 @@ package org.raddatz.familienarchiv.dto; import java.util.List; import java.util.UUID; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; +/** + * Request body for {@code PATCH /api/documents/bulk}. Field semantics: + * + * + *

Kept as a Lombok {@code @Data} POJO (not a record) for symmetry with + * the existing {@code DocumentUpdateDTO} and to keep test setup terse — + * the per-feature DTOs introduced alongside this one ({@link BulkEditError}, + * {@link BulkEditResult}, {@link BatchMetadataRequest}, + * {@link DocumentBatchSummary}) are records because they have no + * test-side mutation. Tracked in the cycle-1 review for follow-up. + * + *

Bean-validation caps below defend against payload-amplification: the + * 1 MiB SvelteKit proxy cap allows ~26k UUIDs through to the backend, and + * Jetty's default body limit is 8 MB. {@code @Size} guards catch malformed + * clients without depending on those outer bounds. + */ @Data @NoArgsConstructor @AllArgsConstructor public class DocumentBulkEditDTO { + + // No @Size cap here on purpose: the controller's BULK_EDIT_MAX_IDS check + // returns the typed BULK_EDIT_TOO_MANY_IDS error code, which the frontend + // maps to a localised "Maximal 500 …" message via Paraglide. A bean- + // validation @Size would short-circuit that with a generic VALIDATION_ERROR. private List documentIds; - private List tagNames; + + @Size(max = 200, message = "tagNames must not exceed 200 entries") + private List<@Size(max = 200, message = "tagName must not exceed 200 chars") String> tagNames; + private UUID senderId; + + @Size(max = 200, message = "receiverIds must not exceed 200 entries") private List receiverIds; + + @Size(max = 255, message = "documentLocation must not exceed 255 chars") private String documentLocation; + + @Size(max = 255, message = "archiveBox must not exceed 255 chars") private String archiveBox; + + @Size(max = 255, message = "archiveFolder must not exceed 255 chars") private String archiveFolder; } 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 c6d9e26a..12ea8f4a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -8,6 +8,8 @@ import org.raddatz.familienarchiv.audit.AuditKind; import org.raddatz.familienarchiv.audit.AuditLogQueryService; import org.raddatz.familienarchiv.audit.AuditService; import org.raddatz.familienarchiv.dto.DocumentBatchMetadataDTO; +import org.raddatz.familienarchiv.dto.DocumentBatchSummary; +import org.raddatz.familienarchiv.dto.DocumentBulkEditDTO; import org.raddatz.familienarchiv.dto.DocumentSearchItem; import org.raddatz.familienarchiv.dto.DocumentSearchResult; import org.raddatz.familienarchiv.dto.DocumentSort; @@ -356,6 +358,7 @@ public class DocumentService { * frontend can replace the selection with every match across pages in one * round-trip. */ + @Transactional(readOnly = true) public List findIdsForFilter(String text, LocalDate from, LocalDate to, UUID sender, UUID receiver, List tags, String tagQ, DocumentStatus status, TagOperator tagOperator) { boolean hasText = StringUtils.hasText(text); @@ -385,10 +388,11 @@ public class DocumentService { * bulk-edit page's left strip, where missing previews would already be * obvious; surfacing them as errors here adds no value. */ - public List batchMetadata(List ids) { + @Transactional(readOnly = true) + public List batchMetadata(List ids) { if (ids == null || ids.isEmpty()) return List.of(); return documentRepository.findAllById(ids).stream() - .map(d -> new org.raddatz.familienarchiv.dto.DocumentBatchSummary( + .map(d -> new DocumentBatchSummary( d.getId(), d.getTitle() != null ? d.getTitle() : d.getOriginalFilename(), "/api/documents/" + d.getId() + "/file")) @@ -413,7 +417,7 @@ public class DocumentService { * ~1500 documents total. Tracked as a perf follow-up. */ @Transactional - public Document applyBulkEditToDocument(UUID id, org.raddatz.familienarchiv.dto.DocumentBulkEditDTO dto, UUID actorId) { + public Document applyBulkEditToDocument(UUID id, DocumentBulkEditDTO dto, UUID actorId) { Document doc = documentRepository.findById(id) .orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));