${escapeHtml(i.subject)}
${new Date(i.createdAt).toLocaleDateString()}
diff --git a/src/main/resources/templates/incidents.html b/src/main/resources/templates/incidents.html
index 7175ee9..2b88966 100644
--- a/src/main/resources/templates/incidents.html
+++ b/src/main/resources/templates/incidents.html
@@ -170,7 +170,7 @@
Active incidents
}
function viewIncident(id) {
- window.location.href = `/incidents/${id}`;
+ window.location.href = `/incident/${id}`;
}
function escapeHtml(str) {
From 2b9c29c625caa5eb41fb6e6ecc8e034f1bcdb09a Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Thu, 9 Apr 2026 14:56:28 +0200
Subject: [PATCH 02/11] Run mvn spotless
---
.../team6backend/config/MinioConfig.java | 4 +--
.../document/entity/Document.java | 8 +++---
.../document/service/S3Service.java | 26 +++++++++----------
3 files changed, 18 insertions(+), 20 deletions(-)
diff --git a/src/main/java/org/example/team6backend/config/MinioConfig.java b/src/main/java/org/example/team6backend/config/MinioConfig.java
index d94e3df..144b04e 100644
--- a/src/main/java/org/example/team6backend/config/MinioConfig.java
+++ b/src/main/java/org/example/team6backend/config/MinioConfig.java
@@ -19,6 +19,6 @@ public class MinioConfig {
@Bean
public MinioClient minioClient() {
- return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
- }
+ return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
+ }
}
diff --git a/src/main/java/org/example/team6backend/document/entity/Document.java b/src/main/java/org/example/team6backend/document/entity/Document.java
index daca6c2..d7e3f43 100644
--- a/src/main/java/org/example/team6backend/document/entity/Document.java
+++ b/src/main/java/org/example/team6backend/document/entity/Document.java
@@ -10,13 +10,13 @@ public class Document {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- @Column(name = "file_name")
+ @Column(name = "file_name")
private String fileName;
- @Column(name = "content_type")
+ @Column(name = "content_type")
private String contentType;
- @Column(name = "file_key")
+ @Column(name = "file_key")
private String fileKey;
- @Column(name = "file_size")
+ @Column(name = "file_size")
private Long fileSize;
@ManyToOne
diff --git a/src/main/java/org/example/team6backend/document/service/S3Service.java b/src/main/java/org/example/team6backend/document/service/S3Service.java
index 5458b43..c1ed209 100644
--- a/src/main/java/org/example/team6backend/document/service/S3Service.java
+++ b/src/main/java/org/example/team6backend/document/service/S3Service.java
@@ -18,20 +18,18 @@ public class S3Service {
@Value("${minio.bucket}")
private String bucketName;
- @PostConstruct
- public void init() {
- try {
- boolean exists = minioClient.bucketExists(
- BucketExistsArgs.builder().bucket(bucketName).build());
-
- if (!exists) {
- minioClient.makeBucket(
- MakeBucketArgs.builder().bucket(bucketName).build());
- }
- } catch (Exception e) {
- throw new RuntimeException("Could not initialize minio service", e);
- }
- }
+ @PostConstruct
+ public void init() {
+ try {
+ boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
+
+ if (!exists) {
+ minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Could not initialize minio service", e);
+ }
+ }
/** Upload file to MinIO */
public void uploadFile(String fileKey, MultipartFile file) {
From 94faf630da0fe30c3074b49463230a32caddb34c Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Fri, 10 Apr 2026 09:37:52 +0200
Subject: [PATCH 03/11] Fix file handling edge cases and improve deletion logic
- Fixed multiple file upload handling by changing controller to accept List
- Added validation to skip empty files during upload
- Fixed path variable mismatch in DocumentController download endpoint
- Prevented IndexOutOfBoundsException when no documents exist for an incident
- Updated redirects to consistently use /incidents/{id}
- Added proper deletion flow to ensure files are removed from MinIO/S3 via DocumentService
- Improved robustness of file handling and error handling across controllers
---
pom.xml | 2 +-
.../document/controller/DocumentController.java | 8 ++++++--
.../document/service/DocumentService.java | 2 +-
.../team6backend/incident/entity/Incident.java | 13 +++++++++++--
.../incident/service/IncidentService.java | 15 ++++++++++++++-
.../example/team6backend/page/PageController.java | 10 ++++++++--
src/main/resources/templates/dashboard.html | 2 +-
7 files changed, 42 insertions(+), 10 deletions(-)
diff --git a/pom.xml b/pom.xml
index 04ee113..62760ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,7 +107,7 @@
com.squareup.okhttp3
okhttp
- 4.11.0
+ 5.1.0
diff --git a/src/main/java/org/example/team6backend/document/controller/DocumentController.java b/src/main/java/org/example/team6backend/document/controller/DocumentController.java
index 35c3b43..159893b 100644
--- a/src/main/java/org/example/team6backend/document/controller/DocumentController.java
+++ b/src/main/java/org/example/team6backend/document/controller/DocumentController.java
@@ -45,13 +45,17 @@ public String uploadFile(@PathVariable Long incidentId, @RequestParam("files") L
return "redirect:/incidents/" + incidentId;
}
- @GetMapping("/download/{id}")
+ @GetMapping("/download/{incidentId}")
public ResponseEntity downloadFile(@PathVariable Long incidentId,
@AuthenticationPrincipal CustomUserDetails userDetails) {
AppUser user = userDetails.getUser();
Incident incident = incidentService.getById(incidentId, user);
- Document document = documentService.getDocumentsByIncident(incident).get(0);
+ List documents = documentService.getDocumentsByIncident(incident);
+ if (documents.isEmpty()) {
+ return ResponseEntity.notFound().build();
+ }
+ Document document = documents.get(0);
InputStream stream = documentService.downloadFile(document.getFileKey());
diff --git a/src/main/java/org/example/team6backend/document/service/DocumentService.java b/src/main/java/org/example/team6backend/document/service/DocumentService.java
index e78d383..f15a71d 100644
--- a/src/main/java/org/example/team6backend/document/service/DocumentService.java
+++ b/src/main/java/org/example/team6backend/document/service/DocumentService.java
@@ -20,7 +20,7 @@ public class DocumentService {
/** Upload file */
public Document uploadFile(MultipartFile file, Incident incident) {
try {
- String fileKey = UUID.randomUUID() + " _ " + file.getOriginalFilename();
+ String fileKey = UUID.randomUUID() + "_" + file.getOriginalFilename();
s3Service.uploadFile(fileKey, file);
diff --git a/src/main/java/org/example/team6backend/incident/entity/Incident.java b/src/main/java/org/example/team6backend/incident/entity/Incident.java
index 797b4f5..6174c95 100644
--- a/src/main/java/org/example/team6backend/incident/entity/Incident.java
+++ b/src/main/java/org/example/team6backend/incident/entity/Incident.java
@@ -4,6 +4,7 @@
import org.example.team6backend.document.entity.Document;
import org.example.team6backend.user.entity.AppUser;
import java.time.LocalDateTime;
+import java.util.ArrayList;
import java.util.List;
@Entity
@@ -41,8 +42,8 @@ public class Incident {
@Column(name = "updated_at")
private LocalDateTime updatedAt;
- @OneToMany(mappedBy = "incident", cascade = CascadeType.ALL)
- private List documents;
+ @OneToMany(mappedBy = "incident", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List documents = new ArrayList<>();
@PrePersist
protected void onCreate() {
@@ -95,6 +96,10 @@ public LocalDateTime getUpdatedAt() {
return updatedAt;
}
+ public List getDocuments() {
+ return documents;
+ }
+
public void setId(Long id) {
this.id = id;
}
@@ -134,4 +139,8 @@ public void setCreatedAt(LocalDateTime createdAt) {
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
+
+ public void setDocuments(List documents) {
+ this.documents = documents;
+ }
}
diff --git a/src/main/java/org/example/team6backend/incident/service/IncidentService.java b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
index cf88c98..e1a3274 100644
--- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java
+++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
@@ -1,6 +1,8 @@
package org.example.team6backend.incident.service;
import org.example.team6backend.activity.service.ActivityLogService;
+import org.example.team6backend.document.entity.Document;
+import org.example.team6backend.document.service.DocumentService;
import org.example.team6backend.exception.ResourceNotFoundException;
import org.example.team6backend.security.CustomUserDetails;
import org.example.team6backend.user.entity.AppUser;
@@ -18,16 +20,20 @@
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
+import java.util.List;
@Service
public class IncidentService {
private final IncidentRepository incidentRepository;
private final ActivityLogService activityLogService;
+ private final DocumentService documentService;
- public IncidentService(IncidentRepository incidentRepository, ActivityLogService activityLogService) {
+ public IncidentService(IncidentRepository incidentRepository, ActivityLogService activityLogService,
+ DocumentService documentService) {
this.incidentRepository = incidentRepository;
this.activityLogService = activityLogService;
+ this.documentService = documentService;
}
/** Help-method for sorting **/
@@ -83,4 +89,11 @@ public Incident getById(Long id, AppUser user) {
}
return incident;
}
+ public void deleteIncident(Incident incident) {
+ List documents = documentService.getDocumentsByIncident(incident);
+ for (Document document : documents) {
+ documentService.deleteFile(document);
+ }
+ incidentRepository.delete(incident);
+ }
}
diff --git a/src/main/java/org/example/team6backend/page/PageController.java b/src/main/java/org/example/team6backend/page/PageController.java
index 4a6b13c..5119988 100644
--- a/src/main/java/org/example/team6backend/page/PageController.java
+++ b/src/main/java/org/example/team6backend/page/PageController.java
@@ -20,6 +20,8 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
+import java.util.List;
+
@Controller
public class PageController {
private final UserService userService;
@@ -89,7 +91,7 @@ public String createIncident(@AuthenticationPrincipal CustomUserDetails userDeta
@PostMapping("/create-incident")
public String submitIncident(@AuthenticationPrincipal CustomUserDetails userDetails,
@Valid @ModelAttribute IncidentRequest incidentRequest, BindingResult bindingResult,
- @RequestParam(value = "files", required = false) MultipartFile files, Model model) {
+ @RequestParam(value = "files", required = false) List files, Model model) {
AppUser user = userDetails.getUser();
@@ -108,7 +110,11 @@ public String submitIncident(@AuthenticationPrincipal CustomUserDetails userDeta
Incident saved = incidentService.createIncident(incident);
if (files != null && !files.isEmpty()) {
- documentService.uploadFile(files, saved);
+ for (MultipartFile file : files) {
+ if (!file.isEmpty()) {
+ documentService.uploadFile(file, saved);
+ }
+ }
}
return "redirect:/incidents/" + saved.getId();
}
diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html
index 6312f95..694940d 100644
--- a/src/main/resources/templates/dashboard.html
+++ b/src/main/resources/templates/dashboard.html
@@ -484,7 +484,7 @@ Account pending approval
container.innerHTML = 'No incidents yet.
';
} else {
container.innerHTML = incidents.map(i => `
-
+
${escapeHtml(i.subject)}
${new Date(i.createdAt).toLocaleDateString()}
From 570845ea65aab6093182f579435584a9871c6bfb Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:27:11 +0200
Subject: [PATCH 04/11] Added space between two methods that made the quality
check fail, run mvn spotless.
---
.../example/team6backend/incident/service/IncidentService.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/org/example/team6backend/incident/service/IncidentService.java b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
index e1a3274..f208e87 100644
--- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java
+++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
@@ -18,7 +18,6 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
-
import java.time.LocalDateTime;
import java.util.List;
@@ -76,6 +75,7 @@ public Page
findByCreatedBy(AppUser user, Pageable pageable) {
public Page findByAssignedTo(AppUser user, Pageable pageable) {
return incidentRepository.findByAssignedTo(user, withDefaultSort(pageable));
}
+
public Incident getById(Long id, AppUser user) {
Incident incident = incidentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found"));
From b7dcb144b9a5716461842f2b843b63e5cf5cf1b6 Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:46:25 +0200
Subject: [PATCH 05/11] Added space between two methods that made the quality
check fail, run mvn spotless.
---
.../incident/service/IncidentService.java | 11 ++++-------
.../org/example/team6backend/page/PageController.java | 5 ++++-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/main/java/org/example/team6backend/incident/service/IncidentService.java b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
index 040a312..3c3b27f 100644
--- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java
+++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
@@ -33,19 +33,15 @@ public class IncidentService {
private final IncidentRepository incidentRepository;
private final ActivityLogService activityLogService;
private final DocumentService documentService;
-
- public IncidentService(IncidentRepository incidentRepository, ActivityLogService activityLogService,
- DocumentService documentService) {
- this.incidentRepository = incidentRepository;
- this.activityLogService = activityLogService;
- this.documentService = documentService;
private final AppUserRepository userRepository;
private final NotificationService notificationService;
public IncidentService(IncidentRepository incidentRepository, ActivityLogService activityLogService,
- AppUserRepository userRepository, NotificationService notificationService) {
+ DocumentService documentService, AppUserRepository userRepository,
+ NotificationService notificationService) {
this.incidentRepository = incidentRepository;
this.activityLogService = activityLogService;
+ this.documentService = documentService;
this.userRepository = userRepository;
this.notificationService = notificationService;
}
@@ -110,6 +106,7 @@ public void deleteIncident(Incident incident) {
documentService.deleteFile(document);
}
incidentRepository.delete(incident);
+ }
@Transactional
public Incident assignIncidentToHandler(Long incidentId, String handlerId, AppUser currentUser) {
diff --git a/src/main/java/org/example/team6backend/page/PageController.java b/src/main/java/org/example/team6backend/page/PageController.java
index 4c60c49..395d475 100644
--- a/src/main/java/org/example/team6backend/page/PageController.java
+++ b/src/main/java/org/example/team6backend/page/PageController.java
@@ -92,11 +92,14 @@ public String createIncident(@AuthenticationPrincipal CustomUserDetails userDeta
@PostMapping("/create-incident")
public String submitIncident(@AuthenticationPrincipal CustomUserDetails userDetails,
@Valid @ModelAttribute IncidentRequest incidentRequest, BindingResult bindingResult,
- @RequestParam(value = "files", required = false) List files, Model model) {
+ @RequestParam(value = "files", required = false) List files, Model model,
+ HttpServletRequest request) {
AppUser user = userDetails.getUser();
if (bindingResult.hasErrors()) {
+ CsrfToken csrf = (CsrfToken) request.getAttribute("_csrf");
+ model.addAttribute("_csrf", csrf);
model.addAttribute("role", user.getRole().name());
model.addAttribute("user", user);
return "createincident";
From 201e4ddaaa56a600f0470f0c4d68e1bbeb39cb07 Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Fri, 10 Apr 2026 11:01:48 +0200
Subject: [PATCH 06/11] Removed okhttp import in pom.xml
---
pom.xml | 5 -----
1 file changed, 5 deletions(-)
diff --git a/pom.xml b/pom.xml
index 62760ec..8f37f26 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,11 +104,6 @@
spring-security-test
test
-
- com.squareup.okhttp3
- okhttp
- 5.1.0
-
From aa9bde80f35a3866bd8bc255213cfb5c7da9addb Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Fri, 10 Apr 2026 11:11:31 +0200
Subject: [PATCH 07/11] Added correct dependencies for okhttp and mvn clean
install is ok.
---
pom.xml | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/pom.xml b/pom.xml
index 8f37f26..0072ddc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,6 +104,16 @@
spring-security-test
test
+
+ com.squareup.okhttp3
+ okhttp
+ 5.1.0
+
+
+ com.squareup.okhttp3
+ logging-interceptor
+ 5.1.0
+
From 5683c99fed94a2de6e6bdd2aebc735ed795fb5db Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Fri, 10 Apr 2026 14:00:57 +0200
Subject: [PATCH 08/11] refactor: restructure service layer and fix CodeRabbit
feedback
- Move incident creation logic from controller to service layer
- Remove SecurityContext usage from IncidentService for cleaner separation of concerns
- Centralize incident + document creation into a single transactional service method
- Align controller methods with service-based architecture
- Improve file upload handling and ensure proper iteration over multipart files
- Fix potential issues with partial uploads and inconsistent incident state
- Improve deleteIncident logic by handling document cleanup and external storage deletion
- Resolve CodeRabbit findings related to architecture, consistency, and robustness
- Improve overall separation between controller, service, and persistence layers
---
.../document/service/DocumentService.java | 1 -
.../controller/IncidentController.java | 11 +++--
.../incident/entity/Incident.java | 3 +-
.../incident/service/IncidentService.java | 44 +++++++++++++------
.../team6backend/page/PageController.java | 17 +------
.../migration/V8__create_document_table.sql | 1 -
6 files changed, 38 insertions(+), 39 deletions(-)
diff --git a/src/main/java/org/example/team6backend/document/service/DocumentService.java b/src/main/java/org/example/team6backend/document/service/DocumentService.java
index f15a71d..629cdb1 100644
--- a/src/main/java/org/example/team6backend/document/service/DocumentService.java
+++ b/src/main/java/org/example/team6backend/document/service/DocumentService.java
@@ -52,5 +52,4 @@ public void deleteFile(Document document) {
public List getDocumentsByIncident(Incident incidentId) {
return documentRepository.findByIncident(incidentId);
}
-
}
diff --git a/src/main/java/org/example/team6backend/incident/controller/IncidentController.java b/src/main/java/org/example/team6backend/incident/controller/IncidentController.java
index fc29768..0db24db 100644
--- a/src/main/java/org/example/team6backend/incident/controller/IncidentController.java
+++ b/src/main/java/org/example/team6backend/incident/controller/IncidentController.java
@@ -38,13 +38,12 @@ public IncidentController(IncidentService incidentService, UserService userServi
/** Create new incident */
@PostMapping
@PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')")
- public IncidentResponse createIncident(@RequestBody @Valid IncidentRequest incidentRequest) {
- Incident incident = new Incident();
- incident.setSubject(incidentRequest.getSubject());
- incident.setDescription(incidentRequest.getDescription());
- incident.setIncidentCategory(incidentRequest.getIncidentCategory());
+ public IncidentResponse createIncident(@RequestBody @Valid IncidentRequest incidentRequest,
+ @AuthenticationPrincipal CustomUserDetails customUserDetails) {
+ AppUser user = customUserDetails.getUser();
+
+ Incident saved = incidentService.createIncident(incidentRequest, null, user);
- Incident saved = incidentService.createIncident(incident);
return IncidentResponse.fromEntity(saved);
}
diff --git a/src/main/java/org/example/team6backend/incident/entity/Incident.java b/src/main/java/org/example/team6backend/incident/entity/Incident.java
index 6174c95..ec53efd 100644
--- a/src/main/java/org/example/team6backend/incident/entity/Incident.java
+++ b/src/main/java/org/example/team6backend/incident/entity/Incident.java
@@ -4,7 +4,6 @@
import org.example.team6backend.document.entity.Document;
import org.example.team6backend.user.entity.AppUser;
import java.time.LocalDateTime;
-import java.util.ArrayList;
import java.util.List;
@Entity
@@ -43,7 +42,7 @@ public class Incident {
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "incident", cascade = CascadeType.ALL, orphanRemoval = true)
- private List documents = new ArrayList<>();
+ private List documents;
@PrePersist
protected void onCreate() {
diff --git a/src/main/java/org/example/team6backend/incident/service/IncidentService.java b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
index 3c3b27f..089a4dd 100644
--- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java
+++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
@@ -4,9 +4,10 @@
import org.example.team6backend.activity.service.ActivityLogService;
import org.example.team6backend.document.entity.Document;
import org.example.team6backend.document.service.DocumentService;
+import org.example.team6backend.document.service.S3Service;
import org.example.team6backend.exception.ResourceNotFoundException;
+import org.example.team6backend.incident.dto.IncidentRequest;
import org.example.team6backend.notification.service.NotificationService;
-import org.example.team6backend.security.CustomUserDetails;
import org.example.team6backend.user.entity.AppUser;
import org.example.team6backend.incident.entity.Incident;
import org.example.team6backend.incident.entity.IncidentStatus;
@@ -18,10 +19,9 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.List;
@@ -35,15 +35,17 @@ public class IncidentService {
private final DocumentService documentService;
private final AppUserRepository userRepository;
private final NotificationService notificationService;
+ private final S3Service s3Service;
public IncidentService(IncidentRepository incidentRepository, ActivityLogService activityLogService,
- DocumentService documentService, AppUserRepository userRepository,
- NotificationService notificationService) {
+ DocumentService documentService, AppUserRepository userRepository, NotificationService notificationService,
+ S3Service s3Service) {
this.incidentRepository = incidentRepository;
this.activityLogService = activityLogService;
this.documentService = documentService;
this.userRepository = userRepository;
this.notificationService = notificationService;
+ this.s3Service = s3Service;
}
/** Help-method for sorting **/
@@ -55,19 +57,28 @@ private Pageable withDefaultSort(Pageable pageable) {
}
/** Create incident **/
- public Incident createIncident(Incident incident) {
- Authentication auth = SecurityContextHolder.getContext().getAuthentication();
- CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
- AppUser appUser = userDetails.getUser();
+ @Transactional
+ public Incident createIncident(IncidentRequest incidentRequest, List files, AppUser user) {
- incident.setCreatedBy(appUser);
+ Incident incident = new Incident();
+ incident.setSubject(incidentRequest.getSubject());
+ incident.setDescription(incidentRequest.getDescription());
+ incident.setIncidentCategory(incidentRequest.getIncidentCategory());
+ incident.setCreatedBy(user);
incident.setIncidentStatus(IncidentStatus.OPEN);
incident.setCreatedAt(LocalDateTime.now());
incident.setUpdatedAt(LocalDateTime.now());
Incident savedIncident = incidentRepository.save(incident);
- activityLogService.log("INCIDENT_CREATED", appUser.getName() + " created the incident", savedIncident, appUser);
+ if (files != null) {
+ for (MultipartFile file : files) {
+ if (file.isEmpty()) {
+ documentService.uploadFile(file, savedIncident);
+ }
+ }
+ }
+ activityLogService.log("INCIDENT_CREATED", user.getName() + "created incident", savedIncident, user);
return savedIncident;
}
@@ -100,10 +111,15 @@ public Incident getById(Long id, AppUser user) {
}
return incident;
}
+ @Transactional
public void deleteIncident(Incident incident) {
- List documents = documentService.getDocumentsByIncident(incident);
- for (Document document : documents) {
- documentService.deleteFile(document);
+
+ for (Document document : incident.getDocuments()) {
+ try {
+ s3Service.deleteFile(document.getFileKey());
+ } catch (Exception e) {
+ log.warn("Failed to delete file from S3: " + document.getFileKey(), e);
+ }
}
incidentRepository.delete(incident);
}
diff --git a/src/main/java/org/example/team6backend/page/PageController.java b/src/main/java/org/example/team6backend/page/PageController.java
index 395d475..5636378 100644
--- a/src/main/java/org/example/team6backend/page/PageController.java
+++ b/src/main/java/org/example/team6backend/page/PageController.java
@@ -105,21 +105,8 @@ public String submitIncident(@AuthenticationPrincipal CustomUserDetails userDeta
return "createincident";
}
- Incident incident = new Incident();
- incident.setSubject(incidentRequest.getSubject());
- incident.setDescription(incidentRequest.getDescription());
- incident.setIncidentCategory(incidentRequest.getIncidentCategory());
- incident.setCreatedBy(user);
-
- Incident saved = incidentService.createIncident(incident);
-
- if (files != null && !files.isEmpty()) {
- for (MultipartFile file : files) {
- if (!file.isEmpty()) {
- documentService.uploadFile(file, saved);
- }
- }
- }
+ Incident saved = incidentService.createIncident(incidentRequest, files, user);
+
return "redirect:/incidents/" + saved.getId();
}
diff --git a/src/main/resources/db/migration/V8__create_document_table.sql b/src/main/resources/db/migration/V8__create_document_table.sql
index a83d124..f9c45aa 100644
--- a/src/main/resources/db/migration/V8__create_document_table.sql
+++ b/src/main/resources/db/migration/V8__create_document_table.sql
@@ -10,5 +10,4 @@ CREATE TABLE document(
CONSTRAINT fk_document_incident
FOREIGN KEY (incident_id)
REFERENCES incident(id)
- ON DELETE CASCADE
);
\ No newline at end of file
From ca3e31963978fdc820bdf450a0fbe92ac2ae44f7 Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Fri, 10 Apr 2026 14:27:28 +0200
Subject: [PATCH 09/11] refactor: improve file upload consistency and error
handling
- Added S3 cleanup on failed DB save to prevent orphan files
- Guarded cleanup with upload flag
- Moved incident + file handling to transactional flow
- Fixed empty file upload condition
---
.../document/service/DocumentService.java | 17 +++++++++++++++--
.../incident/service/IncidentService.java | 2 +-
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/example/team6backend/document/service/DocumentService.java b/src/main/java/org/example/team6backend/document/service/DocumentService.java
index 629cdb1..dc18701 100644
--- a/src/main/java/org/example/team6backend/document/service/DocumentService.java
+++ b/src/main/java/org/example/team6backend/document/service/DocumentService.java
@@ -1,6 +1,7 @@
package org.example.team6backend.document.service;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.example.team6backend.document.entity.Document;
import org.example.team6backend.document.repository.DocumentRepository;
import org.example.team6backend.incident.entity.Incident;
@@ -10,6 +11,7 @@
import java.util.List;
import java.util.UUID;
+@Slf4j
@Service
@RequiredArgsConstructor
public class DocumentService {
@@ -19,10 +21,13 @@ public class DocumentService {
/** Upload file */
public Document uploadFile(MultipartFile file, Incident incident) {
- try {
- String fileKey = UUID.randomUUID() + "_" + file.getOriginalFilename();
+ String fileKey = UUID.randomUUID() + "_" + file.getOriginalFilename();
+ boolean uploaded = false;
+
+ try {
s3Service.uploadFile(fileKey, file);
+ uploaded = true;
Document document = new Document();
document.setFileName(file.getOriginalFilename());
@@ -32,7 +37,15 @@ public Document uploadFile(MultipartFile file, Incident incident) {
document.setIncident(incident);
return documentRepository.save(document);
+
} catch (Exception e) {
+ if (uploaded) {
+ try {
+ s3Service.deleteFile(fileKey);
+ } catch (Exception cleanupEx) {
+ log.warn("Failed to cleanup S3 file: {}", fileKey, cleanupEx);
+ }
+ }
throw new RuntimeException("File upload failed", e);
}
}
diff --git a/src/main/java/org/example/team6backend/incident/service/IncidentService.java b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
index 089a4dd..51a6404 100644
--- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java
+++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
@@ -73,7 +73,7 @@ public Incident createIncident(IncidentRequest incidentRequest, List
Date: Fri, 10 Apr 2026 15:07:15 +0200
Subject: [PATCH 10/11] fix: add rollback cleanup for partial file uploads
- Added cleanup of previously uploaded files if later upload fails
- Prevents orphaned files in S3/MinIO
- Fixed logging
---
.../incident/service/IncidentService.java | 24 +++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/src/main/java/org/example/team6backend/incident/service/IncidentService.java b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
index 51a6404..7322892 100644
--- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java
+++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
@@ -24,6 +24,7 @@
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
+import java.util.ArrayList;
import java.util.List;
@Service
@@ -71,14 +72,29 @@ public Incident createIncident(IncidentRequest incidentRequest, List uploadedKeys = new ArrayList<>();
+
if (files != null) {
- for (MultipartFile file : files) {
- if (!file.isEmpty()) {
- documentService.uploadFile(file, savedIncident);
+ try {
+ for (MultipartFile file : files) {
+ if (!file.isEmpty()) {
+ Document savedDocument = documentService.uploadFile(file, savedIncident);
+ uploadedKeys.add(savedDocument.getFileKey());
+ }
}
+ } catch (Exception e) {
+ for (String key : uploadedKeys) {
+ try {
+ s3Service.deleteFile(key);
+ } catch (Exception cleanupEx) {
+ log.warn("Failed rollback cleanup for fileKey: {}", key, cleanupEx);
+ }
+ }
+ throw e;
}
+
}
- activityLogService.log("INCIDENT_CREATED", user.getName() + "created incident", savedIncident, user);
+ activityLogService.log("INCIDENT_CREATED", user.getName() + " created incident.", savedIncident, user);
return savedIncident;
}
From f7727acc014413635f2ab8124819003ef3dd5669 Mon Sep 17 00:00:00 2001
From: Sandra <229708855+SandraNelj@users.noreply.github.com>
Date: Fri, 10 Apr 2026 15:22:37 +0200
Subject: [PATCH 11/11] fix: cleanup uploaded files on transaction failure
---
.../incident/service/IncidentService.java | 47 ++++++++++---------
1 file changed, 24 insertions(+), 23 deletions(-)
diff --git a/src/main/java/org/example/team6backend/incident/service/IncidentService.java b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
index 7322892..653a438 100644
--- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java
+++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java
@@ -61,42 +61,43 @@ private Pageable withDefaultSort(Pageable pageable) {
@Transactional
public Incident createIncident(IncidentRequest incidentRequest, List files, AppUser user) {
- Incident incident = new Incident();
- incident.setSubject(incidentRequest.getSubject());
- incident.setDescription(incidentRequest.getDescription());
- incident.setIncidentCategory(incidentRequest.getIncidentCategory());
- incident.setCreatedBy(user);
- incident.setIncidentStatus(IncidentStatus.OPEN);
- incident.setCreatedAt(LocalDateTime.now());
- incident.setUpdatedAt(LocalDateTime.now());
+ List uploadedKeys = new ArrayList<>();
- Incident savedIncident = incidentRepository.save(incident);
+ try {
+ Incident incident = new Incident();
+ incident.setSubject(incidentRequest.getSubject());
+ incident.setDescription(incidentRequest.getDescription());
+ incident.setIncidentCategory(incidentRequest.getIncidentCategory());
+ incident.setCreatedBy(user);
+ incident.setIncidentStatus(IncidentStatus.OPEN);
+ incident.setCreatedAt(LocalDateTime.now());
+ incident.setUpdatedAt(LocalDateTime.now());
- List uploadedKeys = new ArrayList<>();
+ Incident savedIncident = incidentRepository.save(incident);
- if (files != null) {
- try {
+ if (files != null) {
for (MultipartFile file : files) {
if (!file.isEmpty()) {
Document savedDocument = documentService.uploadFile(file, savedIncident);
uploadedKeys.add(savedDocument.getFileKey());
}
}
- } catch (Exception e) {
- for (String key : uploadedKeys) {
- try {
- s3Service.deleteFile(key);
- } catch (Exception cleanupEx) {
- log.warn("Failed rollback cleanup for fileKey: {}", key, cleanupEx);
- }
- }
- throw e;
}
+ activityLogService.log("INCIDENT_CREATED", user.getName() + " created incident.", savedIncident, user);
+
+ return savedIncident;
+ } catch (Exception e) {
+ for (String key : uploadedKeys) {
+ try {
+ s3Service.deleteFile(key);
+ } catch (Exception cleanupEx) {
+ log.warn("Failed rollback cleanup for fileKey: {}", key, cleanupEx);
+ }
+ }
+ throw e;
}
- activityLogService.log("INCIDENT_CREATED", user.getName() + " created incident.", savedIncident, user);
- return savedIncident;
}
/** Find all incidents (Admin) **/