diff --git a/src/main/java/org/example/team6backend/activity/controller/ActivityLogController.java b/src/main/java/org/example/team6backend/activity/controller/ActivityLogController.java new file mode 100644 index 0000000..f63d676 --- /dev/null +++ b/src/main/java/org/example/team6backend/activity/controller/ActivityLogController.java @@ -0,0 +1,30 @@ +package org.example.team6backend.activity.controller; + +import org.example.team6backend.activity.dto.ActivityLogResponse; +import org.example.team6backend.activity.service.ActivityLogService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/activity") +public class ActivityLogController { + + private final ActivityLogService activityLogService; + + public ActivityLogController(ActivityLogService activityLogService) { + this.activityLogService = activityLogService; + } + + @GetMapping("/incident/{incidentId}") + public ResponseEntity> getActivityByIncidentId(@PathVariable Long incidentId) { + List activityLogs = activityLogService.getByIncidentId(incidentId).stream() + .map(ActivityLogResponse::fromEntity).toList(); + + return ResponseEntity.ok(activityLogs); + } +} diff --git a/src/main/java/org/example/team6backend/activity/dto/ActivityLogResponse.java b/src/main/java/org/example/team6backend/activity/dto/ActivityLogResponse.java new file mode 100644 index 0000000..cb97274 --- /dev/null +++ b/src/main/java/org/example/team6backend/activity/dto/ActivityLogResponse.java @@ -0,0 +1,53 @@ +package org.example.team6backend.activity.dto; + +import org.example.team6backend.activity.entity.ActivityLog; +import java.time.LocalDateTime; + +public class ActivityLogResponse { + + private String action; + private String description; + private LocalDateTime createdAt; + private String userName; + + public static ActivityLogResponse fromEntity(ActivityLog activityLog) { + ActivityLogResponse response = new ActivityLogResponse(); + response.setAction(activityLog.getAction()); + response.setDescription(activityLog.getDescription()); + response.setCreatedAt(activityLog.getCreatedAt()); + response.setUserName(activityLog.getUser() != null ? activityLog.getUser().getName() : "Unknow user"); + return response; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } +} diff --git a/src/main/java/org/example/team6backend/activity/entity/ActivityLog.java b/src/main/java/org/example/team6backend/activity/entity/ActivityLog.java new file mode 100644 index 0000000..54f4065 --- /dev/null +++ b/src/main/java/org/example/team6backend/activity/entity/ActivityLog.java @@ -0,0 +1,81 @@ +package org.example.team6backend.activity.entity; + +import jakarta.persistence.*; +import org.example.team6backend.incident.entity.Incident; +import org.example.team6backend.user.entity.AppUser; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "activity_log") +public class ActivityLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String action; + private String description; + private LocalDateTime createdAt; + + @ManyToOne + @JoinColumn(name = "incident_id", nullable = false) + private Incident incident; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private AppUser user; + + @PrePersist + void onCreated() { + createdAt = LocalDateTime.now(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public Incident getIncident() { + return incident; + } + + public void setIncident(Incident incident) { + this.incident = incident; + } + + public AppUser getUser() { + return user; + } + + public void setUser(AppUser user) { + this.user = user; + } +} diff --git a/src/main/java/org/example/team6backend/activity/repository/ActivityLogRepository.java b/src/main/java/org/example/team6backend/activity/repository/ActivityLogRepository.java new file mode 100644 index 0000000..3a31b2f --- /dev/null +++ b/src/main/java/org/example/team6backend/activity/repository/ActivityLogRepository.java @@ -0,0 +1,10 @@ +package org.example.team6backend.activity.repository; + +import org.example.team6backend.activity.entity.ActivityLog; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ActivityLogRepository extends JpaRepository { + List findByIncidentIdOrderByCreatedAtDesc(Long incidentId); +} diff --git a/src/main/java/org/example/team6backend/activity/service/ActivityLogService.java b/src/main/java/org/example/team6backend/activity/service/ActivityLogService.java new file mode 100644 index 0000000..910f79c --- /dev/null +++ b/src/main/java/org/example/team6backend/activity/service/ActivityLogService.java @@ -0,0 +1,33 @@ +package org.example.team6backend.activity.service; + +import org.example.team6backend.activity.entity.ActivityLog; +import org.example.team6backend.activity.repository.ActivityLogRepository; +import org.example.team6backend.incident.entity.Incident; +import org.example.team6backend.user.entity.AppUser; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ActivityLogService { + + private final ActivityLogRepository activityLogRepository; + + public ActivityLogService(ActivityLogRepository activityLogRepository) { + this.activityLogRepository = activityLogRepository; + } + + public ActivityLog log(String action, String description, Incident incident, AppUser user) { + ActivityLog activityLog = new ActivityLog(); + activityLog.setAction(action); + activityLog.setDescription(description); + activityLog.setIncident(incident); + activityLog.setUser(user); + + return activityLogRepository.save(activityLog); + } + + public List getByIncidentId(Long incidentId) { + return activityLogRepository.findByIncidentIdOrderByCreatedAtDesc(incidentId); + } +} diff --git a/src/main/java/org/example/team6backend/comment/controller/CommentController.java b/src/main/java/org/example/team6backend/comment/controller/CommentController.java index 3ca92bc..35e1138 100644 --- a/src/main/java/org/example/team6backend/comment/controller/CommentController.java +++ b/src/main/java/org/example/team6backend/comment/controller/CommentController.java @@ -5,11 +5,12 @@ import org.example.team6backend.comment.entity.Comment; import org.example.team6backend.comment.service.CommentService; import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.List; -@RestController +@Controller @RequestMapping("/comments") public class CommentController { @@ -20,15 +21,15 @@ public CommentController(CommentService commentService) { } @GetMapping("/incident/{incidentId}") + @ResponseBody public ResponseEntity> getCommentByIncidentId(@PathVariable Long incidentId) { List comments = commentService.getCommentByIncidentId(incidentId); return ResponseEntity.ok(comments); } @PostMapping - public ResponseEntity createComment(@Valid @RequestBody CommentRequest request) { - Comment saveComment = commentService.createComment(request.getIncidentId(), request.getUserId(), - request.getMessage()); - return ResponseEntity.ok(saveComment); + public String createComment(@Valid CommentRequest request) { + commentService.createComment(request.getIncidentId(), request.getUserId(), request.getMessage()); + return "redirect:/incidents/" + request.getIncidentId(); } } diff --git a/src/main/java/org/example/team6backend/comment/entity/Comment.java b/src/main/java/org/example/team6backend/comment/entity/Comment.java index 17d2b2b..cec2b22 100644 --- a/src/main/java/org/example/team6backend/comment/entity/Comment.java +++ b/src/main/java/org/example/team6backend/comment/entity/Comment.java @@ -30,11 +30,11 @@ public class Comment { private AppUser user; @Column(name = "created_at", nullable = false) - private LocalDateTime createAt; + private LocalDateTime createdAt; @PrePersist - protected void onCreate() { - createAt = LocalDateTime.now(); + protected void onCreated() { + createdAt = LocalDateTime.now(); } } diff --git a/src/main/java/org/example/team6backend/comment/service/CommentService.java b/src/main/java/org/example/team6backend/comment/service/CommentService.java index e0c822c..f7e9032 100644 --- a/src/main/java/org/example/team6backend/comment/service/CommentService.java +++ b/src/main/java/org/example/team6backend/comment/service/CommentService.java @@ -1,5 +1,6 @@ package org.example.team6backend.comment.service; +import org.example.team6backend.activity.service.ActivityLogService; import org.example.team6backend.comment.entity.Comment; import org.example.team6backend.comment.repository.CommentRepository; import org.example.team6backend.exception.ResourceNotFoundException; @@ -17,12 +18,14 @@ public class CommentService { private final CommentRepository commentRepository; private final IncidentRepository incidentRepository; private final AppUserRepository appUserRepository; + private final ActivityLogService activityLogService; public CommentService(CommentRepository commentRepository, IncidentRepository incidentRepository, - AppUserRepository appUserRepository) { + AppUserRepository appUserRepository, ActivityLogService activityLogService) { this.commentRepository = commentRepository; this.incidentRepository = incidentRepository; this.appUserRepository = appUserRepository; + this.activityLogService = activityLogService; } public List getCommentByIncidentId(Long incidentId) { @@ -41,6 +44,10 @@ public Comment createComment(Long incidentId, String userId, String message) { comment.setUser(user); comment.setMessage(message); - return commentRepository.save(comment); + Comment savedComment = commentRepository.save(comment); + + activityLogService.log("COMMENT_ADDED", user.getName() + " added a comment", incident, user); + + return savedComment; } } 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 e130044..8da36fa 100644 --- a/src/main/java/org/example/team6backend/incident/controller/IncidentController.java +++ b/src/main/java/org/example/team6backend/incident/controller/IncidentController.java @@ -60,4 +60,11 @@ public Page getAssignedIncidents(@AuthenticationPrincipal Cust public Page getAllIncidents(Pageable pageable) { return incidentService.findAll(pageable).map(IncidentResponse::fromEntity); } + + @PreAuthorize("hasAnyRole('RESIDENT', 'HANDLER', 'ADMIN')") + @GetMapping("/{id}") + public IncidentResponse getIncidentById(@PathVariable Long id, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return IncidentResponse.fromEntity(incidentService.getById(id, userDetails.getUser())); + } } 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 4de52e4..cf88c98 100644 --- a/src/main/java/org/example/team6backend/incident/service/IncidentService.java +++ b/src/main/java/org/example/team6backend/incident/service/IncidentService.java @@ -1,5 +1,7 @@ package org.example.team6backend.incident.service; +import org.example.team6backend.activity.service.ActivityLogService; +import org.example.team6backend.exception.ResourceNotFoundException; import org.example.team6backend.security.CustomUserDetails; import org.example.team6backend.user.entity.AppUser; import org.example.team6backend.incident.entity.Incident; @@ -9,9 +11,11 @@ import org.springframework.data.domain.PageRequest; 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.web.server.ResponseStatusException; import java.time.LocalDateTime; @@ -19,9 +23,11 @@ public class IncidentService { private final IncidentRepository incidentRepository; + private final ActivityLogService activityLogService; - public IncidentService(IncidentRepository incidentRepository) { + public IncidentService(IncidentRepository incidentRepository, ActivityLogService activityLogService) { this.incidentRepository = incidentRepository; + this.activityLogService = activityLogService; } /** Help-method for sorting **/ @@ -43,7 +49,11 @@ public Incident createIncident(Incident incident) { incident.setCreatedAt(LocalDateTime.now()); incident.setUpdatedAt(LocalDateTime.now()); - return incidentRepository.save(incident); + Incident savedIncident = incidentRepository.save(incident); + + activityLogService.log("INCIDENT_CREATED", appUser.getName() + " created the incident", savedIncident, appUser); + + return savedIncident; } /** Find all incidents (Admin) **/ @@ -60,8 +70,17 @@ 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")); - public Incident findById(Long id) { - return incidentRepository.findById(id).orElse(null); + boolean isAdmin = user.getRole().name().equals("ADMIN"); + boolean isHandler = incident.getAssignedTo() != null && incident.getAssignedTo().getId().equals(user.getId()); + boolean isResident = incident.getCreatedBy().getId().equals(user.getId()); + + if (!isAdmin && !isHandler && !isResident) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + return incident; } } diff --git a/src/main/java/org/example/team6backend/page/PageController.java b/src/main/java/org/example/team6backend/page/PageController.java index 5b3521f..1308cac 100644 --- a/src/main/java/org/example/team6backend/page/PageController.java +++ b/src/main/java/org/example/team6backend/page/PageController.java @@ -10,7 +10,6 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.web.csrf.CsrfToken; @@ -18,7 +17,6 @@ import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; @Controller public class PageController { @@ -109,33 +107,7 @@ public String submitIncident(@AuthenticationPrincipal CustomUserDetails userDeta model.addAttribute("success", "Incident created successfully!"); model.addAttribute("incidentRequest", incidentRequest); - return "redirect:/incident/" + saved.getId(); - } - - @GetMapping("/incident/{id}") - public String viewIncident(@PathVariable Long id, @AuthenticationPrincipal CustomUserDetails userDetails, - Model model) { - - Incident incident = incidentService.findById(id); - - if (incident == null) { - return "redirect:/dashboard"; - } - - AppUser user = userDetails.getUser(); - - boolean isAdmin = user.getRole().name().equals("ADMIN"); - - boolean isHandler = incident.getAssignedTo() != null && incident.getAssignedTo().getId().equals(user.getId()); - - boolean isResident = incident.getCreatedBy().getId().equals(user.getId()); - - if (!isAdmin && !isHandler && !isResident) { - throw new ResponseStatusException(HttpStatus.FORBIDDEN); - } - - model.addAttribute("incident", incident); - return "view-incident"; + return "redirect:/incidents/" + saved.getId(); } @GetMapping("/profile") @@ -159,6 +131,15 @@ public String admin(@AuthenticationPrincipal CustomUserDetails userDetails, Mode return "redirect:/dashboard"; } + @GetMapping("/incidents/{id}") + public String viewIncident(@PathVariable Long id, @AuthenticationPrincipal CustomUserDetails userDetails, + Model model) { + model.addAttribute("role", userDetails.getUser().getRole().name()); + model.addAttribute("incidentId", id); + model.addAttribute("userId", userDetails.getUser().getId()); + return "viewincident"; + } + @GetMapping("/demo") public String demo() { return "demo"; diff --git a/src/main/resources/db/migration/V7__create_activity_log_table.sql b/src/main/resources/db/migration/V7__create_activity_log_table.sql new file mode 100644 index 0000000..453e30f --- /dev/null +++ b/src/main/resources/db/migration/V7__create_activity_log_table.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS activity_log ( + id BIGSERIAL PRIMARY KEY, + action VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + incident_id BIGINT NOT NULL REFERENCES incident(id), + user_id VARCHAR(255) NOT NULL REFERENCES app_user(id) +); + +CREATE INDEX IF NOT EXISTS idx_activity_log_incident_id ON activity_log(incident_id); +CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON activity_log(user_id); +CREATE INDEX IF NOT EXISTS idx_activity_log_created_at ON activity_log(created_at); + +COMMENT ON TABLE activity_log IS 'Activity log entries for incident'; +COMMENT ON COLUMN activity_log.action IS 'Type of activity, e.g CREATED, COMMENT_ADDED, STATUS_CHANGED'; +COMMENT ON COLUMN activity_log.description IS 'Human-readable description of the activity'; +COMMENT ON COLUMN activity_log.created_at IS 'When activity happened'; +COMMENT ON COLUMN activity_log.incident_id IS 'Incident linked to this activity'; +COMMENT ON COLUMN activity_log.user_id IS 'User who preformed the activity'; diff --git a/src/main/resources/templates/createincident.html b/src/main/resources/templates/createincident.html index 55571b1..11254cd 100644 --- a/src/main/resources/templates/createincident.html +++ b/src/main/resources/templates/createincident.html @@ -72,14 +72,14 @@

Report Incident

-
+
Please correct the errors below.
-
@@ -87,7 +87,7 @@

Report Incident

-
@@ -101,7 +101,7 @@

Report Incident

-
diff --git a/src/main/resources/templates/incidents.html b/src/main/resources/templates/incidents.html index 5c141de..7175ee9 100644 --- a/src/main/resources/templates/incidents.html +++ b/src/main/resources/templates/incidents.html @@ -145,11 +145,15 @@

Active incidents

`; @@ -165,6 +169,20 @@

Active incidents

alert("Edit incident " + id); } + function viewIncident(id) { + window.location.href = `/incidents/${id}`; + } + + function escapeHtml(str) { + if (str === null || str === undefined) return ""; + return String(str) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'") + } + loadIncidents(); diff --git a/src/main/resources/templates/viewincident.html b/src/main/resources/templates/viewincident.html new file mode 100644 index 0000000..355accb --- /dev/null +++ b/src/main/resources/templates/viewincident.html @@ -0,0 +1,315 @@ + + + + + + View incident + + + + + +
+

Incident details

+ +
+ +
+
Comments
+
+
+ +
+
Add comment
+ +
+ + + + + + + + +
+
Activity
+
+
+
+
+ + Back +
+ + + + \ No newline at end of file