-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/upload, delete and download files for case officer #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
042e952
d432a09
16c63eb
9fa3d01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,36 +1,50 @@ | ||
| 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')") | ||
| 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") | ||
|
|
@@ -41,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 | ||
| ) { | ||
|
|
@@ -54,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") | ||
|
|
@@ -75,4 +93,85 @@ 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<CaseFileListItemDto> 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 | ||
| ) { | ||
| UserEntity currentUser = userService.getCurrentUser(principal); | ||
|
|
||
| try { | ||
| caseFileService.uploadFile(caseRecordId, file, confidentialityLevel, currentUser); | ||
| model.addAttribute("successMessage", "Filen laddades upp"); | ||
| } catch (Exception e) { | ||
| model.addAttribute("errorMessage", "Kunde inte ladda upp filen: " + e.getMessage()); | ||
| } | ||
|
|
||
| return getCaseFiles(caseRecordId, principal, model); | ||
| } | ||
|
|
||
| @GetMapping("/case-officer/cases/{caseRecordId}/files/{fileId}") | ||
| public ResponseEntity<StreamingResponseBody> 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); | ||
| } | ||
|
Comment on lines
+128
to
+158
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm visibility/signature of getCaseFileForViewer on CaseFileService.
fd -t f 'CaseFileService.java' --exec rg -nP '(?m)^\s*(public|protected|private)\s+\S+\s+getCaseFileForViewer\s*\(' {}Repository: ithsjava25/project-backend-team4you Length of output: 177 🏁 Script executed: fd -t f 'CaseFileService.java' --exec cat -n {} \; | head -300Repository: ithsjava25/project-backend-team4you Length of output: 11762 Redundant authorization and DB lookup during file download.
🤖 Prompt for AI Agents |
||
|
|
||
| @DeleteMapping("/case-officer/cases/{caseRecordId}/files/{fileId}") | ||
| public String deleteFile( | ||
| @PathVariable Long caseRecordId, | ||
| @PathVariable Long fileId, | ||
| Principal principal, | ||
| Model model | ||
| ) { | ||
|
|
||
| UserEntity currentUser = userService.getCurrentUser(principal); | ||
| 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); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||||||||||||||||
| <div th:fragment="caseFileList" class="case-file-panel"> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <h3>Filer</h3> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <p th:if="${successMessage}" | ||||||||||||||||||||||||||||||||||||
| th:text="${successMessage}" | ||||||||||||||||||||||||||||||||||||
| class="feedback-message success-message"></p> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <p th:if="${errorMessage}" | ||||||||||||||||||||||||||||||||||||
| th:text="${errorMessage}" | ||||||||||||||||||||||||||||||||||||
| class="feedback-message error-message"></p> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <form th:action="@{/case-officer/cases/{caseId}/files(caseId=${caseRecordId})}" | ||||||||||||||||||||||||||||||||||||
| th:attr="hx-post=@{/case-officer/cases/{caseId}/files(caseId=${caseRecordId})}" | ||||||||||||||||||||||||||||||||||||
| method="post" | ||||||||||||||||||||||||||||||||||||
| enctype="multipart/form-data" | ||||||||||||||||||||||||||||||||||||
| hx-encoding="multipart/form-data" | ||||||||||||||||||||||||||||||||||||
| hx-target="closest .case-file-panel" | ||||||||||||||||||||||||||||||||||||
| hx-swap="outerHTML"> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <div class="form-group"> | ||||||||||||||||||||||||||||||||||||
| <label for="case-file-upload">Ladda upp fil</label> | ||||||||||||||||||||||||||||||||||||
| <input id="case-file-upload" type="file" name="file" required> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <div class="form-group"> | ||||||||||||||||||||||||||||||||||||
| <label for="file-confidentiality-officer">Sekretessnivå</label> | ||||||||||||||||||||||||||||||||||||
| <select id="file-confidentiality-officer" name="confidentialityLevel"> | ||||||||||||||||||||||||||||||||||||
| <option value="OPEN">Ingen sekretess</option> | ||||||||||||||||||||||||||||||||||||
| <option value="CONFIDENTIAL">Sekretess</option> | ||||||||||||||||||||||||||||||||||||
| </select> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <button type="submit" class="btn-primary">Ladda upp</button> | ||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <ul class="case-file-list"> | ||||||||||||||||||||||||||||||||||||
| <li th:if="${#lists.isEmpty(files)}">Inga filer uppladdade ännu.</li> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <li th:each="file : ${files}" class="case-file-item file-row"> | ||||||||||||||||||||||||||||||||||||
| <div class="case-file-info"> | ||||||||||||||||||||||||||||||||||||
| <span class="case-file-reference" | ||||||||||||||||||||||||||||||||||||
| th:text="${file.documentReference}">KS26-1-1</span> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <span class="case-file-name" | ||||||||||||||||||||||||||||||||||||
| th:text="${file.displayName}">dokument.pdf</span> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <span class="case-file-lock" | ||||||||||||||||||||||||||||||||||||
| th:if="${file.confidential}" | ||||||||||||||||||||||||||||||||||||
| title="Sekretessbelagd fil">🔒</span> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <div class="case-file-actions file-actions"> | ||||||||||||||||||||||||||||||||||||
| <a th:if="${file.canDownload}" | ||||||||||||||||||||||||||||||||||||
| th:href="@{/case-officer/cases/{caseId}/files/{fileId}(caseId=${caseRecordId}, fileId=${file.id})}" | ||||||||||||||||||||||||||||||||||||
| class="btn-secondary"> | ||||||||||||||||||||||||||||||||||||
| Ladda ner | ||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <span th:unless="${file.canDownload}" | ||||||||||||||||||||||||||||||||||||
| class="case-file-restricted-text"> | ||||||||||||||||||||||||||||||||||||
| Ingen behörighet | ||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <button type="button" | ||||||||||||||||||||||||||||||||||||
| class="btn-danger" | ||||||||||||||||||||||||||||||||||||
| th:attr="hx-delete=@{/case-officer/cases/{caseId}/files/{fileId}(caseId=${caseRecordId}, fileId=${file.id})}" | ||||||||||||||||||||||||||||||||||||
| hx-target="closest .case-file-panel" | ||||||||||||||||||||||||||||||||||||
| hx-swap="outerHTML" | ||||||||||||||||||||||||||||||||||||
| hx-confirm="Är du säker på att du vill ta bort denna fil?"> | ||||||||||||||||||||||||||||||||||||
| Ta bort | ||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delete swaps in an empty body, erasing the file panel. The HTMX delete uses Either re-render the file list fragment from the delete endpoint (recommended, mirrors the upload flow) or change 🐛 Recommended template-side variant (if you keep an empty 200 response)- <button type="button"
- class="btn-danger"
- th:attr="hx-delete=@{/case-officer/cases/{caseId}/files/{fileId}(caseId=${caseRecordId}, fileId=${file.id})}"
- th:hx-headers="${_csrf != null} ? '{"' + ${_csrf.headerName} + '":"' + ${_csrf.token} + '"}' : null"
- hx-target="closest .case-file-panel"
- hx-swap="outerHTML"
- hx-confirm="Är du säker på att du vill ta bort denna fil?">
+ <button type="button"
+ class="btn-danger"
+ th:attr="hx-delete=@{/case-officer/cases/{caseId}/files/{fileId}(caseId=${caseRecordId}, fileId=${file.id})}"
+ hx-target="closest .case-file-item"
+ hx-swap="outerHTML swap:200ms"
+ hx-confirm="Är du säker på att du vill ta bort denna fil?">The preferred fix is on the controller side — return the same 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||
| </ul> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: misleading
throws IOExceptionand awkward error message.uploadFiledeclaresthrows IOException, but every checked exception path is swallowed by thecatch (Exception e), so the declaration can never trigger. Also, the user‑facing string"Error to upload: "reads awkwardly — consider"Kunde inte ladda upp filen: "to match the rest of the Swedish UI in this template.Additionally, broad
catch (Exception)here will mask real problems (e.g. authorization failures fromCaseFileAccessService) by surfacing their raw messages to the end user. Consider catching the specific exception types you expect from the service.♻️ Suggested cleanup
🤖 Prompt for AI Agents