From de54dddb6d1ffb14fe9056e18a697d37a1d1e6d4 Mon Sep 17 00:00:00 2001 From: gurkvatten Date: Tue, 14 Apr 2026 13:15:54 +0200 Subject: [PATCH 1/7] added back pdf generation and upload --- .../controllers/HomeController.java | 12 +++++++++--- src/main/resources/templates/private.html | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/example/crimearchive/controllers/HomeController.java b/src/main/java/org/example/crimearchive/controllers/HomeController.java index 414e492..3d47dcf 100644 --- a/src/main/java/org/example/crimearchive/controllers/HomeController.java +++ b/src/main/java/org/example/crimearchive/controllers/HomeController.java @@ -14,7 +14,9 @@ import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.UUID; @Controller @@ -68,13 +70,17 @@ public String casesPage(@RequestParam Long addAccountId, public String saveReport( @ModelAttribute("newReport") @Valid CreateReport newReport, BindingResult bindingResult, - @AuthenticationPrincipal Account currentUser) { + @RequestParam("file") MultipartFile file, // Viktigt: Ta emot filen här! + @AuthenticationPrincipal Account currentUser) throws IOException { if (bindingResult.hasErrors()) { - log.info("Binding Error: {}", bindingResult.getAllErrors().getLast()); + log.info("Binding Error: {}", bindingResult.getAllErrors()); return "private"; } - reportService.saveReport(newReport, currentUser); + + + reportService.saveReportWithFile(newReport, file); + return "redirect:/userpage"; } diff --git a/src/main/resources/templates/private.html b/src/main/resources/templates/private.html index 9420b48..2795c5e 100644 --- a/src/main/resources/templates/private.html +++ b/src/main/resources/templates/private.html @@ -6,14 +6,22 @@

Här kan bara användare med rollen Admin lägga till raporter

-
+ -
+ +
+ -
+ +
+ -
+ +
+ + +
+
From 4f31d130be82b9a591ad105ff31fe58f7b9ec865 Mon Sep 17 00:00:00 2001 From: gurkvatten Date: Mon, 20 Apr 2026 09:21:22 +0200 Subject: [PATCH 2/7] version control --- .../controllers/HomeController.java | 41 ++-- .../crimearchive/mapper/ReportMapper.java | 8 +- .../example/crimearchive/reports/Report.java | 31 +-- .../crimearchive/reports/ReportService.java | 202 ++---------------- src/main/resources/templates/cases.html | 13 +- .../service/ReportServiceTest.java | 180 +++------------- 6 files changed, 80 insertions(+), 395 deletions(-) diff --git a/src/main/java/org/example/crimearchive/controllers/HomeController.java b/src/main/java/org/example/crimearchive/controllers/HomeController.java index 3d47dcf..1d9d9f6 100644 --- a/src/main/java/org/example/crimearchive/controllers/HomeController.java +++ b/src/main/java/org/example/crimearchive/controllers/HomeController.java @@ -2,13 +2,15 @@ import jakarta.validation.Valid; import org.example.crimearchive.DTO.CreateReport; +import org.example.crimearchive.cases.Cases; import org.example.crimearchive.cases.CaseService; import org.example.crimearchive.cases.CasesRepository; +import org.example.crimearchive.evidence.EvidenceFile; +import org.example.crimearchive.evidence.EvidenceFileService; import org.example.crimearchive.polis.Account; import org.example.crimearchive.reports.ReportService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -17,7 +19,9 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.util.UUID; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Controller public class HomeController { @@ -26,11 +30,14 @@ public class HomeController { private final ReportService reportService; private final CaseService caseService; private final CasesRepository casesRepository; + private final EvidenceFileService evidenceFileService; - public HomeController(ReportService reportService, CaseService caseService, CasesRepository casesRepository) { + public HomeController(ReportService reportService, CaseService caseService, + CasesRepository casesRepository, EvidenceFileService evidenceFileService) { this.reportService = reportService; this.caseService = caseService; this.casesRepository = casesRepository; + this.evidenceFileService = evidenceFileService; } @GetMapping("/") @@ -53,15 +60,22 @@ public String userPage(@AuthenticationPrincipal Account user, Model model) { @GetMapping("/cases") public String casesPage(@RequestParam(required = false) Long accountId, Model model) { if (accountId != null) { - model.addAttribute("cases", casesRepository.findByAccountsId(accountId)); + List cases = casesRepository.findByAccountsId(accountId); + model.addAttribute("cases", cases); model.addAttribute("accountId", accountId); + + Map> evidenceMap = new HashMap<>(); + for (Cases c : cases) { + evidenceMap.put(c.getCaseNumber(), evidenceFileService.getByCaseNumber(c.getCaseNumber())); + } + model.addAttribute("evidenceMap", evidenceMap); } return "cases"; } @PostMapping("/cases/add") public String casesPage(@RequestParam Long addAccountId, - @RequestParam String case_number){ + @RequestParam String case_number) { caseService.addAccountToCase(addAccountId, case_number); return "redirect:/cases"; } @@ -70,7 +84,7 @@ public String casesPage(@RequestParam Long addAccountId, public String saveReport( @ModelAttribute("newReport") @Valid CreateReport newReport, BindingResult bindingResult, - @RequestParam("file") MultipartFile file, // Viktigt: Ta emot filen här! + @RequestParam("file") MultipartFile file, @AuthenticationPrincipal Account currentUser) throws IOException { if (bindingResult.hasErrors()) { @@ -78,19 +92,12 @@ public String saveReport( return "private"; } + String caseNumber = reportService.saveReport(newReport, currentUser); - reportService.saveReportWithFile(newReport, file); + if (file != null && !file.isEmpty()) { + evidenceFileService.upload(caseNumber, newReport.name(), newReport.event(), file, currentUser.getUsername()); + } return "redirect:/userpage"; } - - @GetMapping("/reports/{uuid}/download/pdf") - public ResponseEntity downloadPdf(@PathVariable UUID uuid) { - return reportService.downloadPdf(uuid); - } - - @GetMapping("/reports/{uuid}/download/file") - public ResponseEntity downloadFile(@PathVariable UUID uuid) { - return reportService.downloadFile(uuid); - } } diff --git a/src/main/java/org/example/crimearchive/mapper/ReportMapper.java b/src/main/java/org/example/crimearchive/mapper/ReportMapper.java index efb531f..945402f 100644 --- a/src/main/java/org/example/crimearchive/mapper/ReportMapper.java +++ b/src/main/java/org/example/crimearchive/mapper/ReportMapper.java @@ -7,15 +7,11 @@ public class ReportMapper { - public static Report toEntity(CreateReport report, String s3KeyPdf, String s3KeyFile) { + public static Report toEntity(CreateReport report) { return new Report( UUID.randomUUID(), report.name(), - report.event(), - s3KeyPdf, - s3KeyFile - + report.event() ); } - } diff --git a/src/main/java/org/example/crimearchive/reports/Report.java b/src/main/java/org/example/crimearchive/reports/Report.java index 67b2906..d04fd6a 100644 --- a/src/main/java/org/example/crimearchive/reports/Report.java +++ b/src/main/java/org/example/crimearchive/reports/Report.java @@ -16,8 +16,6 @@ public class Report { private UUID uuid; private String name; private String event; - private String s3KeyPdf; - private String s3KeyFile; @ManyToOne @JoinColumn(name = "reports") private Cases caseEntity; @@ -33,25 +31,15 @@ public void setCaseEntity(Cases caseEntity) { public Report() { } - public Report(UUID id, String name, String event, String s3KeyPdf, String s3KeyFile, Cases caseEntity) { + public Report(UUID id, String name, String event, Cases caseEntity) { this.uuid = id; this.name = name; this.event = event; - this.s3KeyPdf = s3KeyPdf; - this.s3KeyFile = s3KeyFile; this.caseEntity = caseEntity; } - public Report(UUID id, String name, String event, String s3Keypdf, String s3KeyFile) { - this(id, name, event, s3Keypdf, s3KeyFile, null); - } - - public Report(UUID id, String name, String event, Cases caseEntity) { - this(id, name, event, null, null, caseEntity); - } - public Report(UUID id, String name, String event) { - this(id, name, event, null, null, null); + this(id, name, event, (Cases) null); } @@ -80,21 +68,6 @@ public void setEvent(String event) { this.event = event; } - public String getS3KeyPdf() { - return s3KeyPdf; - } - - public void setS3KeyPdf(String s3KeyPdf) { - this.s3KeyPdf = s3KeyPdf; - } - public String getS3KeyFile() { - return s3KeyFile; - } - - public void setS3KeyFile(String s3KeyFile) { - this.s3KeyFile = s3KeyFile; - } - @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; diff --git a/src/main/java/org/example/crimearchive/reports/ReportService.java b/src/main/java/org/example/crimearchive/reports/ReportService.java index 690ead3..3ea3ad9 100644 --- a/src/main/java/org/example/crimearchive/reports/ReportService.java +++ b/src/main/java/org/example/crimearchive/reports/ReportService.java @@ -1,35 +1,16 @@ package org.example.crimearchive.reports; -import com.itextpdf.text.Document; -import com.itextpdf.text.Image; -import com.itextpdf.text.Paragraph; -import com.itextpdf.text.pdf.PdfWriter; import org.example.crimearchive.DTO.CreateReport; import org.example.crimearchive.DTO.ReportResponse; import org.example.crimearchive.KNumberService; import org.example.crimearchive.cases.Cases; import org.example.crimearchive.cases.CasesRepository; -import org.example.crimearchive.mapper.ReportMapper; import org.example.crimearchive.polis.Account; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; -import software.amazon.awssdk.core.ResponseBytes; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import org.springframework.http.HttpStatus; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -39,154 +20,32 @@ public class ReportService { private final ReportRepository reportRepository; private final KNumberService knumberService; private final CasesRepository casesRepository; - private final S3Client s3Client; - @Value("${minio.bucket}") - private String bucket; - - public ReportService(ReportRepository reportRepository, KNumberService knumberService, CasesRepository casesRepository, S3Client s3Client) { + public ReportService(ReportRepository reportRepository, KNumberService knumberService, CasesRepository casesRepository) { this.reportRepository = reportRepository; this.knumberService = knumberService; this.casesRepository = casesRepository; - this.s3Client = s3Client; } @Transactional - public void saveReport(CreateReport report, Account currentUser) { + public String saveReport(CreateReport report, Account currentUser) { Cases cases; if (report.caseNumber() == null || report.caseNumber().isBlank()) { String newCaseNumber = knumberService.getKNumber(); cases = new Cases(newCaseNumber); - cases.getAccounts().add(currentUser); - casesRepository.save(cases); } else { String sanitized = caseNumberSanitation(report.caseNumber()); cases = casesRepository.findFirstByCaseNumber(sanitized) - .orElseThrow(() -> new RuntimeException("Case not found: " + sanitized)); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ärende hittades inte: " + sanitized)); } Report newReport = new Report(UUID.randomUUID(), report.name(), report.event(), cases); reportRepository.save(newReport); - } - - @Transactional - public void saveReportWithFile(CreateReport report, MultipartFile file) throws IOException { - String s3KeyPdf = null; - String s3KeyFile = null; - - try { - ByteArrayOutputStream pdfStream = new ByteArrayOutputStream(); - Document document = new Document(); - PdfWriter.getInstance(document, pdfStream); - document.open(); - - - document.add(new Paragraph("Brottsanmälan")); - document.add(new Paragraph("Namn: " + report.name())); - document.add(new Paragraph("Brottstyp: " + report.event())); - document.add(new Paragraph("Datum: " + LocalDateTime.now())); - - - if (file != null && !file.isEmpty()) { - document.newPage(); - - if (isImage(file)) { - document.add(new Paragraph("Bifogat bevisfoto:")); - Image image = Image.getInstance(file.getBytes()); - image.scaleToFit(500, 700); - document.add(image); - - } else if (isPdf(file)) { - document.add(new Paragraph("Bifogat dokument (PDF):")); - document.add(new Paragraph(file.getOriginalFilename())); - document.add(new Paragraph("Se separat bifogad fil för fullständigt dokument.")); - - } else if (isWord(file)) { - document.add(new Paragraph("Bifogat dokument (Word):")); - document.add(new Paragraph(file.getOriginalFilename())); - document.add(new Paragraph("Se separat bifogad fil för fullständigt dokument.")); - - } else { - document.add(new Paragraph("Bifogad fil: " + file.getOriginalFilename())); - } - } - - document.close(); - - byte[] pdfBytes = pdfStream.toByteArray(); - s3KeyPdf = "reports/pdf/" + UUID.randomUUID() + ".pdf"; - - s3Client.putObject( - PutObjectRequest.builder() - .bucket(bucket) - .key(s3KeyPdf) - .contentType("application/pdf") - .build(), - RequestBody.fromBytes(pdfBytes) - ); - - if (file != null && !file.isEmpty()) { - s3KeyFile = "reports/files/" + UUID.randomUUID(); - - s3Client.putObject( - PutObjectRequest.builder() - .bucket(bucket) - .key(s3KeyFile) - .contentType(file.getContentType()) - .build(), - RequestBody.fromInputStream(file.getInputStream(), file.getSize()) - ); - } - - reportRepository.save(ReportMapper.toEntity(report, s3KeyPdf, s3KeyFile)); - - } catch (Exception e) { - try { - if (s3KeyPdf != null) { - s3Client.deleteObject(DeleteObjectRequest.builder() - .bucket(bucket).key(s3KeyPdf).build()); - } - } catch (Exception cleanupEx) { - } - try { - if (s3KeyFile != null) { - s3Client.deleteObject(DeleteObjectRequest.builder() - .bucket(bucket).key(s3KeyFile).build()); - } - } catch (Exception cleanupEx) { - } - throw new IOException("Kunde inte spara rapport: " + e.getMessage()); - } - } - - private boolean isImage(MultipartFile file) { - String contentType = file.getContentType(); - return contentType != null && contentType.startsWith("image/"); - } - - private boolean isPdf(MultipartFile file) { - String contentType = file.getContentType(); - return contentType != null && contentType.equals("application/pdf"); - } - private boolean isWord(MultipartFile file) { - String contentType = file.getContentType(); - return contentType != null && ( - contentType.equals("application/msword") || - contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document") - ); - } - - - private String caseNumberSanitation(String caseNumber) { - if (caseNumber.matches("^\\d{4}-\\d{6}$")) { - return "K-" + caseNumber; - } else { - return caseNumber.toUpperCase(); - } + return cases.getCaseNumber(); } public List getAllReports() { @@ -197,52 +56,17 @@ public long getAmount() { return reportRepository.count(); } - public ResponseEntity downloadPdf(UUID uuid) { - Report report = reportRepository.findById(uuid) - .orElseThrow(() -> new ResponseStatusException( - HttpStatus.NOT_FOUND, "Rapporten hittades inte: " + uuid)); - - if (report.getS3KeyPdf() == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ingen PDF finns för denna rapport"); - } - - ResponseBytes objectBytes = s3Client.getObjectAsBytes( - GetObjectRequest.builder() - .bucket(bucket) - .key(report.getS3KeyPdf()) - .build() - ); - - return ResponseEntity.ok() - .contentType(MediaType.APPLICATION_PDF) - .header("Content-Disposition", "attachment; filename=rapport.pdf") - .body(objectBytes.asByteArray()); - } - - public ResponseEntity downloadFile(UUID uuid) { - Report report = reportRepository.findById(uuid) - .orElseThrow(() -> new ResponseStatusException( - HttpStatus.NOT_FOUND, "Rapporten hittades inte: " + uuid)); - - if (report.getS3KeyFile() == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ingen bifogad fil finns för denna rapport"); - } - - ResponseBytes objectBytes = s3Client.getObjectAsBytes( - GetObjectRequest.builder() - .bucket(bucket) - .key(report.getS3KeyFile()) - .build() - ); - - return ResponseEntity.ok() - .header("Content-Disposition", "attachment; filename=fil") - .body(objectBytes.asByteArray()); - } - public List getAllReportResponses() { return reportRepository.findAll().stream() .map(r -> new ReportResponse(r.getUuid(), r.getName(), r.getEvent())) .toList(); } + + private String caseNumberSanitation(String caseNumber) { + if (caseNumber.matches("^\\d{4}-\\d{6}$")) { + return "K-" + caseNumber; + } else { + return caseNumber.toUpperCase(); + } + } } diff --git a/src/main/resources/templates/cases.html b/src/main/resources/templates/cases.html index 52b6dd2..53c86d4 100644 --- a/src/main/resources/templates/cases.html +++ b/src/main/resources/templates/cases.html @@ -1,5 +1,5 @@ - + Cases @@ -19,11 +19,20 @@

Ärenden

K-nummer + Filer + + + Inga filer uppladdade + @@ -41,4 +50,4 @@

Lägg till konto till case

- \ No newline at end of file + diff --git a/src/test/java/org/example/crimearchive/service/ReportServiceTest.java b/src/test/java/org/example/crimearchive/service/ReportServiceTest.java index 1d71cc9..792c3e2 100644 --- a/src/test/java/org/example/crimearchive/service/ReportServiceTest.java +++ b/src/test/java/org/example/crimearchive/service/ReportServiceTest.java @@ -3,30 +3,19 @@ import org.example.crimearchive.DTO.CreateReport; import org.example.crimearchive.DTO.ReportResponse; import org.example.crimearchive.KNumberService; +import org.example.crimearchive.cases.Cases; import org.example.crimearchive.cases.CasesRepository; import org.example.crimearchive.reports.Report; import org.example.crimearchive.reports.ReportRepository; import org.example.crimearchive.reports.ReportService; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.ResponseStatusException; -import software.amazon.awssdk.core.ResponseBytes; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; - -import java.io.IOException; + import java.util.List; import java.util.Optional; import java.util.UUID; @@ -44,172 +33,52 @@ class ReportServiceTest { private KNumberService kNumberService; @Mock private CasesRepository casesRepository; - @Mock - private S3Client s3Client; @InjectMocks private ReportService reportService; - @BeforeEach - void setUp() { - ReflectionTestUtils.setField(reportService, "bucket", "crime-archive"); - } - - @Test - void saveReport_noFile_uploadsOnlyPdf() throws IOException { - CreateReport request = new CreateReport("Johan", "Murder"); + void saveReport_noCaseNumber_createsNewCaseAndSavesReport() { + CreateReport request = new CreateReport("Murder", "Johan"); + when(kNumberService.getKNumber()).thenReturn("K-2026-000001"); - reportService.saveReport(request, null); - // Kommenterade ut för "Wanted but not invoked ... Actually, there were zero interactions with this mock. " - //verify(s3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); - verify(reportRepository, times(1)).save(any(Report.class)); - } + String caseNumber = reportService.saveReport(request, null); - @Test - void saveReport_withImage_uploadsTwoFiles() throws IOException { - CreateReport request = new CreateReport("Johan", "Murder"); - - byte[] minimalPng = new byte[]{ - (byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, - 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x08, 0x02, 0x00, 0x00, 0x00, (byte)0x90, 0x77, 0x53, - (byte)0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, - 0x54, 0x08, (byte)0xD7, 0x63, (byte)0xF8, (byte)0xCF, (byte)0xC0, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x01, (byte)0xE2, 0x21, (byte)0xBC, - 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, - 0x44, (byte)0xAE, 0x42, 0x60, (byte)0x82 - }; - - MockMultipartFile image = new MockMultipartFile( - "file", "evidence.png", "image/png", minimalPng - ); - - reportService.saveReportWithFile(request, image); - - verify(s3Client, times(2)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + assertEquals("K-2026-000001", caseNumber); + verify(casesRepository, times(1)).save(any(Cases.class)); verify(reportRepository, times(1)).save(any(Report.class)); } @Test - void saveReport_withPdf_uploadsTwoFiles() throws IOException { - CreateReport request = new CreateReport("Johan", "Murder"); - MockMultipartFile pdf = new MockMultipartFile( - "file", "document.pdf", "application/pdf", "fake-pdf".getBytes() - ); + void saveReport_existingCaseNumber_usesExistingCase() { + Cases existingCase = new Cases("K-2026-000001"); + CreateReport request = new CreateReport("Murder", "Johan", "K-2026-000001"); + when(casesRepository.findFirstByCaseNumber("K-2026-000001")).thenReturn(Optional.of(existingCase)); - reportService.saveReportWithFile(request, pdf); + String caseNumber = reportService.saveReport(request, null); - verify(s3Client, times(2)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + assertEquals("K-2026-000001", caseNumber); + verify(casesRepository, never()).save(any()); verify(reportRepository, times(1)).save(any(Report.class)); } @Test - void saveReport_withWordFile_uploadsTwoFiles() throws IOException { - CreateReport request = new CreateReport("Johan", "Murder"); - MockMultipartFile word = new MockMultipartFile( - "file", "report.docx", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "fake-word".getBytes() - ); - - reportService.saveReportWithFile(request, word); - - verify(s3Client, times(2)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); - verify(reportRepository, times(1)).save(any(Report.class)); - } - - @Test - void saveReport_databaseFails_cleansUpS3Files() { - CreateReport request = new CreateReport("Johan", "Murder"); - when(reportRepository.save(any())).thenThrow(new RuntimeException("Database error")); - - assertThrows(IOException.class, () -> reportService.saveReportWithFile(request, null)); - - verify(s3Client, atLeastOnce()).deleteObject(any(DeleteObjectRequest.class)); - } - - - @Test - void downloadPdf_reportExists_returnsPdfWith200() { - UUID uuid = UUID.randomUUID(); - Report report = new Report(uuid, "Johan", "Murder", "reports/pdf/test.pdf", null); - when(reportRepository.findById(uuid)).thenReturn(Optional.of(report)); - - ResponseBytes mockBytes = mock(ResponseBytes.class); - when(mockBytes.asByteArray()).thenReturn("pdf-content".getBytes()); - when(s3Client.getObjectAsBytes(any(GetObjectRequest.class))).thenReturn(mockBytes); - - ResponseEntity response = reportService.downloadPdf(uuid); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - } - - @Test - void downloadPdf_reportNotFound_throws404() { - UUID uuid = UUID.randomUUID(); - when(reportRepository.findById(uuid)).thenReturn(Optional.empty()); - - ResponseStatusException ex = assertThrows( - ResponseStatusException.class, - () -> reportService.downloadPdf(uuid) - ); - - assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); - } - - @Test - void downloadPdf_noPdfKey_throws404() { - UUID uuid = UUID.randomUUID(); - Report report = new Report(uuid, "Johan", "Murder", null, null); - when(reportRepository.findById(uuid)).thenReturn(Optional.of(report)); + void saveReport_caseNotFound_throws404() { + CreateReport request = new CreateReport("Murder", "Johan", "K-2026-999999"); + when(casesRepository.findFirstByCaseNumber("K-2026-999999")).thenReturn(Optional.empty()); ResponseStatusException ex = assertThrows( ResponseStatusException.class, - () -> reportService.downloadPdf(uuid) + () -> reportService.saveReport(request, null) ); assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); } - @Test - void downloadFile_fileExists_returnsFileWith200() { + void getAllReportResponses_returnsCorrectData() { UUID uuid = UUID.randomUUID(); - Report report = new Report(uuid, "Johan", "Murder", "reports/pdf/test.pdf", "reports/files/evidence.jpg"); - when(reportRepository.findById(uuid)).thenReturn(Optional.of(report)); - - ResponseBytes mockBytes = mock(ResponseBytes.class); - when(mockBytes.asByteArray()).thenReturn("file-content".getBytes()); - when(s3Client.getObjectAsBytes(any(GetObjectRequest.class))).thenReturn(mockBytes); - - ResponseEntity response = reportService.downloadFile(uuid); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - } - - @Test - void downloadFile_noFileKey_throws404() { - UUID uuid = UUID.randomUUID(); - Report report = new Report(uuid, "Johan", "Murder", "reports/pdf/test.pdf", null); - when(reportRepository.findById(uuid)).thenReturn(Optional.of(report)); - - ResponseStatusException ex = assertThrows( - ResponseStatusException.class, - () -> reportService.downloadFile(uuid) - ); - - assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); - } - - - @Test - void getAllReportResponses_returnsListWithoutS3Keys() { - UUID uuid = UUID.randomUUID(); - Report report = new Report(uuid, "Johan", "Murder", "reports/pdf/test.pdf", "reports/files/evidence.jpg"); + Report report = new Report(uuid, "Johan", "Murder"); when(reportRepository.findAll()).thenReturn(List.of(report)); List responses = reportService.getAllReportResponses(); @@ -228,4 +97,11 @@ void getAllReportResponses_noReports_returnsEmptyList() { assertTrue(responses.isEmpty()); } + + @Test + void getAmount_returnsRepositoryCount() { + when(reportRepository.count()).thenReturn(5L); + + assertEquals(5L, reportService.getAmount()); + } } From 3f7d78010a3f00b836e02ded16598a55019660bf Mon Sep 17 00:00:00 2001 From: gurkvatten Date: Mon, 20 Apr 2026 09:23:48 +0200 Subject: [PATCH 3/7] fix after push --- .../crimearchive/evidence/EvidenceFile.java | 48 ++++ .../evidence/EvidenceFileController.java | 43 ++++ .../evidence/EvidenceFileRepository.java | 17 ++ .../evidence/EvidenceFileService.java | 210 ++++++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 src/main/java/org/example/crimearchive/evidence/EvidenceFile.java create mode 100644 src/main/java/org/example/crimearchive/evidence/EvidenceFileController.java create mode 100644 src/main/java/org/example/crimearchive/evidence/EvidenceFileRepository.java create mode 100644 src/main/java/org/example/crimearchive/evidence/EvidenceFileService.java diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java new file mode 100644 index 0000000..b8f1956 --- /dev/null +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java @@ -0,0 +1,48 @@ +package org.example.crimearchive.evidence; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +public class EvidenceFile { + + @Id + private UUID id; + + private UUID groupId; + private int version; + private String caseNumber; + private String s3KeyPdf; + private String s3KeyFile; + private String originalFilename; + private String uploadedBy; + private LocalDateTime uploadedAt; + + public EvidenceFile() { + } + + public EvidenceFile(UUID id, UUID groupId, int version, String caseNumber, String s3KeyPdf, + String s3KeyFile, String originalFilename, String uploadedBy, LocalDateTime uploadedAt) { + this.id = id; + this.groupId = groupId; + this.version = version; + this.caseNumber = caseNumber; + this.s3KeyPdf = s3KeyPdf; + this.s3KeyFile = s3KeyFile; + this.originalFilename = originalFilename; + this.uploadedBy = uploadedBy; + this.uploadedAt = uploadedAt; + } + + public UUID getId() { return id; } + public UUID getGroupId() { return groupId; } + public int getVersion() { return version; } + public String getCaseNumber() { return caseNumber; } + public String getS3KeyPdf() { return s3KeyPdf; } + public String getS3KeyFile() { return s3KeyFile; } + public String getOriginalFilename() { return originalFilename; } + public String getUploadedBy() { return uploadedBy; } + public LocalDateTime getUploadedAt() { return uploadedAt; } +} diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFileController.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFileController.java new file mode 100644 index 0000000..091afb1 --- /dev/null +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFileController.java @@ -0,0 +1,43 @@ +package org.example.crimearchive.evidence; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/evidence") +public class EvidenceFileController { + + private final EvidenceFileService evidenceFileService; + + public EvidenceFileController(EvidenceFileService evidenceFileService) { + this.evidenceFileService = evidenceFileService; + } + + @GetMapping("/case/{caseNumber}") + public List getByCaseNumber(@PathVariable String caseNumber) { + return evidenceFileService.getByCaseNumber(caseNumber); + } + + @GetMapping("/case/{caseNumber}/latest") + public List getLatestVersions(@PathVariable String caseNumber) { + return evidenceFileService.getLatestVersionsByCaseNumber(caseNumber); + } + + @GetMapping("/group/{groupId}/history") + public List getVersionHistory(@PathVariable UUID groupId) { + return evidenceFileService.getVersionHistory(groupId); + } + + @GetMapping("/{id}/pdf") + public ResponseEntity downloadPdf(@PathVariable UUID id) { + return evidenceFileService.downloadPdf(id); + } + + @GetMapping("/{id}/file") + public ResponseEntity downloadFile(@PathVariable UUID id) { + return evidenceFileService.downloadFile(id); + } +} diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFileRepository.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFileRepository.java new file mode 100644 index 0000000..8d340a2 --- /dev/null +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFileRepository.java @@ -0,0 +1,17 @@ +package org.example.crimearchive.evidence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface EvidenceFileRepository extends JpaRepository { + List findByCaseNumber(String caseNumber); + List findByGroupIdOrderByVersionAsc(UUID groupId); + Optional findTopByGroupIdOrderByVersionDesc(UUID groupId); + + @Query("SELECT e FROM EvidenceFile e WHERE e.caseNumber = :caseNumber AND e.version = (SELECT MAX(e2.version) FROM EvidenceFile e2 WHERE e2.groupId = e.groupId)") + List findLatestVersionsByCaseNumber(String caseNumber); +} diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFileService.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFileService.java new file mode 100644 index 0000000..23a7778 --- /dev/null +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFileService.java @@ -0,0 +1,210 @@ +package org.example.crimearchive.evidence; + +import com.itextpdf.text.Document; +import com.itextpdf.text.Image; +import com.itextpdf.text.Paragraph; +import com.itextpdf.text.pdf.PdfWriter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Service +public class EvidenceFileService { + + private final EvidenceFileRepository evidenceFileRepository; + private final S3Client s3Client; + + @Value("${minio.bucket}") + private String bucket; + + public EvidenceFileService(EvidenceFileRepository evidenceFileRepository, S3Client s3Client) { + this.evidenceFileRepository = evidenceFileRepository; + this.s3Client = s3Client; + } + + public void upload(String caseNumber, String reportName, String reportEvent, + MultipartFile file, String uploadedBy) throws IOException { + upload(caseNumber, reportName, reportEvent, file, uploadedBy, null); + } + + public void upload(String caseNumber, String reportName, String reportEvent, + MultipartFile file, String uploadedBy, UUID groupId) throws IOException { + String s3KeyPdf = null; + String s3KeyFile = null; + + try { + int nextVersion = 1; + UUID resolvedGroupId = groupId; + + if (resolvedGroupId != null) { + int latest = evidenceFileRepository.findTopByGroupIdOrderByVersionDesc(resolvedGroupId) + .map(EvidenceFile::getVersion) + .orElse(0); + nextVersion = latest + 1; + } else { + resolvedGroupId = UUID.randomUUID(); + } + + byte[] pdfBytes = generatePdf(reportName, reportEvent, file); + s3KeyPdf = "cases/" + caseNumber + "/pdf/" + UUID.randomUUID() + ".pdf"; + + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(s3KeyPdf) + .contentType("application/pdf") + .build(), + RequestBody.fromBytes(pdfBytes) + ); + + if (file != null && !file.isEmpty()) { + s3KeyFile = "cases/" + caseNumber + "/files/" + UUID.randomUUID(); + + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(s3KeyFile) + .contentType(file.getContentType()) + .build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); + } + + evidenceFileRepository.save(new EvidenceFile( + UUID.randomUUID(), + resolvedGroupId, + nextVersion, + caseNumber, + s3KeyPdf, + s3KeyFile, + file != null ? file.getOriginalFilename() : null, + uploadedBy, + LocalDateTime.now() + )); + + } catch (Exception e) { + deleteIfExists(s3KeyPdf); + deleteIfExists(s3KeyFile); + throw new IOException("Kunde inte ladda upp bevisfil: " + e.getMessage(), e); + } + } + + public ResponseEntity downloadPdf(UUID id) { + EvidenceFile evidence = evidenceFileRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Bevisfil hittades inte: " + id)); + + ResponseBytes objectBytes = s3Client.getObjectAsBytes( + GetObjectRequest.builder() + .bucket(bucket) + .key(evidence.getS3KeyPdf()) + .build() + ); + + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_PDF) + .header("Content-Disposition", "attachment; filename=rapport.pdf") + .body(objectBytes.asByteArray()); + } + + public ResponseEntity downloadFile(UUID id) { + EvidenceFile evidence = evidenceFileRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Bevisfil hittades inte: " + id)); + + if (evidence.getS3KeyFile() == null) { + throw new RuntimeException("Ingen bifogad fil finns för denna post"); + } + + ResponseBytes objectBytes = s3Client.getObjectAsBytes( + GetObjectRequest.builder() + .bucket(bucket) + .key(evidence.getS3KeyFile()) + .build() + ); + + return ResponseEntity.ok() + .header("Content-Disposition", "attachment; filename=" + evidence.getOriginalFilename()) + .body(objectBytes.asByteArray()); + } + + public List getByCaseNumber(String caseNumber) { + return evidenceFileRepository.findByCaseNumber(caseNumber); + } + + public List getLatestVersionsByCaseNumber(String caseNumber) { + return evidenceFileRepository.findLatestVersionsByCaseNumber(caseNumber); + } + + public List getVersionHistory(UUID groupId) { + return evidenceFileRepository.findByGroupIdOrderByVersionAsc(groupId); + } + + private byte[] generatePdf(String reportName, String reportEvent, MultipartFile file) throws Exception { + ByteArrayOutputStream pdfStream = new ByteArrayOutputStream(); + Document document = new Document(); + PdfWriter.getInstance(document, pdfStream); + document.open(); + + document.add(new Paragraph("Brottsanmälan")); + document.add(new Paragraph("Namn: " + reportName)); + document.add(new Paragraph("Brottstyp: " + reportEvent)); + document.add(new Paragraph("Datum: " + LocalDateTime.now())); + + if (file != null && !file.isEmpty()) { + document.newPage(); + if (isImage(file)) { + document.add(new Paragraph("Bifogat bevisfoto:")); + Image image = Image.getInstance(file.getBytes()); + image.scaleToFit(500, 700); + document.add(image); + } else if (isPdf(file)) { + document.add(new Paragraph("Bifogat dokument (PDF): " + file.getOriginalFilename())); + } else if (isWord(file)) { + document.add(new Paragraph("Bifogat dokument (Word): " + file.getOriginalFilename())); + } else { + document.add(new Paragraph("Bifogad fil: " + file.getOriginalFilename())); + } + } + + document.close(); + return pdfStream.toByteArray(); + } + + private void deleteIfExists(String key) { + if (key == null) return; + try { + s3Client.deleteObject(DeleteObjectRequest.builder().bucket(bucket).key(key).build()); + } catch (Exception ignored) { + } + } + + private boolean isImage(MultipartFile file) { + String ct = file.getContentType(); + return ct != null && ct.startsWith("image/"); + } + + private boolean isPdf(MultipartFile file) { + String ct = file.getContentType(); + return "application/pdf".equals(ct); + } + + private boolean isWord(MultipartFile file) { + String ct = file.getContentType(); + return ct != null && (ct.equals("application/msword") || + ct.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document")); + } +} From b56a1375c14c7c9d02be40f953f65fc7ca83164a Mon Sep 17 00:00:00 2001 From: gurkvatten Date: Mon, 20 Apr 2026 10:29:06 +0200 Subject: [PATCH 4/7] evidencefileservice tests --- .../service/EvidenceFileServiceTest.java | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/test/java/org/example/crimearchive/service/EvidenceFileServiceTest.java diff --git a/src/test/java/org/example/crimearchive/service/EvidenceFileServiceTest.java b/src/test/java/org/example/crimearchive/service/EvidenceFileServiceTest.java new file mode 100644 index 0000000..f083e11 --- /dev/null +++ b/src/test/java/org/example/crimearchive/service/EvidenceFileServiceTest.java @@ -0,0 +1,163 @@ +package org.example.crimearchive.service; + +import org.example.crimearchive.evidence.EvidenceFile; +import org.example.crimearchive.evidence.EvidenceFileRepository; +import org.example.crimearchive.evidence.EvidenceFileService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EvidenceFileServiceTest { + + @Mock + private EvidenceFileRepository evidenceFileRepository; + @Mock + private S3Client s3Client; + + @InjectMocks + private EvidenceFileService evidenceFileService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(evidenceFileService, "bucket", "crime-archive"); + } + + @Test + void upload_withImage_uploadsTwoFiles() throws IOException { + byte[] minimalPng = new byte[]{ + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x02, 0x00, 0x00, 0x00, (byte) 0x90, 0x77, 0x53, + (byte) 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, + 0x54, 0x08, (byte) 0xD7, 0x63, (byte) 0xF8, (byte) 0xCF, (byte) 0xC0, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x01, (byte) 0xE2, 0x21, (byte) 0xBC, + 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, + 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82 + }; + MockMultipartFile image = new MockMultipartFile("file", "evidence.png", "image/png", minimalPng); + + evidenceFileService.upload("K-2026-000001", "Johan", "Murder", image, "officer1"); + + verify(s3Client, times(2)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + verify(evidenceFileRepository, times(1)).save(any(EvidenceFile.class)); + } + + @Test + void upload_withPdf_uploadsTwoFiles() throws IOException { + MockMultipartFile pdf = new MockMultipartFile("file", "document.pdf", "application/pdf", "fake-pdf".getBytes()); + + evidenceFileService.upload("K-2026-000001", "Johan", "Murder", pdf, "officer1"); + + verify(s3Client, times(2)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + verify(evidenceFileRepository, times(1)).save(any(EvidenceFile.class)); + } + + @Test + void upload_withWordFile_uploadsTwoFiles() throws IOException { + MockMultipartFile word = new MockMultipartFile( + "file", "report.docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "fake-word".getBytes() + ); + + evidenceFileService.upload("K-2026-000001", "Johan", "Murder", word, "officer1"); + + verify(s3Client, times(2)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + verify(evidenceFileRepository, times(1)).save(any(EvidenceFile.class)); + } + + @Test + void upload_noFile_uploadsOnlyPdf() throws IOException { + evidenceFileService.upload("K-2026-000001", "Johan", "Murder", null, "officer1"); + + verify(s3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + verify(evidenceFileRepository, times(1)).save(any(EvidenceFile.class)); + } + + @Test + void upload_databaseFails_cleansUpS3Files() { + when(evidenceFileRepository.save(any())).thenThrow(new RuntimeException("Database error")); + + assertThrows(IOException.class, () -> + evidenceFileService.upload("K-2026-000001", "Johan", "Murder", null, "officer1") + ); + + verify(s3Client, atLeastOnce()).deleteObject(any(DeleteObjectRequest.class)); + } + + @Test + void downloadPdf_exists_returnsPdfWith200() { + UUID id = UUID.randomUUID(); + EvidenceFile ev = new EvidenceFile(id, UUID.randomUUID(), 1, "K-2026-000001", "cases/K-2026-000001/pdf/test.pdf", null, null, "officer1", null); + when(evidenceFileRepository.findById(id)).thenReturn(Optional.of(ev)); + + ResponseBytes mockBytes = mock(ResponseBytes.class); + when(mockBytes.asByteArray()).thenReturn("pdf-content".getBytes()); + when(s3Client.getObjectAsBytes(any(GetObjectRequest.class))).thenReturn(mockBytes); + + ResponseEntity response = evidenceFileService.downloadPdf(id); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + } + + @Test + void downloadPdf_notFound_throwsException() { + UUID id = UUID.randomUUID(); + when(evidenceFileRepository.findById(id)).thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> evidenceFileService.downloadPdf(id)); + } + + @Test + void downloadFile_exists_returnsWith200() { + UUID id = UUID.randomUUID(); + EvidenceFile ev = new EvidenceFile(id, UUID.randomUUID(), 1, "K-2026-000001", + "cases/K-2026-000001/pdf/test.pdf", "cases/K-2026-000001/files/evidence.jpg", + "evidence.jpg", "officer1", null); + when(evidenceFileRepository.findById(id)).thenReturn(Optional.of(ev)); + + ResponseBytes mockBytes = mock(ResponseBytes.class); + when(mockBytes.asByteArray()).thenReturn("file-content".getBytes()); + when(s3Client.getObjectAsBytes(any(GetObjectRequest.class))).thenReturn(mockBytes); + + ResponseEntity response = evidenceFileService.downloadFile(id); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + } + + @Test + void downloadFile_noFileKey_throwsException() { + UUID id = UUID.randomUUID(); + EvidenceFile ev = new EvidenceFile(id, UUID.randomUUID(), 1, "K-2026-000001", + "cases/K-2026-000001/pdf/test.pdf", null, null, "officer1", null); + when(evidenceFileRepository.findById(id)).thenReturn(Optional.of(ev)); + + assertThrows(RuntimeException.class, () -> evidenceFileService.downloadFile(id)); + } +} From 5554915617176716f313b2c825223878f879f961 Mon Sep 17 00:00:00 2001 From: gurkvatten Date: Mon, 20 Apr 2026 10:41:36 +0200 Subject: [PATCH 5/7] fixes after merge --- .../controllers/HomeController.java | 13 -- .../crimearchive/reports/ReportService.java | 184 +----------------- 2 files changed, 4 insertions(+), 193 deletions(-) diff --git a/src/main/java/org/example/crimearchive/controllers/HomeController.java b/src/main/java/org/example/crimearchive/controllers/HomeController.java index 66f6408..0e2b97a 100644 --- a/src/main/java/org/example/crimearchive/controllers/HomeController.java +++ b/src/main/java/org/example/crimearchive/controllers/HomeController.java @@ -8,18 +8,14 @@ import org.example.crimearchive.reports.ReportService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import java.util.UUID; - @Controller public class HomeController { @@ -69,13 +65,4 @@ public String saveReport( - @GetMapping("/reports/{uuid}/download/pdf") - public ResponseEntity downloadPdf(@PathVariable UUID uuid) { - return reportService.downloadPdf(uuid); - } - - @GetMapping("/reports/{uuid}/download/file") - public ResponseEntity downloadFile(@PathVariable UUID uuid) { - return reportService.downloadFile(uuid); - } } diff --git a/src/main/java/org/example/crimearchive/reports/ReportService.java b/src/main/java/org/example/crimearchive/reports/ReportService.java index 81989a6..704f2c3 100644 --- a/src/main/java/org/example/crimearchive/reports/ReportService.java +++ b/src/main/java/org/example/crimearchive/reports/ReportService.java @@ -1,35 +1,16 @@ package org.example.crimearchive.reports; -import com.itextpdf.text.Document; -import com.itextpdf.text.Image; -import com.itextpdf.text.Paragraph; -import com.itextpdf.text.pdf.PdfWriter; import org.example.crimearchive.DTO.CreateReport; import org.example.crimearchive.DTO.ReportResponse; import org.example.crimearchive.KNumberService; import org.example.crimearchive.cases.Cases; import org.example.crimearchive.cases.CasesRepository; -import org.example.crimearchive.mapper.ReportMapper; import org.example.crimearchive.polis.Account; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; -import software.amazon.awssdk.core.ResponseBytes; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -39,147 +20,33 @@ public class ReportService { private final ReportRepository reportRepository; private final KNumberService knumberService; private final CasesRepository casesRepository; - private final S3Client s3Client; - @Value("${minio.bucket}") - private String bucket; - - public ReportService(ReportRepository reportRepository, KNumberService knumberService, CasesRepository casesRepository, S3Client s3Client) { + public ReportService(ReportRepository reportRepository, KNumberService knumberService, CasesRepository casesRepository) { this.reportRepository = reportRepository; this.knumberService = knumberService; this.casesRepository = casesRepository; - this.s3Client = s3Client; } @Transactional - public void saveReport(CreateReport report, Account currentUser) { + public String saveReport(CreateReport report, Account currentUser) { Cases cases; if (report.caseNumber() == null || report.caseNumber().isBlank()) { String newCaseNumber = knumberService.getKNumber(); cases = new Cases(newCaseNumber); if (currentUser != null) cases.getAccounts().add(currentUser); - casesRepository.save(cases); } else { String sanitized = caseNumberSanitation(report.caseNumber()); cases = casesRepository.findFirstByCaseNumber(sanitized) - .orElseThrow(() -> new RuntimeException("Case not found: " + sanitized)); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found: " + sanitized)); } Report newReport = new Report(UUID.randomUUID(), report.name(), report.event(), cases); reportRepository.save(newReport); + return cases.getCaseNumber(); } - @Transactional - public void saveReportWithFile(CreateReport report, MultipartFile file) throws IOException { - String s3KeyPdf = null; - String s3KeyFile = null; - - try { - ByteArrayOutputStream pdfStream = new ByteArrayOutputStream(); - Document document = new Document(); - PdfWriter.getInstance(document, pdfStream); - document.open(); - - - document.add(new Paragraph("Brottsanmälan")); - document.add(new Paragraph("Namn: " + report.name())); - document.add(new Paragraph("Brottstyp: " + report.event())); - document.add(new Paragraph("Datum: " + LocalDateTime.now())); - - - if (file != null && !file.isEmpty()) { - document.newPage(); - - if (isImage(file)) { - document.add(new Paragraph("Bifogat bevisfoto:")); - Image image = Image.getInstance(file.getBytes()); - image.scaleToFit(500, 700); - document.add(image); - - } else if (isPdf(file)) { - document.add(new Paragraph("Bifogat dokument (PDF):")); - document.add(new Paragraph(file.getOriginalFilename())); - document.add(new Paragraph("Se separat bifogad fil för fullständigt dokument.")); - - } else if (isWord(file)) { - document.add(new Paragraph("Bifogat dokument (Word):")); - document.add(new Paragraph(file.getOriginalFilename())); - document.add(new Paragraph("Se separat bifogad fil för fullständigt dokument.")); - - } else { - document.add(new Paragraph("Bifogad fil: " + file.getOriginalFilename())); - } - } - - document.close(); - - byte[] pdfBytes = pdfStream.toByteArray(); - s3KeyPdf = "reports/pdf/" + UUID.randomUUID() + ".pdf"; - - s3Client.putObject( - PutObjectRequest.builder() - .bucket(bucket) - .key(s3KeyPdf) - .contentType("application/pdf") - .build(), - RequestBody.fromBytes(pdfBytes) - ); - - if (file != null && !file.isEmpty()) { - s3KeyFile = "reports/files/" + UUID.randomUUID(); - - s3Client.putObject( - PutObjectRequest.builder() - .bucket(bucket) - .key(s3KeyFile) - .contentType(file.getContentType()) - .build(), - RequestBody.fromInputStream(file.getInputStream(), file.getSize()) - ); - } - - reportRepository.save(ReportMapper.toEntity(report, s3KeyPdf, s3KeyFile)); - - } catch (Exception e) { - try { - if (s3KeyPdf != null) { - s3Client.deleteObject(DeleteObjectRequest.builder() - .bucket(bucket).key(s3KeyPdf).build()); - } - } catch (Exception cleanupEx) { - } - try { - if (s3KeyFile != null) { - s3Client.deleteObject(DeleteObjectRequest.builder() - .bucket(bucket).key(s3KeyFile).build()); - } - } catch (Exception cleanupEx) { - } - throw new IOException("Kunde inte spara rapport: " + e.getMessage()); - } - } - - private boolean isImage(MultipartFile file) { - String contentType = file.getContentType(); - return contentType != null && contentType.startsWith("image/"); - } - - private boolean isPdf(MultipartFile file) { - String contentType = file.getContentType(); - return contentType != null && contentType.equals("application/pdf"); - } - - private boolean isWord(MultipartFile file) { - String contentType = file.getContentType(); - return contentType != null && ( - contentType.equals("application/msword") || - contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document") - ); - } - - private String caseNumberSanitation(String caseNumber) { if (caseNumber.matches("^\\d{4}-\\d{6}$")) { return "K-" + caseNumber; @@ -196,49 +63,6 @@ public long getAmount() { return reportRepository.count(); } - public ResponseEntity downloadPdf(UUID uuid) { - Report report = reportRepository.findById(uuid) - .orElseThrow(() -> new ResponseStatusException( - HttpStatus.NOT_FOUND, "Rapporten hittades inte: " + uuid)); - - if (report.getS3KeyPdf() == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ingen PDF finns för denna rapport"); - } - - ResponseBytes objectBytes = s3Client.getObjectAsBytes( - GetObjectRequest.builder() - .bucket(bucket) - .key(report.getS3KeyPdf()) - .build() - ); - - return ResponseEntity.ok() - .contentType(MediaType.APPLICATION_PDF) - .header("Content-Disposition", "attachment; filename=rapport.pdf") - .body(objectBytes.asByteArray()); - } - - public ResponseEntity downloadFile(UUID uuid) { - Report report = reportRepository.findById(uuid) - .orElseThrow(() -> new ResponseStatusException( - HttpStatus.NOT_FOUND, "Rapporten hittades inte: " + uuid)); - - if (report.getS3KeyFile() == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ingen bifogad fil finns för denna rapport"); - } - - ResponseBytes objectBytes = s3Client.getObjectAsBytes( - GetObjectRequest.builder() - .bucket(bucket) - .key(report.getS3KeyFile()) - .build() - ); - - return ResponseEntity.ok() - .header("Content-Disposition", "attachment; filename=fil") - .body(objectBytes.asByteArray()); - } - public List getAllReportResponses() { return reportRepository.findAll().stream() .map(r -> new ReportResponse(r.getUuid(), r.getName(), r.getEvent())) From bc60303936051a411af7128f2332855844c93eaf Mon Sep 17 00:00:00 2001 From: Johan Karlsson <93186588+gurkvatten@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:51:51 +0200 Subject: [PATCH 6/7] Update src/main/java/org/example/crimearchive/evidence/EvidenceFile.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../crimearchive/evidence/EvidenceFile.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java index b8f1956..23a775d 100644 --- a/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java @@ -5,19 +5,37 @@ import java.time.LocalDateTime; import java.util.UUID; -@Entity +`@Entity` +`@Table`( + uniqueConstraints = `@UniqueConstraint`( + name = "uk_evidence_file_group_version", + columnNames = {"group_id", "version"} + ), + indexes = { + `@Index`(name = "idx_evidence_file_case_number", columnList = "case_number"), + `@Index`(name = "idx_evidence_file_group_id_version", columnList = "group_id, version") + } +) public class EvidenceFile { - @Id + `@Id` + `@Column`(nullable = false, updatable = false) private UUID id; + `@Column`(name = "group_id", nullable = false) private UUID groupId; + `@Column`(nullable = false) private int version; + `@Column`(name = "case_number", nullable = false) private String caseNumber; + `@Column`(name = "s3_key_pdf", nullable = false) private String s3KeyPdf; + `@Column`(name = "s3_key_file") private String s3KeyFile; private String originalFilename; + `@Column`(nullable = false) private String uploadedBy; + `@Column`(nullable = false) private LocalDateTime uploadedAt; public EvidenceFile() { From 657122a58c7f70d8914257ced97b616c79fb2993 Mon Sep 17 00:00:00 2001 From: gurkvatten Date: Mon, 20 Apr 2026 11:12:14 +0200 Subject: [PATCH 7/7] fixes after coderabbit suggestions and ci/cd run --- .../controllers/HomeController.java | 25 +++++--- .../crimearchive/evidence/EvidenceFile.java | 31 +++++----- .../evidence/EvidenceFileController.java | 31 +++++++--- .../evidence/EvidenceFileRepository.java | 7 ++- .../evidence/EvidenceFileResponse.java | 21 +++++++ .../evidence/EvidenceFileService.java | 61 +++++++++++++++---- .../service/EvidenceFileServiceTest.java | 33 ++++++++-- 7 files changed, 162 insertions(+), 47 deletions(-) create mode 100644 src/main/java/org/example/crimearchive/evidence/EvidenceFileResponse.java diff --git a/src/main/java/org/example/crimearchive/controllers/HomeController.java b/src/main/java/org/example/crimearchive/controllers/HomeController.java index 0e2b97a..03116ad 100644 --- a/src/main/java/org/example/crimearchive/controllers/HomeController.java +++ b/src/main/java/org/example/crimearchive/controllers/HomeController.java @@ -4,6 +4,7 @@ import org.example.crimearchive.DTO.CreateReport; import org.example.crimearchive.cases.CaseService; import org.example.crimearchive.cases.CasesRepository; +import org.example.crimearchive.evidence.EvidenceFileService; import org.example.crimearchive.polis.Account; import org.example.crimearchive.reports.ReportService; import org.slf4j.Logger; @@ -15,17 +16,22 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; @Controller public class HomeController { Logger log = LoggerFactory.getLogger(HomeController.class); private final ReportService reportService; + private final EvidenceFileService evidenceFileService; private final CaseService caseService; private final CasesRepository casesRepository; - public HomeController(ReportService reportService, CaseService caseService, CasesRepository casesRepository) { + public HomeController(ReportService reportService, EvidenceFileService evidenceFileService, + CaseService caseService, CasesRepository casesRepository) { this.reportService = reportService; + this.evidenceFileService = evidenceFileService; this.caseService = caseService; this.casesRepository = casesRepository; } @@ -47,22 +53,27 @@ public String userPage(@AuthenticationPrincipal Account user, Model model) { return "userpage"; } - - @PostMapping("/reports/add") public String saveReport( @ModelAttribute("newReport") @Valid CreateReport newReport, BindingResult bindingResult, + @RequestParam(required = false) MultipartFile file, @AuthenticationPrincipal Account currentUser) { if (bindingResult.hasErrors()) { log.info("Binding Error: {}", bindingResult.getAllErrors().getLast()); return "registerreport"; } - reportService.saveReport(newReport, null); - return "redirect:/userpage"; - } - + try { + String caseNumber = reportService.saveReport(newReport, currentUser); + evidenceFileService.upload(caseNumber, newReport.name(), newReport.event(), + file, currentUser.getUsername()); + } catch (Exception e) { + log.error("Fel vid sparande av rapport", e); + return "registerreport"; + } + return "redirect:/userpage"; + } } diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java index 23a775d..bfe49a4 100644 --- a/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFile.java @@ -5,37 +5,38 @@ import java.time.LocalDateTime; import java.util.UUID; -`@Entity` -`@Table`( - uniqueConstraints = `@UniqueConstraint`( +@Entity +@Table( + uniqueConstraints = @UniqueConstraint( name = "uk_evidence_file_group_version", columnNames = {"group_id", "version"} ), indexes = { - `@Index`(name = "idx_evidence_file_case_number", columnList = "case_number"), - `@Index`(name = "idx_evidence_file_group_id_version", columnList = "group_id, version") + @Index(name = "idx_evidence_file_case_number", columnList = "case_number"), + @Index(name = "idx_evidence_file_group_id_version", columnList = "group_id, version") } ) public class EvidenceFile { - `@Id` - `@Column`(nullable = false, updatable = false) + @Id + @Column(nullable = false, updatable = false) private UUID id; - `@Column`(name = "group_id", nullable = false) + @Column(name = "group_id", nullable = false) private UUID groupId; - `@Column`(nullable = false) + @Column(nullable = false) private int version; - `@Column`(name = "case_number", nullable = false) + @Column(name = "case_number", nullable = false) private String caseNumber; - `@Column`(name = "s3_key_pdf", nullable = false) + @Column(name = "s3_key_pdf", nullable = false) private String s3KeyPdf; - `@Column`(name = "s3_key_file") + @Column(name = "s3_key_file") private String s3KeyFile; private String originalFilename; - `@Column`(nullable = false) + private String contentType; + @Column(nullable = false) private String uploadedBy; - `@Column`(nullable = false) + @Column(nullable = false) private LocalDateTime uploadedAt; public EvidenceFile() { @@ -61,6 +62,8 @@ public EvidenceFile(UUID id, UUID groupId, int version, String caseNumber, Strin public String getS3KeyPdf() { return s3KeyPdf; } public String getS3KeyFile() { return s3KeyFile; } public String getOriginalFilename() { return originalFilename; } + public String getContentType() { return contentType; } + public void setContentType(String contentType) { this.contentType = contentType; } public String getUploadedBy() { return uploadedBy; } public LocalDateTime getUploadedAt() { return uploadedAt; } } diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFileController.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFileController.java index 091afb1..7bbce0e 100644 --- a/src/main/java/org/example/crimearchive/evidence/EvidenceFileController.java +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFileController.java @@ -1,6 +1,9 @@ package org.example.crimearchive.evidence; +import org.example.crimearchive.polis.Account; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -17,27 +20,35 @@ public EvidenceFileController(EvidenceFileService evidenceFileService) { } @GetMapping("/case/{caseNumber}") - public List getByCaseNumber(@PathVariable String caseNumber) { - return evidenceFileService.getByCaseNumber(caseNumber); + @PreAuthorize("@caseSecurity.canAccessCase(#caseNumber, principal)") + public List getByCaseNumber(@PathVariable String caseNumber) { + return evidenceFileService.getByCaseNumber(caseNumber) + .stream().map(EvidenceFileResponse::from).toList(); } @GetMapping("/case/{caseNumber}/latest") - public List getLatestVersions(@PathVariable String caseNumber) { - return evidenceFileService.getLatestVersionsByCaseNumber(caseNumber); + @PreAuthorize("@caseSecurity.canAccessCase(#caseNumber, principal)") + public List getLatestVersions(@PathVariable String caseNumber) { + return evidenceFileService.getLatestVersionsByCaseNumber(caseNumber) + .stream().map(EvidenceFileResponse::from).toList(); } @GetMapping("/group/{groupId}/history") - public List getVersionHistory(@PathVariable UUID groupId) { - return evidenceFileService.getVersionHistory(groupId); + public List getVersionHistory(@PathVariable UUID groupId, + @AuthenticationPrincipal Account currentUser) { + return evidenceFileService.getVersionHistory(groupId, currentUser) + .stream().map(EvidenceFileResponse::from).toList(); } @GetMapping("/{id}/pdf") - public ResponseEntity downloadPdf(@PathVariable UUID id) { - return evidenceFileService.downloadPdf(id); + public ResponseEntity downloadPdf(@PathVariable UUID id, + @AuthenticationPrincipal Account currentUser) { + return evidenceFileService.downloadPdf(id, currentUser); } @GetMapping("/{id}/file") - public ResponseEntity downloadFile(@PathVariable UUID id) { - return evidenceFileService.downloadFile(id); + public ResponseEntity downloadFile(@PathVariable UUID id, + @AuthenticationPrincipal Account currentUser) { + return evidenceFileService.downloadFile(id, currentUser); } } diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFileRepository.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFileRepository.java index 8d340a2..05370ad 100644 --- a/src/main/java/org/example/crimearchive/evidence/EvidenceFileRepository.java +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFileRepository.java @@ -1,7 +1,10 @@ package org.example.crimearchive.evidence; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -10,8 +13,10 @@ public interface EvidenceFileRepository extends JpaRepository { List findByCaseNumber(String caseNumber); List findByGroupIdOrderByVersionAsc(UUID groupId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) Optional findTopByGroupIdOrderByVersionDesc(UUID groupId); @Query("SELECT e FROM EvidenceFile e WHERE e.caseNumber = :caseNumber AND e.version = (SELECT MAX(e2.version) FROM EvidenceFile e2 WHERE e2.groupId = e.groupId)") - List findLatestVersionsByCaseNumber(String caseNumber); + List findLatestVersionsByCaseNumber(@Param("caseNumber") String caseNumber); } diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFileResponse.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFileResponse.java new file mode 100644 index 0000000..1de5d61 --- /dev/null +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFileResponse.java @@ -0,0 +1,21 @@ +package org.example.crimearchive.evidence; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record EvidenceFileResponse( + UUID id, + UUID groupId, + int version, + String caseNumber, + String originalFilename, + String uploadedBy, + LocalDateTime uploadedAt +) { + static EvidenceFileResponse from(EvidenceFile e) { + return new EvidenceFileResponse( + e.getId(), e.getGroupId(), e.getVersion(), e.getCaseNumber(), + e.getOriginalFilename(), e.getUploadedBy(), e.getUploadedAt() + ); + } +} diff --git a/src/main/java/org/example/crimearchive/evidence/EvidenceFileService.java b/src/main/java/org/example/crimearchive/evidence/EvidenceFileService.java index 23a7778..ece2c63 100644 --- a/src/main/java/org/example/crimearchive/evidence/EvidenceFileService.java +++ b/src/main/java/org/example/crimearchive/evidence/EvidenceFileService.java @@ -4,11 +4,17 @@ import com.itextpdf.text.Image; import com.itextpdf.text.Paragraph; import com.itextpdf.text.pdf.PdfWriter; +import org.example.crimearchive.permissions.PermissionService; +import org.example.crimearchive.polis.Account; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; @@ -19,6 +25,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -28,13 +35,17 @@ public class EvidenceFileService { private final EvidenceFileRepository evidenceFileRepository; private final S3Client s3Client; + private final PermissionService permissionService; @Value("${minio.bucket}") private String bucket; - public EvidenceFileService(EvidenceFileRepository evidenceFileRepository, S3Client s3Client) { + public EvidenceFileService(EvidenceFileRepository evidenceFileRepository, + S3Client s3Client, + PermissionService permissionService) { this.evidenceFileRepository = evidenceFileRepository; this.s3Client = s3Client; + this.permissionService = permissionService; } public void upload(String caseNumber, String reportName, String reportEvent, @@ -42,6 +53,7 @@ public void upload(String caseNumber, String reportName, String reportEvent, upload(caseNumber, reportName, reportEvent, file, uploadedBy, null); } + @Transactional public void upload(String caseNumber, String reportName, String reportEvent, MultipartFile file, String uploadedBy, UUID groupId) throws IOException { String s3KeyPdf = null; @@ -85,7 +97,7 @@ public void upload(String caseNumber, String reportName, String reportEvent, ); } - evidenceFileRepository.save(new EvidenceFile( + EvidenceFile evidenceFile = new EvidenceFile( UUID.randomUUID(), resolvedGroupId, nextVersion, @@ -95,7 +107,9 @@ public void upload(String caseNumber, String reportName, String reportEvent, file != null ? file.getOriginalFilename() : null, uploadedBy, LocalDateTime.now() - )); + ); + evidenceFile.setContentType(file != null ? file.getContentType() : null); + evidenceFileRepository.save(evidenceFile); } catch (Exception e) { deleteIfExists(s3KeyPdf); @@ -104,9 +118,11 @@ public void upload(String caseNumber, String reportName, String reportEvent, } } - public ResponseEntity downloadPdf(UUID id) { + public ResponseEntity downloadPdf(UUID id, Account currentUser) { EvidenceFile evidence = evidenceFileRepository.findById(id) - .orElseThrow(() -> new RuntimeException("Bevisfil hittades inte: " + id)); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Bevisfil hittades inte: " + id)); + + requireCaseAccess(evidence.getCaseNumber(), currentUser); ResponseBytes objectBytes = s3Client.getObjectAsBytes( GetObjectRequest.builder() @@ -121,12 +137,14 @@ public ResponseEntity downloadPdf(UUID id) { .body(objectBytes.asByteArray()); } - public ResponseEntity downloadFile(UUID id) { + public ResponseEntity downloadFile(UUID id, Account currentUser) { EvidenceFile evidence = evidenceFileRepository.findById(id) - .orElseThrow(() -> new RuntimeException("Bevisfil hittades inte: " + id)); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Bevisfil hittades inte: " + id)); + + requireCaseAccess(evidence.getCaseNumber(), currentUser); if (evidence.getS3KeyFile() == null) { - throw new RuntimeException("Ingen bifogad fil finns för denna post"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ingen bifogad fil finns för denna post"); } ResponseBytes objectBytes = s3Client.getObjectAsBytes( @@ -136,8 +154,19 @@ public ResponseEntity downloadFile(UUID id) { .build() ); + String contentDisposition = ContentDisposition.attachment() + .filename(evidence.getOriginalFilename() != null ? evidence.getOriginalFilename() : "fil", + StandardCharsets.UTF_8) + .build() + .toString(); + + MediaType mediaType = evidence.getContentType() != null + ? MediaType.parseMediaType(evidence.getContentType()) + : MediaType.APPLICATION_OCTET_STREAM; + return ResponseEntity.ok() - .header("Content-Disposition", "attachment; filename=" + evidence.getOriginalFilename()) + .contentType(mediaType) + .header("Content-Disposition", contentDisposition) .body(objectBytes.asByteArray()); } @@ -149,8 +178,18 @@ public List getLatestVersionsByCaseNumber(String caseNumber) { return evidenceFileRepository.findLatestVersionsByCaseNumber(caseNumber); } - public List getVersionHistory(UUID groupId) { - return evidenceFileRepository.findByGroupIdOrderByVersionAsc(groupId); + public List getVersionHistory(UUID groupId, Account currentUser) { + List history = evidenceFileRepository.findByGroupIdOrderByVersionAsc(groupId); + if (!history.isEmpty()) { + requireCaseAccess(history.get(0).getCaseNumber(), currentUser); + } + return history; + } + + private void requireCaseAccess(String caseNumber, Account currentUser) { + if (!permissionService.canAccessCase(caseNumber, currentUser)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Åtkomst nekad till ärende: " + caseNumber); + } } private byte[] generatePdf(String reportName, String reportEvent, MultipartFile file) throws Exception { diff --git a/src/test/java/org/example/crimearchive/service/EvidenceFileServiceTest.java b/src/test/java/org/example/crimearchive/service/EvidenceFileServiceTest.java index f083e11..831264e 100644 --- a/src/test/java/org/example/crimearchive/service/EvidenceFileServiceTest.java +++ b/src/test/java/org/example/crimearchive/service/EvidenceFileServiceTest.java @@ -3,6 +3,8 @@ import org.example.crimearchive.evidence.EvidenceFile; import org.example.crimearchive.evidence.EvidenceFileRepository; import org.example.crimearchive.evidence.EvidenceFileService; +import org.example.crimearchive.permissions.PermissionService; +import org.example.crimearchive.polis.Account; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -13,6 +15,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.server.ResponseStatusException; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; @@ -27,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -36,6 +40,10 @@ class EvidenceFileServiceTest { private EvidenceFileRepository evidenceFileRepository; @Mock private S3Client s3Client; + @Mock + private PermissionService permissionService; + @Mock + private Account mockUser; @InjectMocks private EvidenceFileService evidenceFileService; @@ -114,12 +122,13 @@ void downloadPdf_exists_returnsPdfWith200() { UUID id = UUID.randomUUID(); EvidenceFile ev = new EvidenceFile(id, UUID.randomUUID(), 1, "K-2026-000001", "cases/K-2026-000001/pdf/test.pdf", null, null, "officer1", null); when(evidenceFileRepository.findById(id)).thenReturn(Optional.of(ev)); + when(permissionService.canAccessCase(anyString(), any())).thenReturn(true); ResponseBytes mockBytes = mock(ResponseBytes.class); when(mockBytes.asByteArray()).thenReturn("pdf-content".getBytes()); when(s3Client.getObjectAsBytes(any(GetObjectRequest.class))).thenReturn(mockBytes); - ResponseEntity response = evidenceFileService.downloadPdf(id); + ResponseEntity response = evidenceFileService.downloadPdf(id, mockUser); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); @@ -130,7 +139,19 @@ void downloadPdf_notFound_throwsException() { UUID id = UUID.randomUUID(); when(evidenceFileRepository.findById(id)).thenReturn(Optional.empty()); - assertThrows(RuntimeException.class, () -> evidenceFileService.downloadPdf(id)); + assertThrows(ResponseStatusException.class, () -> evidenceFileService.downloadPdf(id, mockUser)); + } + + @Test + void downloadPdf_forbidden_throwsException() { + UUID id = UUID.randomUUID(); + EvidenceFile ev = new EvidenceFile(id, UUID.randomUUID(), 1, "K-2026-000001", "cases/K-2026-000001/pdf/test.pdf", null, null, "officer1", null); + when(evidenceFileRepository.findById(id)).thenReturn(Optional.of(ev)); + when(permissionService.canAccessCase(anyString(), any())).thenReturn(false); + + ResponseStatusException ex = assertThrows(ResponseStatusException.class, + () -> evidenceFileService.downloadPdf(id, mockUser)); + assertEquals(HttpStatus.FORBIDDEN, ex.getStatusCode()); } @Test @@ -140,12 +161,13 @@ void downloadFile_exists_returnsWith200() { "cases/K-2026-000001/pdf/test.pdf", "cases/K-2026-000001/files/evidence.jpg", "evidence.jpg", "officer1", null); when(evidenceFileRepository.findById(id)).thenReturn(Optional.of(ev)); + when(permissionService.canAccessCase(anyString(), any())).thenReturn(true); ResponseBytes mockBytes = mock(ResponseBytes.class); when(mockBytes.asByteArray()).thenReturn("file-content".getBytes()); when(s3Client.getObjectAsBytes(any(GetObjectRequest.class))).thenReturn(mockBytes); - ResponseEntity response = evidenceFileService.downloadFile(id); + ResponseEntity response = evidenceFileService.downloadFile(id, mockUser); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); @@ -157,7 +179,10 @@ void downloadFile_noFileKey_throwsException() { EvidenceFile ev = new EvidenceFile(id, UUID.randomUUID(), 1, "K-2026-000001", "cases/K-2026-000001/pdf/test.pdf", null, null, "officer1", null); when(evidenceFileRepository.findById(id)).thenReturn(Optional.of(ev)); + when(permissionService.canAccessCase(anyString(), any())).thenReturn(true); - assertThrows(RuntimeException.class, () -> evidenceFileService.downloadFile(id)); + ResponseStatusException ex = assertThrows(ResponseStatusException.class, + () -> evidenceFileService.downloadFile(id, mockUser)); + assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); } }