Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<AttachmentResponse> 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<List<AttachmentResponse>> listAttachments(
@AuthenticationPrincipal User currentUser,
@PathVariable UUID recordId) {

List<AttachmentResponse> responses = attachmentService.getAttachmentsByRecord(currentUser, recordId);
return ResponseEntity.ok(responses);
}


@GetMapping("/{id}/download")
public ResponseEntity<AttachmentResponse> downloadAttachment(
@AuthenticationPrincipal User currentUser,
@PathVariable UUID id) {

AttachmentResponse response = attachmentService.getAttachment(currentUser, id);
return ResponseEntity.ok(response);
}


@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteAttachment(
@AuthenticationPrincipal User currentUser,
@PathVariable UUID id) {

attachmentService.deleteAttachment(currentUser, id);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -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.*;

Expand Down Expand Up @@ -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<ErrorResponse> handleConstraintViolation(ConstraintViolationException ex) {
Map<String, List<String>> 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<String, List<String>>
);

return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@Repository
public interface AttachmentRepository extends JpaRepository<Attachment, UUID> {

List<Attachment> findByMedicalRecordId(UUID recordId);
List<Attachment> findByMedicalRecordId(UUID medicalRecordId);

List<Attachment> findByUploadedById(UUID userId);

Expand Down
43 changes: 31 additions & 12 deletions src/main/java/org/example/vet1177/services/AttachmentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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()));

Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -163,6 +169,19 @@ public void afterCommit() {
log.info("Attachment {} marked for deletion in database", attachmentId);
}

@Transactional(readOnly = true)
public List<AttachmentResponse> 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());

Expand Down