Skip to content
Original file line number Diff line number Diff line change
@@ -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<List<ActivityLogResponse>> getActivityByIncidentId(@PathVariable Long incidentId) {
List<ActivityLogResponse> activityLogs = activityLogService.getByIncidentId(incidentId).stream()
.map(ActivityLogResponse::fromEntity).toList();

return ResponseEntity.ok(activityLogs);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<ActivityLog, Long> {
List<ActivityLog> findByIncidentIdOrderByCreatedAtDesc(Long incidentId);
}
Original file line number Diff line number Diff line change
@@ -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<ActivityLog> getByIncidentId(Long incidentId) {
return activityLogRepository.findByIncidentIdOrderByCreatedAtDesc(incidentId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -20,15 +21,15 @@ public CommentController(CommentService commentService) {
}

@GetMapping("/incident/{incidentId}")
@ResponseBody
public ResponseEntity<List<Comment>> getCommentByIncidentId(@PathVariable Long incidentId) {
List<Comment> comments = commentService.getCommentByIncidentId(incidentId);
return ResponseEntity.ok(comments);
}

@PostMapping
public ResponseEntity<Comment> 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();
}
Comment on lines 30 to 34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle validation errors and null incidentId gracefully.

Two concerns with the redirect flow:

  1. Validation errors: If @Valid fails (e.g., empty message), Spring throws MethodArgumentNotValidException, likely showing a Whitelabel error page without a global handler.

  2. Null safety: If request.getIncidentId() is null (malformed form submission), the string concatenation on line 33 would produce redirect:/incidents/null.

🛡️ Suggested improvement
 `@PostMapping`
-public String createComment(`@Valid` CommentRequest request) {
+public String createComment(`@Valid` CommentRequest request, BindingResult bindingResult) {
+    if (bindingResult.hasErrors() || request.getIncidentId() == null) {
+        // Redirect back with error indicator, or to incidents list
+        return "redirect:/incidents";
+    }
     commentService.createComment(request.getIncidentId(), request.getUserId(), request.getMessage());
     return "redirect:/incidents/" + request.getIncidentId();
 }

You'd also need to import org.springframework.validation.BindingResult.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/team6backend/comment/controller/CommentController.java`
around lines 30 - 34, The createComment method should handle validation failures
and guard against a null incidentId: change the signature of
CommentController.createComment to accept a
org.springframework.validation.BindingResult immediately after the `@Valid`
CommentRequest, check bindingResult.hasErrors() and return the appropriate view
(e.g., the incident detail page or the form view) so validation errors render
instead of throwing MethodArgumentNotValidException; also
validate/request.getIncidentId() != null before building the redirect, and if
null, handle gracefully (render an error view or redirect to a safe list page)
instead of concatenating "null"; ensure you still call
commentService.createComment(request.getIncidentId(), request.getUserId(),
request.getMessage()) only when validation passes and incidentId is present.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Comment> getCommentByIncidentId(Long incidentId) {
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,11 @@ public Page<IncidentResponse> getAssignedIncidents(@AuthenticationPrincipal Cust
public Page<IncidentResponse> 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()));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,19 +11,23 @@
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;

@Service
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 **/
Expand All @@ -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) **/
Expand All @@ -60,8 +70,17 @@ public Page<Incident> findByCreatedBy(AppUser user, Pageable pageable) {
public Page<Incident> 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;
}
}
Loading
Loading