Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import org.example.team6backend.incident.dto.IncidentResponse;
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.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
Expand All @@ -23,7 +26,7 @@ public IncidentController(IncidentService incidentService) {
/** Create new incident */
@PostMapping
@PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')")
public IncidentResponse createIncident(@Valid @RequestBody IncidentRequest incidentRequest) {
public IncidentResponse createIncident(@RequestBody @Valid IncidentRequest incidentRequest) {
Incident incident = new Incident();
incident.setSubject(incidentRequest.getSubject());
incident.setDescription(incidentRequest.getDescription());
Expand All @@ -36,14 +39,20 @@ public IncidentResponse createIncident(@Valid @RequestBody IncidentRequest incid
/** Get my incidents(user) */
@PreAuthorize("hasRole('RESIDENT')")
@GetMapping("/my")
public Page<IncidentResponse> getMyIncidents(Pageable pageable) {
return incidentService.findByCreatedBy(pageable).map(IncidentResponse::fromEntity);
public Page<IncidentResponse> getMyIncidents(@AuthenticationPrincipal CustomUserDetails userDetails,
Pageable pageable) {

AppUser user = userDetails.getUser();
return incidentService.findByCreatedBy(user, pageable).map(IncidentResponse::fromEntity);
}

/** Get assigned incidents(handler) */
@PreAuthorize("hasRole('HANDLER')")
@GetMapping("/assigned")
public Page<IncidentResponse> getAssignedIncidents(Pageable pageable) {
return incidentService.findByAssignedTo(pageable).map(IncidentResponse::fromEntity);
public Page<IncidentResponse> getAssignedIncidents(@AuthenticationPrincipal CustomUserDetails userDetails,
Pageable pageable) {
AppUser user = userDetails.getUser();
return incidentService.findByAssignedTo(user, pageable).map(IncidentResponse::fromEntity);
}

@PreAuthorize("hasRole('ADMIN')")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,16 @@ public Page<Incident> findAll(Pageable pageable) {
}

/** Find your own incidents (user) **/
public Page<Incident> findByCreatedBy(Pageable pageable) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
AppUser user = userDetails.getUser();

public Page<Incident> findByCreatedBy(AppUser user, Pageable pageable) {
return incidentRepository.findByCreatedBy(user, withDefaultSort(pageable));
}

/** Find assigned incidents per HANDLER **/
public Page<Incident> findByAssignedTo(Pageable pageable) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
AppUser user = userDetails.getUser();

public Page<Incident> findByAssignedTo(AppUser user, Pageable pageable) {
return incidentRepository.findByAssignedTo(user, withDefaultSort(pageable));
}

public Incident findById(Long id) {
return incidentRepository.findById(id).orElse(null);
}
}
106 changes: 98 additions & 8 deletions src/main/java/org/example/team6backend/page/PageController.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package org.example.team6backend.page;

import jakarta.validation.Valid;
import org.example.team6backend.incident.dto.IncidentRequest;
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.example.team6backend.user.service.UserService;
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;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

@Controller
public class PageController {
private final UserService userService;
private final IncidentService incidentService;

public PageController(UserService userService) {
public PageController(UserService userService, IncidentService incidentService) {
this.userService = userService;
this.incidentService = incidentService;
}

@GetMapping("/")
Expand All @@ -22,10 +36,26 @@ public String index() {
}

@GetMapping("/dashboard")
public String dashboard(@AuthenticationPrincipal CustomUserDetails userDetails, Model model) {
AppUser user = userDetails.getUser();
model.addAttribute("user", user);
model.addAttribute("role", user.getRole().name());
public String dashboard(@AuthenticationPrincipal CustomUserDetails userDetails, Model model, Pageable pageable) {

if (userDetails != null && userDetails.getUser() != null) {
AppUser user = userDetails.getUser();
model.addAttribute("user", userDetails.getUser());
model.addAttribute("role", user.getRole().name());

Page<Incident> incidents;

switch (user.getRole()) {
case RESIDENT -> incidents = incidentService.findByCreatedBy(user, pageable);
case HANDLER -> incidents = incidentService.findByAssignedTo(user, pageable);
case ADMIN -> incidents = incidentService.findAll(pageable);
default -> incidents = Page.empty();
}
model.addAttribute("incidents", incidents);

} else {
model.addAttribute("role", "PENDING");
}
return "dashboard";
}

Expand All @@ -36,18 +66,78 @@ public String incidents(@AuthenticationPrincipal CustomUserDetails userDetails,
}

@GetMapping("/create-incident")
public String createIncident(@AuthenticationPrincipal CustomUserDetails userDetails, Model model) {
public String createIncident(@AuthenticationPrincipal CustomUserDetails userDetails, Model model,
HttpServletRequest request) {
AppUser user = userDetails.getUser();

String role = user.getRole().name();

CsrfToken csrf = (CsrfToken) request.getAttribute("_csrf");
model.addAttribute("_csrf", csrf);

if (role.equals("RESIDENT") || role.equals("ADMIN")) {
model.addAttribute("role", role);
model.addAttribute("user", user);
model.addAttribute("incidentRequest", new IncidentRequest());
return "createincident";
}
return "redirect:/dashboard";
}

@PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')")
@PostMapping("/create-incident")
public String submitIncident(@AuthenticationPrincipal CustomUserDetails userDetails,
@Valid @ModelAttribute IncidentRequest incidentRequest, BindingResult bindingResult, Model model,
HttpServletRequest request) {
AppUser user = userDetails.getUser();
String role = user.getRole().name();

if (bindingResult.hasErrors()) {
model.addAttribute("role", role);
model.addAttribute("user", user);
CsrfToken csrf = (CsrfToken) request.getAttribute("_csrf");
model.addAttribute("_csrf", csrf);
return "createincident";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Incident incident = new Incident();
incident.setSubject(incidentRequest.getSubject());
incident.setDescription(incidentRequest.getDescription());
incident.setIncidentCategory(incidentRequest.getIncidentCategory());
incident.setCreatedBy(user);

Incident saved = incidentService.createIncident(incident);

model.addAttribute("success", "Incident created successfully!");
model.addAttribute("incidentRequest", incidentRequest);
return "redirect:/incident/" + saved.getId();
Comment on lines +110 to +112

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

Model attributes won't survive the redirect.

Lines 107-108 set success and incidentRequest as model attributes, but these are lost when performing an HTTP redirect. Use RedirectAttributes to flash messages across redirects.

🔧 Proposed fix
 	`@PostMapping`("/create-incident")
 	public String submitIncident(`@AuthenticationPrincipal` CustomUserDetails userDetails,
-			`@Valid` `@ModelAttribute` IncidentRequest incidentRequest, BindingResult bindingResult, Model model) {
+			`@Valid` `@ModelAttribute` IncidentRequest incidentRequest, BindingResult bindingResult,
+			RedirectAttributes redirectAttributes, Model model) {
 		AppUser user = userDetails.getUser();
 		String role = user.getRole().name();

 		if (bindingResult.hasErrors()) {
 			model.addAttribute("role", role);
 			model.addAttribute("user", user);
 			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);

-		model.addAttribute("success", "Incident created successfully!");
-		model.addAttribute("incidentRequest", incidentRequest);
+		redirectAttributes.addFlashAttribute("success", "Incident created successfully!");
 		return "redirect:/incident/" + saved.getId();
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
model.addAttribute("success", "Incident created successfully!");
model.addAttribute("incidentRequest", incidentRequest);
return "redirect:/incident/" + saved.getId();
`@PostMapping`("/create-incident")
public String submitIncident(`@AuthenticationPrincipal` CustomUserDetails userDetails,
`@Valid` `@ModelAttribute` IncidentRequest incidentRequest, BindingResult bindingResult,
RedirectAttributes redirectAttributes, Model model) {
AppUser user = userDetails.getUser();
String role = user.getRole().name();
if (bindingResult.hasErrors()) {
model.addAttribute("role", role);
model.addAttribute("user", user);
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);
redirectAttributes.addFlashAttribute("success", "Incident created successfully!");
return "redirect:/incident/" + saved.getId();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/team6backend/page/PageController.java` around lines
107 - 109, The controller is adding model attributes before returning a redirect
so they are lost; update the handler in PageController (the method that
currently calls model.addAttribute("success", ...) and
model.addAttribute("incidentRequest", ...)) to accept a RedirectAttributes
parameter and replace those model.addAttribute calls with
redirectAttributes.addFlashAttribute("success", ...) and
redirectAttributes.addFlashAttribute("incidentRequest", ...), then keep the
existing "redirect:/incident/" + saved.getId() return so the flash attributes
survive the redirect.

}

@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());

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

Add null check for createdBy defensively.

While IncidentService.createIncident always sets createdBy, incidents created before this change or via other paths could have null createdBy, causing an NPE.

🛡️ Proposed fix
-		boolean isResident = incident.getCreatedBy().getId().equals(user.getId());
+		boolean isResident = incident.getCreatedBy() != null && incident.getCreatedBy().getId().equals(user.getId());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
boolean isResident = incident.getCreatedBy().getId().equals(user.getId());
boolean isResident = incident.getCreatedBy() != null && incident.getCreatedBy().getId().equals(user.getId());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/team6backend/page/PageController.java` at line 128,
The current assignment to isResident calls incident.getCreatedBy().getId() and
can NPE if createdBy is null; update the logic in PageController where
isResident is set so you first check createdBy is non-null and then compare IDs
(e.g., guard with incident.getCreatedBy() != null &&
Objects.equals(incident.getCreatedBy().getId(), user.getId())), or otherwise use
a null-safe comparator to compare createdBy.getId() and user.getId(); ensure you
import java.util.Objects if using Objects.equals.


if (!isAdmin && !isHandler && !isResident) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}

model.addAttribute("incident", incident);
return "view-incident";
}

@GetMapping("/profile")
public String profile(@AuthenticationPrincipal CustomUserDetails userDetails, Model model) {
AppUser user = userDetails.getUser();
Expand Down
76 changes: 35 additions & 41 deletions src/main/resources/templates/createincident.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,56 +68,50 @@
<div class="card">
<h2>Report Incident</h2>

<form id="incidentForm">

<label for="subject">Subject:</label>
<input type="text" id="subject" name="subject" required />

<label for="description">Description</label>
<textarea id="description" name="description"></textarea>

<label for="category">Category:</label>
<select id="category" name="incidentCategory" required>
<form th:action="@{/create-incident}" th:object="${incidentRequest}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

<!-- GLOBAL ERROR -->
<div th:if="${fields.hasErrors('*')}" style="color: red;">
Please correct the errors below.
</div>

<!--SUBJECT-->
<label>Subject:</label>
<input type="text" th:field="*{subject}"/>
<div th:if="${fields.hasErrors('subject')}"
th:errors="*{subject}"
style="color: red;">
</div>

<!--DESCRIPTION-->
<label>Description:</label>
<textarea th:field="*{description}"></textarea>
<div th:if="${fields.hasErrors('description')}"
th:errors="*{description}"
style="color: red;">
</div>

<!--CATEGORY-->
<label>Category:</label>
<select th:field="*{incidentCategory}">
<option value="">-- Select Category --</option>
<option value="OTHER">Other</option>
<option value="LAUNDRY_ROOM">Laundry room</option>
<option value="NOISE_DISTURBANCE">Noise Disturbance</option>
<option value="DAMAGE">Damage</option>
</select>
<div th:if="${fields.hasErrors('incidentCategory')}"
th:errors="*{incidentCategory}"
style="color: red;">
</div>

<button type="submit">Create Incident</button>
</form>

<!--SUCCESS-MESSAGE-->
<div th:if="${success}" th:text="${success}" style="color:green;"></div>
</div>
</div>

<script>
document.getElementById("incidentForm").addEventListener("submit", async function(e) {
e.preventDefault();

const formData = new FormData(e.target);

const data = {
subject: formData.get("subject"),
description: formData.get("description"),
incidentCategory: formData.get("incidentCategory")
};

const response = await fetch("/api/incidents", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});

if (response.ok) {
alert("Incident created!");
e.target.reset();
} else {
const errorData = await response.json().catch(() => null);
const message = errorData?.message || 'Error: ${response.status}';
alert('Failed to create incident: ${message}')
}
});
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion src/main/resources/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ <h3>Recent incidents</h3>
<p style="text-align: center; color: #6b7280;">Loading incidents...</p>
</div>
<div id="actionButtons" class="action-buttons">
<a th:if="${role == 'RESIDENT'}" href="/incidents?action=create" class="btn btn-primary">Create new incident</a>
<a th:if="${role == 'RESIDENT'}" href="/create-incident" class="btn btn-primary">Create new incident</a>
<a th:if="${role == 'HANDLER'}" href="/incidents" class="btn btn-handler">Manage incidents</a>
<a th:if="${role == 'ADMIN'}" href="/admin" class="btn btn-admin">Go to Admin Panel</a>
</div>
Expand Down
Loading