From 042e9527d28a3d5b6f919d127217b960359741a9 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Mon, 27 Apr 2026 13:21:10 +0200 Subject: [PATCH 1/4] Implemented case file management for CaseOfficer role including upload, download, and delete functionality. --- .../team4you/config/SecurityConfig.java | 7 +- .../controller/CaseOfficerController.java | 103 +++++++++++++++++- .../templates/case-officer-layout.html | 1 + .../case-file-list-officer.html | 77 +++++++++++++ .../fragments/case-officer-cases.html | 25 +++-- 5 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/templates/fragments/case-management/case-file-list-officer.html diff --git a/src/main/java/backendlab/team4you/config/SecurityConfig.java b/src/main/java/backendlab/team4you/config/SecurityConfig.java index dcf9c160..93ac720a 100644 --- a/src/main/java/backendlab/team4you/config/SecurityConfig.java +++ b/src/main/java/backendlab/team4you/config/SecurityConfig.java @@ -31,7 +31,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthenticationSuccessHandler successHandler) throws Exception { return http - .csrf(csrf -> csrf.ignoringRequestMatchers("/webauthn/**", "/api/files/**")) + .csrf(csrf -> csrf.ignoringRequestMatchers("/webauthn/**", "/api/files/**", "/case-officer/cases/**")) .authorizeHttpRequests( authorizeHttp -> authorizeHttp // Public endpoints @@ -43,6 +43,11 @@ SecurityFilterChain securityFilterChain(HttpSecurity http, .requestMatchers(HttpMethod.GET, "/api/files/download/**").hasRole(ADMIN) .requestMatchers(HttpMethod.DELETE, "/api/files/delete/**").hasRole(ADMIN) + .requestMatchers(HttpMethod.POST, "/case-officer/cases/**").hasRole(CASE_OFFICER) + .requestMatchers(HttpMethod.GET, "/case-officer/cases/**").hasRole(CASE_OFFICER) + .requestMatchers(HttpMethod.DELETE, "/case-officer/cases/**").hasRole(CASE_OFFICER) + + .requestMatchers("/webauthn/**").hasAnyRole("USER", ADMIN, CASE_OFFICER) .requestMatchers("/admin/**").hasRole(ADMIN) .requestMatchers("/case-officer/**").hasRole(CASE_OFFICER) diff --git a/src/main/java/backendlab/team4you/controller/CaseOfficerController.java b/src/main/java/backendlab/team4you/controller/CaseOfficerController.java index f63d86db..51816cf1 100644 --- a/src/main/java/backendlab/team4you/controller/CaseOfficerController.java +++ b/src/main/java/backendlab/team4you/controller/CaseOfficerController.java @@ -1,25 +1,35 @@ package backendlab.team4you.controller; +import backendlab.team4you.casefile.CaseFile; +import backendlab.team4you.casefile.CaseFileListItemDto; +import backendlab.team4you.casefile.CaseFileService; import backendlab.team4you.caserecord.CaseRecord; import backendlab.team4you.caserecord.CaseRecordRepository; import backendlab.team4you.caserecord.CaseRecordService; import backendlab.team4you.caserecord.CaseStatus; +import backendlab.team4you.common.ConfidentialityLevel; import backendlab.team4you.exceptions.CaseRecordNotFoundException; import backendlab.team4you.exceptions.UserNotFoundException; import backendlab.team4you.user.UserEntity; import backendlab.team4you.user.UserRepository; +import backendlab.team4you.user.UserService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import org.springframework.http.HttpStatus; +import org.springframework.http.*; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.List; @Controller @PreAuthorize("hasRole('CASE_OFFICER')") @@ -27,10 +37,14 @@ public class CaseOfficerController { private final CaseRecordRepository caseRecordRepository; private final UserRepository userRepository; + private final UserService userService; + private final CaseFileService caseFileService; - public CaseOfficerController(CaseRecordRepository caseRecordRepository, UserRepository userRepository) { + public CaseOfficerController(CaseRecordRepository caseRecordRepository, UserRepository userRepository, UserService userService, CaseFileService caseFileService) { this.caseRecordRepository = caseRecordRepository; this.userRepository = userRepository; + this.userService = userService; + this.caseFileService = caseFileService; } @GetMapping("/case-officer") @@ -75,4 +89,81 @@ public String closeCase(@RequestParam String id, Authentication auth) { return ""; } + @GetMapping("/case-officer/cases/{caseRecordId}/files") + public String getCaseFiles(@PathVariable Long caseRecordId, Principal principal, Model model) { + UserEntity currentUser = userService.getCurrentUser(principal); + List files = caseFileService.listFileItemsForViewer(caseRecordId, currentUser); + + model.addAttribute("files", files); + model.addAttribute("caseRecordId", caseRecordId); + + return "fragments/case-management/case-file-list-officer :: caseFileList"; + } + + + @PostMapping("/case-officer/cases/{caseRecordId}/files") + public String uploadFile( + @PathVariable Long caseRecordId, + @RequestParam("file") MultipartFile file, + @RequestParam("confidentialityLevel") ConfidentialityLevel confidentialityLevel, + Principal principal, + Model model + ) throws IOException { + UserEntity currentUser = userService.getCurrentUser(principal); + + try { + caseFileService.uploadFile(caseRecordId, file, confidentialityLevel, currentUser); + model.addAttribute("successMessage", "File uploaded"); + } catch (Exception e) { + model.addAttribute("errorMessage", "Error to upload: " + e.getMessage()); + } + + return getCaseFiles(caseRecordId, principal, model); + } + + @GetMapping("/case-officer/cases/{caseRecordId}/files/{fileId}") + public ResponseEntity downloadFile( + @PathVariable Long caseRecordId, + @PathVariable Long fileId, + Principal principal + ) { + UserEntity currentUser = userService.getCurrentUser(principal); + CaseFile caseFile = caseFileService.getCaseFileForViewer(caseRecordId, fileId, currentUser); + + MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; + if (caseFile.getContentType() != null && !caseFile.getContentType().isBlank()) { + mediaType = MediaType.parseMediaType(caseFile.getContentType()); + } + + StreamingResponseBody body = outputStream -> { + try (InputStream stream = caseFileService.downloadFile(caseRecordId, fileId, currentUser)) { + stream.transferTo(outputStream); + } + }; + + return ResponseEntity.ok() + .header( + HttpHeaders.CONTENT_DISPOSITION, + ContentDisposition.attachment() + .filename(caseFile.getOriginalFilename(), StandardCharsets.UTF_8) + .build() + .toString() + ) + .contentType(mediaType) + .body(body); + } + + @DeleteMapping("/case-officer/cases/{caseRecordId}/files/{fileId}") + @ResponseBody + public ResponseEntity deleteFile( + @PathVariable Long caseRecordId, + @PathVariable Long fileId, + Principal principal + ) { + + UserEntity currentUser = userService.getCurrentUser(principal); + caseFileService.deleteFile(caseRecordId, fileId, currentUser); + + return ResponseEntity.ok().build(); + } } diff --git a/src/main/resources/templates/case-officer-layout.html b/src/main/resources/templates/case-officer-layout.html index 30d17265..4cf58efb 100644 --- a/src/main/resources/templates/case-officer-layout.html +++ b/src/main/resources/templates/case-officer-layout.html @@ -9,6 +9,7 @@ + diff --git a/src/main/resources/templates/fragments/case-management/case-file-list-officer.html b/src/main/resources/templates/fragments/case-management/case-file-list-officer.html new file mode 100644 index 00000000..794c2370 --- /dev/null +++ b/src/main/resources/templates/fragments/case-management/case-file-list-officer.html @@ -0,0 +1,77 @@ +
+ +

Filer

+ + + + + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
    +
  • Inga filer uppladdade ännu.
  • + +
  • +
    + KS26-1-1 + + dokument.pdf + + 🔒 +
    + +
    + + Ladda ner + + + + Ingen behörighet + + + +
    +
  • +
+
diff --git a/src/main/resources/templates/fragments/case-officer-cases.html b/src/main/resources/templates/fragments/case-officer-cases.html index a093c9a3..79841541 100644 --- a/src/main/resources/templates/fragments/case-officer-cases.html +++ b/src/main/resources/templates/fragments/case-officer-cases.html @@ -25,9 +25,8 @@

Mina ärenden

-
Beskrivning
-

-

+

Beskrivning

+

Registry: KS

@@ -35,17 +34,29 @@
Beskrivning

Ägare: Admin Name

-
- + + + From d432a09261ee2b84544108d3428c920b4b132a2e Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Mon, 27 Apr 2026 15:24:55 +0200 Subject: [PATCH 2/4] Fix misleading throws IOException and awkward error message. Return the updated file list fragment instead of an empty body. --- .../controller/CaseOfficerController.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/backendlab/team4you/controller/CaseOfficerController.java b/src/main/java/backendlab/team4you/controller/CaseOfficerController.java index 51816cf1..a297442a 100644 --- a/src/main/java/backendlab/team4you/controller/CaseOfficerController.java +++ b/src/main/java/backendlab/team4you/controller/CaseOfficerController.java @@ -108,14 +108,14 @@ public String uploadFile( @RequestParam("confidentialityLevel") ConfidentialityLevel confidentialityLevel, Principal principal, Model model - ) throws IOException { + ) { UserEntity currentUser = userService.getCurrentUser(principal); try { caseFileService.uploadFile(caseRecordId, file, confidentialityLevel, currentUser); - model.addAttribute("successMessage", "File uploaded"); + model.addAttribute("successMessage", "Filen laddades upp"); } catch (Exception e) { - model.addAttribute("errorMessage", "Error to upload: " + e.getMessage()); + model.addAttribute("errorMessage", "Kunde inte ladda upp filen: " + e.getMessage()); } return getCaseFiles(caseRecordId, principal, model); @@ -154,16 +154,20 @@ public ResponseEntity downloadFile( } @DeleteMapping("/case-officer/cases/{caseRecordId}/files/{fileId}") - @ResponseBody - public ResponseEntity deleteFile( + public String deleteFile( @PathVariable Long caseRecordId, @PathVariable Long fileId, - Principal principal + Principal principal, + Model model ) { UserEntity currentUser = userService.getCurrentUser(principal); - caseFileService.deleteFile(caseRecordId, fileId, currentUser); - - return ResponseEntity.ok().build(); + try { + caseFileService.deleteFile(caseRecordId, fileId, currentUser); + model.addAttribute("successMessage", "Filen togs bort"); + } catch (Exception e) { + model.addAttribute("errorMessage", "Kunde inte ta bort filen: " + e.getMessage()); + } + return getCaseFiles(caseRecordId, principal, model); } } From 16c63eb346a34c434b7208b3409ad9ad61042ef9 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Mon, 27 Apr 2026 15:25:14 +0200 Subject: [PATCH 3/4] Fix disabling CSRF on state-changing officer endpoints is a security regression. --- src/main/java/backendlab/team4you/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/backendlab/team4you/config/SecurityConfig.java b/src/main/java/backendlab/team4you/config/SecurityConfig.java index 93ac720a..4145846e 100644 --- a/src/main/java/backendlab/team4you/config/SecurityConfig.java +++ b/src/main/java/backendlab/team4you/config/SecurityConfig.java @@ -31,7 +31,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthenticationSuccessHandler successHandler) throws Exception { return http - .csrf(csrf -> csrf.ignoringRequestMatchers("/webauthn/**", "/api/files/**", "/case-officer/cases/**")) + .csrf(csrf -> csrf.ignoringRequestMatchers("/webauthn/**", "/api/files/**")) .authorizeHttpRequests( authorizeHttp -> authorizeHttp // Public endpoints From 9fa3d01eb90568dbc47481d1924223bfe01eb1ad Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Mon, 27 Apr 2026 16:09:48 +0200 Subject: [PATCH 4/4] Configured HTMX CSRF protection and dynamic fragment rendering in CaseOfficerController --- .../controller/CaseOfficerController.java | 6 +++++- .../resources/templates/case-officer-layout.html | 16 +++++++++++++--- .../case-management/case-file-list-officer.html | 3 ++- .../fragments/case-officer-sidenav.html | 4 +++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/backendlab/team4you/controller/CaseOfficerController.java b/src/main/java/backendlab/team4you/controller/CaseOfficerController.java index a297442a..542d795d 100644 --- a/src/main/java/backendlab/team4you/controller/CaseOfficerController.java +++ b/src/main/java/backendlab/team4you/controller/CaseOfficerController.java @@ -55,6 +55,7 @@ public String caseOfficerHome() { @GetMapping("/case-officer/cases") public String listCases( @RequestParam(defaultValue = "0") int page, + @RequestHeader(value = "HX-Request", required = false) String htmxRequest, Authentication auth, Model model ) { @@ -68,7 +69,10 @@ public String listCases( model.addAttribute("currentPage", page); model.addAttribute("totalPages", cases.getTotalPages()); - return "fragments/case-officer-cases :: content"; + if (htmxRequest != null) { + return "fragments/case-officer-cases :: content"; + } + return "case-officer-cases"; } @PostMapping("/case-officer/cases/close") diff --git a/src/main/resources/templates/case-officer-layout.html b/src/main/resources/templates/case-officer-layout.html index 4cf58efb..7b217949 100644 --- a/src/main/resources/templates/case-officer-layout.html +++ b/src/main/resources/templates/case-officer-layout.html @@ -9,9 +9,9 @@ - - + + @@ -24,7 +24,17 @@

Välkommen Handläggare

- + + + diff --git a/src/main/resources/templates/fragments/case-management/case-file-list-officer.html b/src/main/resources/templates/fragments/case-management/case-file-list-officer.html index 794c2370..b5abf039 100644 --- a/src/main/resources/templates/fragments/case-management/case-file-list-officer.html +++ b/src/main/resources/templates/fragments/case-management/case-file-list-officer.html @@ -18,6 +18,8 @@

Filer

hx-target="closest .case-file-panel" hx-swap="outerHTML"> + +
@@ -65,7 +67,6 @@

Filer