From 29d529eb96b28827ee0686f62c330ff98d4dc7a2 Mon Sep 17 00:00:00 2001 From: Sandra <229708855+SandraNelj@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:53:08 +0200 Subject: [PATCH 01/11] Implement file handling for incidents with S3/MinIO support - Added file upload and download functionality for incidents in DocumentController. - Updated S3Service and MinIO integration for storing and retrieving files. - Linked file uploads to incident creation and ensured correct redirects to `/incidents/{id}` after upload and comment creation. - Updated PageController submitIncident redirect to match `/incidents/{id}`. - Updated CommentController redirect after creating comment to `/incidents/{id}`. - Fixed frontend form to pass incidentId correctly for file uploads and comments. - Ensured API endpoints for incidents remain consistent with `/api/incidents`. - Refactored URLs to consistently use plural `/incidents` in UI routes. --- pom.xml | 5 ++ .../team6backend/config/MinioConfig.java | 24 ++++++ .../controller/DocumentController.java | 63 ++++++++++++++++ .../document/entity/Document.java | 73 +++++++++++++++++++ .../repository/DocumentRepository.java | 11 +++ .../document/service/DocumentService.java | 56 ++++++++++++++ .../document/service/S3Service.java | 63 ++++++++++++++++ .../incident/entity/Incident.java | 6 +- .../team6backend/page/PageController.java | 21 +++--- .../migration/V8__create_document_table.sql | 14 ++++ .../resources/templates/createincident.html | 9 ++- src/main/resources/templates/dashboard.html | 2 +- src/main/resources/templates/incidents.html | 2 +- 13 files changed, 336 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/example/team6backend/config/MinioConfig.java create mode 100644 src/main/java/org/example/team6backend/document/controller/DocumentController.java create mode 100644 src/main/java/org/example/team6backend/document/entity/Document.java create mode 100644 src/main/java/org/example/team6backend/document/repository/DocumentRepository.java create mode 100644 src/main/java/org/example/team6backend/document/service/DocumentService.java create mode 100644 src/main/java/org/example/team6backend/document/service/S3Service.java create mode 100644 src/main/resources/db/migration/V8__create_document_table.sql diff --git a/pom.xml b/pom.xml index 8f37f26..04ee113 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,11 @@ spring-security-test test + + com.squareup.okhttp3 + okhttp + 4.11.0 + diff --git a/src/main/java/org/example/team6backend/config/MinioConfig.java b/src/main/java/org/example/team6backend/config/MinioConfig.java new file mode 100644 index 0000000..d94e3df --- /dev/null +++ b/src/main/java/org/example/team6backend/config/MinioConfig.java @@ -0,0 +1,24 @@ +package org.example.team6backend.config; + +import io.minio.MinioClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MinioConfig { + + @Value("${minio.url}") + private String url; + + @Value("${minio.access-key}") + private String accessKey; + + @Value("${minio.secret-key}") + private String secretKey; + + @Bean + public MinioClient minioClient() { + return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build(); + } +} diff --git a/src/main/java/org/example/team6backend/document/controller/DocumentController.java b/src/main/java/org/example/team6backend/document/controller/DocumentController.java new file mode 100644 index 0000000..35c3b43 --- /dev/null +++ b/src/main/java/org/example/team6backend/document/controller/DocumentController.java @@ -0,0 +1,63 @@ +package org.example.team6backend.document.controller; + +import org.example.team6backend.document.entity.Document; +import org.example.team6backend.document.service.DocumentService; +import org.example.team6backend.incident.entity.Incident; +import org.example.team6backend.incident.service.IncidentService; +import org.example.team6backend.security.CustomUserDetails; +import org.example.team6backend.user.entity.AppUser; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.List; + +@Controller +@RequestMapping("/documents") +public class DocumentController { + + private final DocumentService documentService; + private final IncidentService incidentService; + + public DocumentController(DocumentService documentService, IncidentService incidentService) { + this.documentService = documentService; + this.incidentService = incidentService; + } + + @PostMapping("/upload/{incidentId}") + public String uploadFile(@PathVariable Long incidentId, @RequestParam("files") List files, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + AppUser user = userDetails.getUser(); + Incident incident = incidentService.getById(incidentId, user); + + for (MultipartFile file : files) { + if (!file.isEmpty()) { + documentService.uploadFile(file, incident); + } + } + return "redirect:/incidents/" + incidentId; + } + + @GetMapping("/download/{id}") + 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); + + InputStream stream = documentService.downloadFile(document.getFileKey()); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + document.getFileName() + "\"") + .contentType(MediaType.parseMediaType(document.getContentType())).body(new InputStreamResource(stream)); + + } +} diff --git a/src/main/java/org/example/team6backend/document/entity/Document.java b/src/main/java/org/example/team6backend/document/entity/Document.java new file mode 100644 index 0000000..daca6c2 --- /dev/null +++ b/src/main/java/org/example/team6backend/document/entity/Document.java @@ -0,0 +1,73 @@ +package org.example.team6backend.document.entity; + +import jakarta.persistence.*; +import org.example.team6backend.incident.entity.Incident; + +@Entity +public class Document { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "file_name") + private String fileName; + @Column(name = "content_type") + private String contentType; + @Column(name = "file_key") + private String fileKey; + @Column(name = "file_size") + private Long fileSize; + + @ManyToOne + @JoinColumn(name = "incident_id") + private Incident incident; + + public Long getId() { + return id; + } + + public String getFileName() { + return fileName; + } + + public String getContentType() { + return contentType; + } + + public String getFileKey() { + return fileKey; + } + + public Long getFileSize() { + return fileSize; + } + + public Incident getIncident() { + return incident; + } + + public void setId(Long id) { + this.id = id; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public void setFileKey(String fileKey) { + this.fileKey = fileKey; + } + + public void setFileSize(Long fileSize) { + this.fileSize = fileSize; + } + + public void setIncident(Incident incident) { + this.incident = incident; + } +} diff --git a/src/main/java/org/example/team6backend/document/repository/DocumentRepository.java b/src/main/java/org/example/team6backend/document/repository/DocumentRepository.java new file mode 100644 index 0000000..7833464 --- /dev/null +++ b/src/main/java/org/example/team6backend/document/repository/DocumentRepository.java @@ -0,0 +1,11 @@ +package org.example.team6backend.document.repository; + +import org.example.team6backend.document.entity.Document; +import org.example.team6backend.incident.entity.Incident; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface DocumentRepository extends JpaRepository { + List findByIncident(Incident incident); +} diff --git a/src/main/java/org/example/team6backend/document/service/DocumentService.java b/src/main/java/org/example/team6backend/document/service/DocumentService.java new file mode 100644 index 0000000..e78d383 --- /dev/null +++ b/src/main/java/org/example/team6backend/document/service/DocumentService.java @@ -0,0 +1,56 @@ +package org.example.team6backend.document.service; + +import lombok.RequiredArgsConstructor; +import org.example.team6backend.document.entity.Document; +import org.example.team6backend.document.repository.DocumentRepository; +import org.example.team6backend.incident.entity.Incident; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import java.io.InputStream; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class DocumentService { + + private final S3Service s3Service; + private final DocumentRepository documentRepository; + + /** Upload file */ + public Document uploadFile(MultipartFile file, Incident incident) { + try { + String fileKey = UUID.randomUUID() + " _ " + file.getOriginalFilename(); + + s3Service.uploadFile(fileKey, file); + + Document document = new Document(); + document.setFileName(file.getOriginalFilename()); + document.setContentType(file.getContentType()); + document.setFileKey(fileKey); + document.setFileSize(file.getSize()); + document.setIncident(incident); + + return documentRepository.save(document); + } catch (Exception e) { + throw new RuntimeException("File upload failed", e); + } + } + + /** Download file */ + public InputStream downloadFile(String objectKey) { + return s3Service.downloadFile(objectKey); + } + + /** Delete file */ + public void deleteFile(Document document) { + s3Service.deleteFile(document.getFileKey()); + documentRepository.delete(document); + } + + /** Fetch all files connected to one incident */ + public List getDocumentsByIncident(Incident incidentId) { + return documentRepository.findByIncident(incidentId); + } + +} diff --git a/src/main/java/org/example/team6backend/document/service/S3Service.java b/src/main/java/org/example/team6backend/document/service/S3Service.java new file mode 100644 index 0000000..5458b43 --- /dev/null +++ b/src/main/java/org/example/team6backend/document/service/S3Service.java @@ -0,0 +1,63 @@ +package org.example.team6backend.document.service; + +import io.minio.*; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private final MinioClient minioClient; + + @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); + } + } + + /** Upload file to MinIO */ + public void uploadFile(String fileKey, MultipartFile file) { + try { + minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(fileKey) + .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build()); + } catch (Exception e) { + throw new RuntimeException("Failed to upload file " + fileKey, e); + } + } + + /** Fetch file from MinIO */ + public InputStream downloadFile(String fileKey) { + try { + return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(fileKey).build()); + } catch (Exception e) { + throw new RuntimeException("Failed to download file " + fileKey, e); + } + } + + /** Delete file from MinIO */ + public void deleteFile(String fileKey) { + try { + minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileKey).build()); + } catch (Exception e) { + throw new RuntimeException("Failed to delete file " + fileKey, e); + } + } +} 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 959e9f0..797b4f5 100644 --- a/src/main/java/org/example/team6backend/incident/entity/Incident.java +++ b/src/main/java/org/example/team6backend/incident/entity/Incident.java @@ -1,9 +1,10 @@ package org.example.team6backend.incident.entity; import jakarta.persistence.*; +import org.example.team6backend.document.entity.Document; import org.example.team6backend.user.entity.AppUser; - import java.time.LocalDateTime; +import java.util.List; @Entity public class Incident { @@ -40,6 +41,9 @@ public class Incident { @Column(name = "updated_at") private LocalDateTime updatedAt; + @OneToMany(mappedBy = "incident", cascade = CascadeType.ALL) + private List documents; + @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); diff --git a/src/main/java/org/example/team6backend/page/PageController.java b/src/main/java/org/example/team6backend/page/PageController.java index 1308cac..4a6b13c 100644 --- a/src/main/java/org/example/team6backend/page/PageController.java +++ b/src/main/java/org/example/team6backend/page/PageController.java @@ -1,6 +1,7 @@ package org.example.team6backend.page; import jakarta.validation.Valid; +import org.example.team6backend.document.service.DocumentService; import org.example.team6backend.incident.dto.IncidentRequest; import org.example.team6backend.incident.entity.Incident; import org.example.team6backend.incident.service.IncidentService; @@ -17,15 +18,18 @@ import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @Controller public class PageController { private final UserService userService; private final IncidentService incidentService; + private final DocumentService documentService; - public PageController(UserService userService, IncidentService incidentService) { + public PageController(UserService userService, IncidentService incidentService, DocumentService documentService) { this.userService = userService; this.incidentService = incidentService; + this.documentService = documentService; } @GetMapping("/") @@ -84,16 +88,14 @@ public String createIncident(@AuthenticationPrincipal CustomUserDetails userDeta @PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')") @PostMapping("/create-incident") public String submitIncident(@AuthenticationPrincipal CustomUserDetails userDetails, - @Valid @ModelAttribute IncidentRequest incidentRequest, BindingResult bindingResult, Model model, - HttpServletRequest request) { + @Valid @ModelAttribute IncidentRequest incidentRequest, BindingResult bindingResult, + @RequestParam(value = "files", required = false) MultipartFile files, Model model) { + AppUser user = userDetails.getUser(); - String role = user.getRole().name(); if (bindingResult.hasErrors()) { - model.addAttribute("role", role); + model.addAttribute("role", user.getRole().name()); model.addAttribute("user", user); - CsrfToken csrf = (CsrfToken) request.getAttribute("_csrf"); - model.addAttribute("_csrf", csrf); return "createincident"; } @@ -105,8 +107,9 @@ public String submitIncident(@AuthenticationPrincipal CustomUserDetails userDeta Incident saved = incidentService.createIncident(incident); - model.addAttribute("success", "Incident created successfully!"); - model.addAttribute("incidentRequest", incidentRequest); + if (files != null && !files.isEmpty()) { + documentService.uploadFile(files, saved); + } 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 new file mode 100644 index 0000000..a83d124 --- /dev/null +++ b/src/main/resources/db/migration/V8__create_document_table.sql @@ -0,0 +1,14 @@ +CREATE TABLE document( + id BIGSERIAL PRIMARY KEY, + file_name VARCHAR(255), + content_type VARCHAR(255), + file_key VARCHAR(255) NOT NULL, + file_size BIGINT, + + incident_id BIGINT NOT NULL, + + CONSTRAINT fk_document_incident + FOREIGN KEY (incident_id) + REFERENCES incident(id) + ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/main/resources/templates/createincident.html b/src/main/resources/templates/createincident.html index 11254cd..ce21bbb 100644 --- a/src/main/resources/templates/createincident.html +++ b/src/main/resources/templates/createincident.html @@ -68,7 +68,11 @@

Report Incident

-
+ + @@ -101,6 +105,9 @@

Report Incident

+ + +
diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index a668238..6312f95 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()}
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) **/