diff --git a/src/main/java/org/example/vet1177/controller/AttachmentController.java b/src/main/java/org/example/vet1177/controller/AttachmentController.java new file mode 100644 index 0000000..75aa0b0 --- /dev/null +++ b/src/main/java/org/example/vet1177/controller/AttachmentController.java @@ -0,0 +1,82 @@ +package org.example.vet1177.controller; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; +import org.example.vet1177.dto.request.attachment.AttachmentRequest; +import org.example.vet1177.dto.response.attachment.AttachmentResponse; +import org.example.vet1177.entities.User; +import org.example.vet1177.services.AttachmentService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/attachments") +public class AttachmentController { + + private final AttachmentService attachmentService; + private final Validator validator; + + public AttachmentController(AttachmentService attachmentService, Validator validator) { + + this.attachmentService = attachmentService; + this.validator = validator; + + } + + + @PostMapping(value = "/record/{recordId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity uploadAttachment( + @AuthenticationPrincipal User currentUser, + @PathVariable UUID recordId, + @RequestPart("file") MultipartFile file, + @RequestPart(value = "description", required = false) String description) { + + + AttachmentRequest request = new AttachmentRequest(recordId, description); + + var violations = validator.validate(request); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + + AttachmentResponse response = attachmentService.uploadAttachment(currentUser, file, request); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + + @GetMapping("/record/{recordId}") + public ResponseEntity> listAttachments( + @AuthenticationPrincipal User currentUser, + @PathVariable UUID recordId) { + + List responses = attachmentService.getAttachmentsByRecord(currentUser, recordId); + return ResponseEntity.ok(responses); + } + + + @GetMapping("/{id}/download") + public ResponseEntity downloadAttachment( + @AuthenticationPrincipal User currentUser, + @PathVariable UUID id) { + + AttachmentResponse response = attachmentService.getAttachment(currentUser, id); + return ResponseEntity.ok(response); + } + + + @DeleteMapping("/{id}") + public ResponseEntity deleteAttachment( + @AuthenticationPrincipal User currentUser, + @PathVariable UUID id) { + + attachmentService.deleteAttachment(currentUser, id); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/vet1177/exception/GlobalExceptionHandler.java b/src/main/java/org/example/vet1177/exception/GlobalExceptionHandler.java index 58b7064..cf62f5c 100644 --- a/src/main/java/org/example/vet1177/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/vet1177/exception/GlobalExceptionHandler.java @@ -1,6 +1,8 @@ package org.example.vet1177.exception; +import jakarta.validation.ConstraintViolationException; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; @@ -77,4 +79,26 @@ public ErrorResponse handleGeneric(Exception ex) { return new ErrorResponse(500, "Something went wrong", null); } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleConstraintViolation(ConstraintViolationException ex) { + Map> validationErrors = new HashMap<>(); + + ex.getConstraintViolations().forEach(violation -> { + String fieldName = violation.getPropertyPath().toString(); + String errorMessage = violation.getMessage(); + + validationErrors.computeIfAbsent(fieldName, k -> new ArrayList<>()) + .add(errorMessage); + }); + + ErrorResponse error = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Valideringsfel", + validationErrors // <--- Nu matchar vi Map> + ); + + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } } \ No newline at end of file diff --git a/src/main/java/org/example/vet1177/repository/AttachmentRepository.java b/src/main/java/org/example/vet1177/repository/AttachmentRepository.java index 7c5e46f..9a85a72 100644 --- a/src/main/java/org/example/vet1177/repository/AttachmentRepository.java +++ b/src/main/java/org/example/vet1177/repository/AttachmentRepository.java @@ -11,7 +11,7 @@ @Repository public interface AttachmentRepository extends JpaRepository { - List findByMedicalRecordId(UUID recordId); + List findByMedicalRecordId(UUID medicalRecordId); List findByUploadedById(UUID userId); diff --git a/src/main/java/org/example/vet1177/services/AttachmentService.java b/src/main/java/org/example/vet1177/services/AttachmentService.java index 1494f06..9724bde 100644 --- a/src/main/java/org/example/vet1177/services/AttachmentService.java +++ b/src/main/java/org/example/vet1177/services/AttachmentService.java @@ -8,6 +8,7 @@ import org.example.vet1177.entities.User; import org.example.vet1177.exception.ResourceNotFoundException; import org.example.vet1177.policy.AttachmentPolicy; +import org.example.vet1177.policy.MedicalRecordPolicy; import org.example.vet1177.repository.AttachmentRepository; import org.example.vet1177.repository.MedicalRecordRepository; import org.slf4j.Logger; @@ -17,8 +18,9 @@ import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; - +import org.springframework.security.access.AccessDeniedException; import java.io.IOException; +import java.util.List; import java.util.UUID; @Service @@ -31,22 +33,25 @@ public class AttachmentService { private final MedicalRecordRepository medicalRecordRepository; private final AttachmentPolicy attachmentPolicy; private final String bucketName; + private final MedicalRecordPolicy medicalRecordPolicy; public AttachmentService(AttachmentRepository attachmentRepository, FileStorageService fileStorageService, MedicalRecordRepository medicalRecordRepository, AttachmentPolicy attachmentPolicy, - AwsS3Properties props) { + AwsS3Properties props, + MedicalRecordPolicy medicalRecordPolicy) { this.attachmentRepository = attachmentRepository; this.fileStorageService = fileStorageService; this.medicalRecordRepository = medicalRecordRepository; this.attachmentPolicy = attachmentPolicy; this.bucketName = props.getBucketName(); + this.medicalRecordPolicy = medicalRecordPolicy; } @Transactional(rollbackFor = Exception.class) - public AttachmentResponse uploadAttachment(User currentUser, MultipartFile file, AttachmentRequest request) throws IOException { + public AttachmentResponse uploadAttachment(User currentUser, MultipartFile file, AttachmentRequest request) { MedicalRecord record = medicalRecordRepository.findById(request.recordId()) .orElseThrow(() -> new ResourceNotFoundException("MedicalRecord", request.recordId())); @@ -64,14 +69,17 @@ public AttachmentResponse uploadAttachment(User currentUser, MultipartFile file, String s3Key = String.format("records/%s/%s_%s", record.getId(), UUID.randomUUID(), - file.getOriginalFilename()); + sanitizedName); // Anropa FileStorageService try { fileStorageService.upload(s3Key, file.getInputStream(), file.getSize(), file.getContentType()); + } catch (IOException e) { + log.error("Kritisk IO-fel vid läsning av MultipartFile: {}", originalName, e); + throw new RuntimeException("Kunde inte läsa den uppladdade filen. Försök igen.", e); } catch (Exception e) { - log.error("S3 upload failed for key: {}", s3Key); - throw new RuntimeException("Kunde inte ladda upp filen till lagringen", e); + log.error("S3 upload failed for key: {}", s3Key, e); + throw new RuntimeException("Kunde inte ladda upp filen till molnlagringen", e); } // Skapa entitet @@ -93,17 +101,15 @@ public AttachmentResponse uploadAttachment(User currentUser, MultipartFile file, return mapToResponse(attachment); } catch (Exception e) { - // Tack vare saveAndFlush hamnar vi här om databasen nekar sparningen - log.error("Database persistence failed for attachment with S3 key: {}. Triggering S3 cleanup.", s3Key); + log.error("Database persistence failed for S3 key: {}. Triggering cleanup.", s3Key); try { fileStorageService.delete(s3Key); } catch (Exception deleteEx) { - log.error("CRITICAL: Failed to cleanup S3 object {} after DB failure!", s3Key, deleteEx); - } - - throw new RuntimeException("Kunde inte spara bilagans metadata. Uppladdningen avbröts.", e); + log.error("CRITICAL: Failed to cleanup S3 object {}!", s3Key, deleteEx); } + throw new RuntimeException("Kunde inte spara metadata i databasen. Uppladdningen avbröts.", e); + } } private String sanitizeFilename(String originalFilename) { @@ -163,6 +169,19 @@ public void afterCommit() { log.info("Attachment {} marked for deletion in database", attachmentId); } + @Transactional(readOnly = true) + public List getAttachmentsByRecord(User currentUser, UUID recordId) { + MedicalRecord record = medicalRecordRepository.findById(recordId) + .orElseThrow(() -> new ResourceNotFoundException("MedicalRecord", recordId)); + + medicalRecordPolicy.canView(currentUser, record); + + + return attachmentRepository.findByMedicalRecordId(recordId).stream() + .map(this::mapToResponse) + .toList(); + } + private AttachmentResponse mapToResponse(Attachment attachment) { String downloadUrl = fileStorageService.generatePresignedUrl(attachment.getS3Key());