Skip to content

Refactored code to align with Vanilla JS / REST-api#96

Merged
SandraNelj merged 5 commits into
mainfrom
fix/refactorRESTapi
Apr 22, 2026
Merged

Refactored code to align with Vanilla JS / REST-api#96
SandraNelj merged 5 commits into
mainfrom
fix/refactorRESTapi

Conversation

@SandraNelj

@SandraNelj SandraNelj commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Document endpoints exposed under REST-style API; downloads now use direct file keys.
  • Bug Fixes & Improvements

    • Navigation and client redirects standardized to .html paths across the site.
    • File uploads now return explicit success responses; download streaming updated.
    • Deletion processes now log and suppress storage-related errors instead of failing.
  • Removed

    • Server-side page controller and associated dynamic page routes removed in favor of static .html pages.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@SandraNelj has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 50 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 50 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0ef12e0-6852-4500-9ce4-27675bb23138

📥 Commits

Reviewing files that changed from the base of the PR and between 0288abe and b551c5d.

📒 Files selected for processing (6)
  • src/main/java/org/example/team6backend/config/SecurityConfig.java
  • src/main/java/org/example/team6backend/document/service/DocumentService.java
  • src/main/resources/static/dashboard.html
  • src/main/resources/static/incidents.html
  • src/main/resources/static/viewincident.html
  • src/test/java/org/example/team6backend/document/service/DocumentServiceTest.java
📝 Walkthrough

Walkthrough

Replaced server-side page controller with static .html pages, updated SecurityConfig to protect .html routes, refactored DocumentController into REST endpoints using file-key-based downloads, and adjusted MinIO/DocumentService deletion error handling.

Changes

Cohort / File(s) Summary
Security Configuration
src/main/java/org/example/team6backend/config/SecurityConfig.java
Updated HTTP matcher mappings to use .html endpoints (/dashboard.html, /incidents.html, /profile.html, /admin.html); reorganized role checks to cover /incidents.html + /api/incidents/** for RESIDENT/HANDLER/ADMIN and /admin.html + /api/admin/** for ADMIN; OAuth2 success redirect set to /dashboard.html.
Page Controller Removal
src/main/java/org/example/team6backend/page/PageController.java
Deleted the entire server-side PageController including all route handlers and its injected dependencies, removing server-rendered endpoints for /, /dashboard, /incidents, /create-incident, /profile, /admin, /incidents/{id}, and /demo.
Document API Changes
src/main/java/org/example/team6backend/document/controller/DocumentController.java, src/test/java/.../DocumentControllerTest.java
Converted controller to @RestController and base path /documents/api/documents; upload returns ResponseEntity<Void>; download endpoint changed from /download/{incidentId} to /{fileKey}/download and now resolves documents by fileKey and streams from minioService.getFile(...); tests updated to new path.
Storage & Deletion Handling
src/main/java/org/example/team6backend/document/service/DocumentService.java, src/main/java/org/example/team6backend/document/service/MinioService.java
DocumentService.deleteFile now wraps repository + MinIO deletes in a try/catch and catches Throwable; MinioService.deleteFile no longer throws on failure — logs to stderr and returns.
Static Frontend Updates
src/main/resources/static/admin.html, .../createincident.html, .../dashboard.html, .../incidents.html, .../profile.html, .../viewincident.html
Updated client-side navigation and JS redirect targets to use .html-suffixed routes (/dashboard.html, /incidents.html, /profile.html, /admin.html) across affected static pages.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DocumentController
    participant DocumentService
    participant MinioService
    participant Repository
    Client->>DocumentController: GET /api/documents/{fileKey}/download
    DocumentController->>DocumentService: getByFileKey(fileKey)
    DocumentService->>Repository: findByFileKey(fileKey)
    Repository-->>DocumentService: Document or empty
    alt Document found
        DocumentService->>MinioService: getFile(fileKey)
        MinioService-->>DocumentService: InputStream (file)
        DocumentController-->>Client: 200 OK + stream (Content-Disposition)
    else Not found
        DocumentController-->>Client: 404 Not Found
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through routes both old and new,
Switched paths to .html with a joyful chew.
Files now fetched by a secret key,
MinIO hums, the API is free.
Security stands guard — hip hop hooray! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring effort: converting from server-side templating (PageController) to a REST API architecture with HTML static files and Vanilla JS clients.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/refactorRESTapi

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
src/main/resources/static/admin.html (1)

778-782: ⚠️ Potential issue | 🟠 Major

Residual Thymeleaf in the navbar won't render in a static file.

th:text="${user.name ...}" and th:text="${role}" are inert here since admin.html is served as a static resource. The navbar will display the literal fallback text ("User", "ADMIN") for every session. Fetch the current user (e.g. /api/users/me) in JS and populate .user-name / .role-badge from the response, same pattern the rest of the page already uses.

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

In `@src/main/resources/static/admin.html` around lines 778 - 782, The navbar
still contains Thymeleaf attributes that won't be processed in this static
admin.html; replace the inert th:text usage by adding client-side code to fetch
the current user (GET /api/users/me) and then set the DOM textContent for the
.user-name element (use user.name || user.githubLogin) and the .role-badge
element (use user.role or response.role), keeping the existing logout() button
behavior; locate the .user-name and .role-badge nodes in the script that
initializes the page (or add a small init function) and update them after the
fetch, handling errors by leaving the fallback text unchanged or logging to
console.
src/main/resources/static/incidents.html (1)

173-199: ⚠️ Potential issue | 🔴 Critical

role is always unset, so the page always shows "Unable to load incidents".

document.body.dataset.role is sourced from th:attr="data-role=${role}" (line 173). Since the file is now static, the data-role attribute is never written — role is undefined at line 185 — and the dispatch at lines 191-199 falls into the else branch that renders "Unable to load incidents".

Fetch the role client-side (e.g. via a /api/users/me or /api/auth/role endpoint) before branching, or attach the role via a small cookie/header set by the backend during authentication.

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

In `@src/main/resources/static/incidents.html` around lines 173 - 199, The page
reads role from document.body.dataset.role (set via th:attr="data-role=${role}")
but since the HTML is now static role is undefined, causing loadIncidents() to
always hit the unknown-role branch; update loadIncidents (or an init function
called before it) to retrieve the current user's role from the backend (e.g.,
call a /api/users/me or /api/auth/role endpoint) and set a local variable (role)
before the existing branching logic, or alternatively read role from a
backend-set cookie/header and use that value instead of
document.body.dataset.role; ensure you reference and update the same symbol
loadIncidents and remove reliance on document.body.dataset.role so the
ADMIN/HANDLER/RESIDENT branches work correctly.
src/main/resources/static/profile.html (2)

213-218: ⚠️ Potential issue | 🟡 Minor

Two navigation links missed the .html rename.

Lines 215 and 217 still point to the extensionless routes, so they bypass the /profile.html / /admin.html matchers added in SecurityConfig and will 404 once PageController is gone.

Proposed fix
         <a href="/dashboard.html">Home</a>
         <a th:if="${role != 'PENDING'}" href="/incidents.html">Incidents</a>
-        <a href="/profile">Profile</a>
+        <a href="/profile.html">Profile</a>
         <div th:if="${role == 'ADMIN'}">
-            <a href="/admin">Admin</a>
+            <a href="/admin.html">Admin</a>
         </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/profile.html` around lines 213 - 218, Update the
hardcoded anchor hrefs in profile.html so they point to the new .html pages:
change the Incidents link (the <a th:if="${role != 'PENDING'}"
href="/incidents.html">) and the Profile link (currently href="/profile") to use
/incidents.html and /profile.html respectively, and also update the Admin link
inside the <div th:if="${role == 'ADMIN'}"> to href="/admin.html" so these
anchors match the SecurityConfig .html matchers and avoid 404s; keep the
existing th:if conditions unchanged.

1-295: ⚠️ Potential issue | 🔴 Critical

Thymeleaf directives throughout won't be processed in a static file.

profile.html now lives under src/main/resources/static/ but is still built around server-side rendering: th:if, th:text, th:classappend, th:src, th:attr, xmlns:th at lines 2, 214, 216, 220–221, 231–236, 242, 247, 252, 257, 260. Served statically these are emitted verbatim, so:

  • User identity/name/email/avatar never populate (the page shows literal placeholders "User", "user@example.com", "K", "-").
  • The PENDING role pending-note and ADMIN-only Admin link never render conditionally.
  • accountCreated datum is read from data-created-at at line 287 — but that attribute is only set via th:attr, so the date won't render either.

Convert to vanilla JS consistent with the PR's direction: fetch user data from a REST endpoint (e.g. /api/users/me) in DOMContentLoaded and render into plain DOM nodes; use a data API instead of ${role} for the admin link visibility. Same pattern as dashboard.html / admin.html / incidents.html / viewincident.html.

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

In `@src/main/resources/static/profile.html` around lines 1 - 295, The file still
uses Thymeleaf directives but now lives in static/—replace server-side rendering
with client-side rendering: in the existing DOMContentLoaded handler
(document.addEventListener("DOMContentLoaded", ...)) fetch the current user from
/api/users/me, then populate DOM nodes by id/class: set .user-name text,
.info-value nodes for display name/email/githubLogin, set the profile-avatar img
src or create the avatar-placeholder text, set .role-badge text and add the role
class (role.toLowerCase()), show/hide the Admin nav link and Incidents link
based on role, and write the formatted createdAt string into `#accountCreated`
using the existing formatDate function; ensure logout() stays intact and
remove/ignore all th:* attributes so the page works purely from the fetched
JSON.
src/main/resources/static/createincident.html (1)

93-139: ⚠️ Potential issue | 🔴 Critical

Form is non-functional: Thymeleaf in a static file + stale action.

This file is served from src/main/resources/static/ and is therefore delivered as a static resource — Spring does not run Thymeleaf against static resources. Every directive (th:action, th:object, th:field, th:if, th:errors, th:name, th:value, th:text) is rendered as literal attributes, so:

  • The action attribute resolves to nothing useful; th:action="@{/create-incident}" also no longer matches any controller after PageController removal.
  • The hidden CSRF input (th:name="${_csrf.parameterName}") is never populated → POSTs will be rejected by the CSRF filter.
  • th:field="*{subject}", th:field="*{description}", and th:field="*{incidentCategory}" never become real name/value attributes, so the form submits nothing the backend can bind.
  • Validation blocks (#fields.hasErrors(...)) render as static markup with no conditional.

This whole page needs to be converted to vanilla JS (matching the rest of the PR): add explicit name attributes, manually fetch the CSRF token (or add /api/** to csrf.ignoringRequestMatchers appropriately), and fetch() the relevant REST endpoints (e.g. POST /api/incidents then POST /api/documents/upload/{incidentId} as a follow-up upload), handling errors in JS rather than Thymeleaf #fields.

The same Thymeleaf-in-static issue applies to profile.html, admin.html, dashboard.html, viewincident.html, and incidents.html — called out individually there.

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

In `@src/main/resources/static/createincident.html` around lines 93 - 139, The
form in createincident.html is broken because it contains Thymeleaf attributes
that are not processed in static resources; replace the template directives by
converting the form to plain HTML with explicit name attributes (e.g., inputs
for subject, description, incidentCategory, files) and remove th:* attributes,
then implement a small JS module that (1) retrieves a CSRF token (via a meta tag
or a GET to /api/csrf), (2) posts JSON to the REST create endpoint (POST
/api/incidents) to create the incident, (3) on success uploads files via
multipart to POST /api/documents/upload/{incidentId}, and (4) displays
validation/server errors in the DOM instead of using `#fields.hasErrors`; apply
the same conversion pattern to profile.html, admin.html, dashboard.html,
viewincident.html, and incidents.html so no th:* attributes remain in static
files.
src/main/resources/static/dashboard.html (2)

693-712: ⚠️ Potential issue | 🟡 Minor

Several navigation links missed the .html rename.

These four still point at the removed PageController routes and will 404 (and bypass the new SecurityConfig role matchers):

Proposed fix
-                <a href="/admin" class="btn btn-outline btn-sm">Manage all →</a>
+                <a href="/admin.html" class="btn btn-outline btn-sm">Manage all →</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>
+                <a th:if="${role == 'RESIDENT'}" href="/createincident.html" class="btn btn-primary">Create new incident</a>
+                <a th:if="${role == 'HANDLER'}" href="/incidents.html" class="btn btn-handler">Manage incidents</a>
+                <a th:if="${role == 'ADMIN'}" href="/admin.html" class="btn btn-admin">Go to Admin Panel</a>

(The th:if on these will remain non-functional until the Thymeleaf-in-static issue above is fixed, but the href values should be consistent with the rest of the file regardless.)

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

In `@src/main/resources/static/dashboard.html` around lines 693 - 712, Update the
hard-coded hrefs to use the .html routes so they match the new pages and
SecurityConfig matchers: change the anchor with text "Manage all →" (inside the
pending users card), and the three action buttons "Create new incident", "Manage
incidents", and "Go to Admin Panel" (inside the div with id "actionButtons") to
point to /admin.html, /create-incident.html, /incidents.html, and /admin.html
respectively; leave the th:if expressions as-is.

594-714: ⚠️ Potential issue | 🔴 Critical

Entire dashboard depends on Thymeleaf that won't run in a static file.

dashboard.html is served from src/main/resources/static/, so no th:* attribute is ever processed. In particular:

  • Line 594: data-role, data-user-id, data-user-active never get written → userRole, userId, userActive at lines 718-720 are all undefined/"false". The DOMContentLoaded handler at lines 987-994 then skips loadIncidents() and loadPendingUsers() on every page load, so stats/lists stay at their placeholder text.
  • Lines 645-646: user name and role-badge never populate.
  • Lines 657, 665, 672, 690, 703, 709-711, 603: every th:if branch is ignored, so the "Account Deactivated" card, "pending approval" card, and role-scoped action buttons are either all rendered together as static markup or all missing, regardless of the user.

This page (and the other static HTML files flagged) needs to fetch user/role via a REST endpoint on load, render the right branch client-side, and set the role/userId on document.body.dataset itself.

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

In `@src/main/resources/static/dashboard.html` around lines 594 - 714, The
dashboard currently relies on Thymeleaf-only attributes (th:*) in
static/dashboard.html so client-side code never gets role/user info; replace
that by fetching the current user from your REST user endpoint (e.g., GET
/api/me) in the DOMContentLoaded handler, then set document.body.dataset.role,
dataset.userId and dataset.userActive from the JSON response and update DOM
nodes (querySelector('.user-name'), querySelector('.role-badge'),
`#totalIncidents`, `#openIncidents`, `#resolvedIncidents`, `#pendingUsersCard`,
`#incidentsList`, `#actionButtons`, `#dropdownUsersList`, etc.) to show/hide content
based on role/active state; finally ensure after setting these datasets you call
loadIncidents() and loadPendingUsers() as needed so stats/lists populate.
src/main/resources/static/viewincident.html (2)

215-243: ⚠️ Potential issue | 🟠 Major

Document links use /documents/... but the controller is now under /api/documents.

DocumentController was remapped to @RequestMapping("/api/documents"), so these links need /api/documents/ as the base. As written, clicking an image/file opens a 404.

Proposed fix
-                    const fileUrl = "/documents/" + encodeURIComponent(doc.fileKey);
+                    const fileUrl = "/api/documents/" + encodeURIComponent(doc.fileKey);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/viewincident.html` around lines 215 - 243, The
document links are using "/documents/" but the controller was remapped to
"/api/documents"; update the construction of fileUrl in the
incident.documents.map block (where fileUrl = "/documents/" +
encodeURIComponent(doc.fileKey)) to use "/api/documents/" as the base so both
the anchor href and img src point to "/api/documents/{encodedKey}"; keep the
existing encodeURIComponent, isImage detection, and escapeHtml(doc.fileName)
logic intact so only the base path is changed to match DocumentController's
`@RequestMapping`("/api/documents").

145-183: ⚠️ Potential issue | 🔴 Critical

Page is unreachable at the URL the app navigates to, and Thymeleaf inputs render as literals.

  • getIncidentIdFromUrl() (line 186) assumes the page is served at a path like /incidents.html/{id}, but incidents.html and viewincident.html are both static files. Requests to /incidents.html/5 don't match any static resource or controller — they 404. Either name this file to match the URL and add a proper controller (or static resource mapping) for /incidents/{id}, or switch the navigation to use a query string (e.g. viewincident.html?id=5) and update getIncidentIdFromUrl() to read URLSearchParams.
  • body th:attr="data-role=${role}", the th:name/th:value CSRF inputs (lines 163-164), and th:value="${userId}" (line 165) are not processed from static/, so the hidden incidentId/userId/CSRF fields submit empty. The comment form will fail CSRF and the backend user/incident binding.
  • Line 162 still posts to /comments (the extensionless Thymeleaf form-redirect controller). With the page being vanilla JS elsewhere, this should go through a REST endpoint with a JSON body and explicit CSRF handling.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/viewincident.html` around lines 145 - 183, The page
is unreachable and Thymeleaf expressions are not processed because
viewincident.html is served as a static file and getIncidentIdFromUrl() assumes
a path-style ID; either move this file into Thymeleaf templates and expose a
controller for /incidents/{id} so body th:attr="data-role=${role}" and the
hidden CSRF/userId inputs are rendered, or keep it static and remove all th:*
usages and instead populate data-role, incidentIdInput, userId and CSRF token in
JS at runtime (e.g., read a meta tag or an authenticated API). Also stop posting
the form directly to the Thymeleaf controller at "/comments": change the form to
submit via fetch to a REST endpoint (e.g., /api/comments) with a JSON body and
include the CSRF token in the header; update getIncidentIdFromUrl() to read the
ID from URLSearchParams if you switch to query-string navigation
(viewincident.html?id=5), or keep path routing only if you add the controller.
Ensure references: getIncidentIdFromUrl(), body th:attr="data-role=${role}", the
hidden inputs (incidentIdInput, userId, CSRF parameterName), and the form action
"/comments" are updated accordingly.
src/main/java/org/example/team6backend/document/controller/DocumentController.java (1)

79-93: ⚠️ Potential issue | 🔴 Critical

Missing authorization check — IDOR on file download.

Unlike getFile (lines 48-51), downloadFile resolves the document by fileKey and streams it without verifying that the requesting user has access to the parent incident. Any authenticated principal (including PENDING users, and anyone who learns or guesses a fileKey) can retrieve any stored document. This is a direct-object-reference authorization bypass.

🔒 Proposed fix — mirror the incident access check from getFile
 	`@GetMapping`("/{fileKey}/download")
 	public ResponseEntity<InputStreamResource> downloadFile(`@PathVariable` String fileKey,
 			`@AuthenticationPrincipal` CustomUserDetails userDetails) {
 		AppUser user = userDetails.getUser();

 		Document document = documentService.getByFileKey(fileKey)
 				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

+		Incident incident = incidentService.getById(document.getIncident().getId(), user);
+		if (incident == null) {
+			throw new ResponseStatusException(HttpStatus.NOT_FOUND);
+		}
+
 		InputStream stream = minioService.getFile(fileKey);

 		return ResponseEntity.ok()
 				.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + document.getFileName() + "\"")
 				.contentType(MediaType.parseMediaType(document.getContentType())).body(new InputStreamResource(stream));

 	}

Also consider extracting the document-resolve + incident-authorize logic into a shared private helper since both endpoints need it.

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

In
`@src/main/java/org/example/team6backend/document/controller/DocumentController.java`
around lines 79 - 93, The downloadFile endpoint currently streams a file without
verifying the requesting user's access; update downloadFile to mirror the
incident-access check used by getFile: after resolving the Document via
documentService.getByFileKey(fileKey) and before calling
minioService.getFile(fileKey), verify that userDetails.getUser() is authorized
for the Document's parent incident (the same logic or service call used in
getFile) and throw a ResponseStatusException(HttpStatus.FORBIDDEN) when
unauthorized; to avoid duplication, extract the "resolve document + authorize
incident" sequence into a shared private helper (used by both getFile and
downloadFile).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/example/team6backend/config/SecurityConfig.java`:
- Around line 23-33: SecurityConfig: update the URL matchers and CSRF ignores to
cover the missing subpaths and document upload API and ensure extensionless
routes are protected; change exact matchers like
requestMatchers("/incidents.html", "/profile.html") to include subpaths (e.g.
"/incidents.html/**", "/admin.html/**" where applicable) so requests like
"/incidents.html/{id}" and admin subpaths are role-restricted, add
"/api/documents/**" to the csrf(...).ignoringRequestMatchers list to avoid CSRF
failures for POST /api/documents/upload/{incidentId}, and either add equivalent
requestMatchers for the extensionless routes ("/dashboard", "/incidents",
"/admin", "/create-incident") with the same role rules or coordinate callers to
use the .html paths so role checks remain consistent.

In `@src/main/resources/static/incidents.html`:
- Around line 345-347: The client-side navigation in function viewIncident uses
a path `/incidents.html/${id}` that 404s; update viewIncident to navigate to the
static detail page with a query param (e.g., `viewincident.html?id=${id}`) and
then read the id via URLSearchParams in viewincident.html, and apply the same
change to the two identical navigations in dashboard.html (the occurrences
around the functions that call viewIncident/navigation) so the detail page is
reachable; alternatively, if you prefer server-side routing, add a controller
mapping `/incidents/{id}` → forward to the static `viewincident.html` and keep
the original `/incidents/{id}` navigation, but pick one approach and make the
corresponding code changes to the function viewIncident and the dashboard
navigation callers.

---

Outside diff comments:
In
`@src/main/java/org/example/team6backend/document/controller/DocumentController.java`:
- Around line 79-93: The downloadFile endpoint currently streams a file without
verifying the requesting user's access; update downloadFile to mirror the
incident-access check used by getFile: after resolving the Document via
documentService.getByFileKey(fileKey) and before calling
minioService.getFile(fileKey), verify that userDetails.getUser() is authorized
for the Document's parent incident (the same logic or service call used in
getFile) and throw a ResponseStatusException(HttpStatus.FORBIDDEN) when
unauthorized; to avoid duplication, extract the "resolve document + authorize
incident" sequence into a shared private helper (used by both getFile and
downloadFile).

In `@src/main/resources/static/admin.html`:
- Around line 778-782: The navbar still contains Thymeleaf attributes that won't
be processed in this static admin.html; replace the inert th:text usage by
adding client-side code to fetch the current user (GET /api/users/me) and then
set the DOM textContent for the .user-name element (use user.name ||
user.githubLogin) and the .role-badge element (use user.role or response.role),
keeping the existing logout() button behavior; locate the .user-name and
.role-badge nodes in the script that initializes the page (or add a small init
function) and update them after the fetch, handling errors by leaving the
fallback text unchanged or logging to console.

In `@src/main/resources/static/createincident.html`:
- Around line 93-139: The form in createincident.html is broken because it
contains Thymeleaf attributes that are not processed in static resources;
replace the template directives by converting the form to plain HTML with
explicit name attributes (e.g., inputs for subject, description,
incidentCategory, files) and remove th:* attributes, then implement a small JS
module that (1) retrieves a CSRF token (via a meta tag or a GET to /api/csrf),
(2) posts JSON to the REST create endpoint (POST /api/incidents) to create the
incident, (3) on success uploads files via multipart to POST
/api/documents/upload/{incidentId}, and (4) displays validation/server errors in
the DOM instead of using `#fields.hasErrors`; apply the same conversion pattern to
profile.html, admin.html, dashboard.html, viewincident.html, and incidents.html
so no th:* attributes remain in static files.

In `@src/main/resources/static/dashboard.html`:
- Around line 693-712: Update the hard-coded hrefs to use the .html routes so
they match the new pages and SecurityConfig matchers: change the anchor with
text "Manage all →" (inside the pending users card), and the three action
buttons "Create new incident", "Manage incidents", and "Go to Admin Panel"
(inside the div with id "actionButtons") to point to /admin.html,
/create-incident.html, /incidents.html, and /admin.html respectively; leave the
th:if expressions as-is.
- Around line 594-714: The dashboard currently relies on Thymeleaf-only
attributes (th:*) in static/dashboard.html so client-side code never gets
role/user info; replace that by fetching the current user from your REST user
endpoint (e.g., GET /api/me) in the DOMContentLoaded handler, then set
document.body.dataset.role, dataset.userId and dataset.userActive from the JSON
response and update DOM nodes (querySelector('.user-name'),
querySelector('.role-badge'), `#totalIncidents`, `#openIncidents`,
`#resolvedIncidents`, `#pendingUsersCard`, `#incidentsList`, `#actionButtons`,
`#dropdownUsersList`, etc.) to show/hide content based on role/active state;
finally ensure after setting these datasets you call loadIncidents() and
loadPendingUsers() as needed so stats/lists populate.

In `@src/main/resources/static/incidents.html`:
- Around line 173-199: The page reads role from document.body.dataset.role (set
via th:attr="data-role=${role}") but since the HTML is now static role is
undefined, causing loadIncidents() to always hit the unknown-role branch; update
loadIncidents (or an init function called before it) to retrieve the current
user's role from the backend (e.g., call a /api/users/me or /api/auth/role
endpoint) and set a local variable (role) before the existing branching logic,
or alternatively read role from a backend-set cookie/header and use that value
instead of document.body.dataset.role; ensure you reference and update the same
symbol loadIncidents and remove reliance on document.body.dataset.role so the
ADMIN/HANDLER/RESIDENT branches work correctly.

In `@src/main/resources/static/profile.html`:
- Around line 213-218: Update the hardcoded anchor hrefs in profile.html so they
point to the new .html pages: change the Incidents link (the <a th:if="${role !=
'PENDING'}" href="/incidents.html">) and the Profile link (currently
href="/profile") to use /incidents.html and /profile.html respectively, and also
update the Admin link inside the <div th:if="${role == 'ADMIN'}"> to
href="/admin.html" so these anchors match the SecurityConfig .html matchers and
avoid 404s; keep the existing th:if conditions unchanged.
- Around line 1-295: The file still uses Thymeleaf directives but now lives in
static/—replace server-side rendering with client-side rendering: in the
existing DOMContentLoaded handler (document.addEventListener("DOMContentLoaded",
...)) fetch the current user from /api/users/me, then populate DOM nodes by
id/class: set .user-name text, .info-value nodes for display
name/email/githubLogin, set the profile-avatar img src or create the
avatar-placeholder text, set .role-badge text and add the role class
(role.toLowerCase()), show/hide the Admin nav link and Incidents link based on
role, and write the formatted createdAt string into `#accountCreated` using the
existing formatDate function; ensure logout() stays intact and remove/ignore all
th:* attributes so the page works purely from the fetched JSON.

In `@src/main/resources/static/viewincident.html`:
- Around line 215-243: The document links are using "/documents/" but the
controller was remapped to "/api/documents"; update the construction of fileUrl
in the incident.documents.map block (where fileUrl = "/documents/" +
encodeURIComponent(doc.fileKey)) to use "/api/documents/" as the base so both
the anchor href and img src point to "/api/documents/{encodedKey}"; keep the
existing encodeURIComponent, isImage detection, and escapeHtml(doc.fileName)
logic intact so only the base path is changed to match DocumentController's
`@RequestMapping`("/api/documents").
- Around line 145-183: The page is unreachable and Thymeleaf expressions are not
processed because viewincident.html is served as a static file and
getIncidentIdFromUrl() assumes a path-style ID; either move this file into
Thymeleaf templates and expose a controller for /incidents/{id} so body
th:attr="data-role=${role}" and the hidden CSRF/userId inputs are rendered, or
keep it static and remove all th:* usages and instead populate data-role,
incidentIdInput, userId and CSRF token in JS at runtime (e.g., read a meta tag
or an authenticated API). Also stop posting the form directly to the Thymeleaf
controller at "/comments": change the form to submit via fetch to a REST
endpoint (e.g., /api/comments) with a JSON body and include the CSRF token in
the header; update getIncidentIdFromUrl() to read the ID from URLSearchParams if
you switch to query-string navigation (viewincident.html?id=5), or keep path
routing only if you add the controller. Ensure references:
getIncidentIdFromUrl(), body th:attr="data-role=${role}", the hidden inputs
(incidentIdInput, userId, CSRF parameterName), and the form action "/comments"
are updated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d177e49-de86-42ba-a7d9-156cad111eab

📥 Commits

Reviewing files that changed from the base of the PR and between 90400b1 and 9e864f5.

📒 Files selected for processing (10)
  • src/main/java/org/example/team6backend/config/SecurityConfig.java
  • src/main/java/org/example/team6backend/document/controller/DocumentController.java
  • src/main/java/org/example/team6backend/page/PageController.java
  • src/main/resources/static/admin.html
  • src/main/resources/static/createincident.html
  • src/main/resources/static/dashboard.html
  • src/main/resources/static/incidents.html
  • src/main/resources/static/index.html
  • src/main/resources/static/profile.html
  • src/main/resources/static/viewincident.html
💤 Files with no reviewable changes (1)
  • src/main/java/org/example/team6backend/page/PageController.java

Comment thread src/main/java/org/example/team6backend/config/SecurityConfig.java Outdated
Comment thread src/main/resources/static/incidents.html

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/java/org/example/team6backend/document/controller/DocumentController.java (2)

68-81: ⚠️ Potential issue | 🟡 Minor

uploadFile missing null-check on userDetails.

getFile was hardened against a missing principal (lines 43‑45), but uploadFile still calls userDetails.getUser() directly on line 72. If Spring Security ever resolves an anonymous/no principal here (e.g., an endpoint-level permit-all rule, or a misconfiguration), this produces an opaque 500 NPE instead of a clean 401. Mirror the check from getFile for consistency.

🛠️ Suggested change
 	public ResponseEntity<Void> uploadFile(`@PathVariable` Long incidentId,
 			`@RequestParam`("files") List<MultipartFile> files, `@AuthenticationPrincipal` CustomUserDetails userDetails) {
 
+		if (userDetails == null) {
+			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
+		}
 		AppUser user = userDetails.getUser();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/team6backend/document/controller/DocumentController.java`
around lines 68 - 81, The uploadFile method calls userDetails.getUser() without
checking for a null principal; mirror the null-check used in getFile by first
verifying CustomUserDetails userDetails is not null and returning a
401/Unauthorized (or throwing the same AuthenticationException used in getFile)
when it is null, before calling userDetails.getUser() and proceeding to call
incidentService.getById and documentService.uploadFile.

83-97: ⚠️ Potential issue | 🔴 Critical

Authorization bypass: /{fileKey}/download doesn't verify the caller owns the incident.

Compared to getFile (lines 49‑55), this endpoint:

  1. Does not null-check userDetails (will NPE at userDetails.getUser() if the Security filter is ever relaxed for this route).
  2. Fetches user on line 86 but then never uses it to validate access — there is no incidentService.getById(document.getIncident().getId(), user) check. Any authenticated user who knows (or guesses) a fileKey can download any document belonging to any incident. This is an authorization bypass that contradicts the authorization model used by the sibling inline endpoint.
  3. MediaType.parseMediaType(document.getContentType()) will throw InvalidMediaTypeException if contentType is null — getFile already guards this (lines 59‑61) but downloadFile does not.

Given the two handlers only differ by Content-Disposition (inline vs attachment), consider collapsing them or extracting a private helper so the authorization and null-safety logic can’t drift again.

🛠️ Suggested minimum fix
 	`@GetMapping`("/{fileKey}/download")
 	public ResponseEntity<InputStreamResource> downloadFile(`@PathVariable` String fileKey,
 			`@AuthenticationPrincipal` CustomUserDetails userDetails) {
-		AppUser user = userDetails.getUser();
-
-		Document document = documentService.getByFileKey(fileKey)
-				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
-
-		InputStream stream = minioService.getFile(fileKey);
-
-		return ResponseEntity.ok()
-				.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + document.getFileName() + "\"")
-				.contentType(MediaType.parseMediaType(document.getContentType())).body(new InputStreamResource(stream));
-
+		if (userDetails == null) {
+			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
+		}
+		AppUser user = userDetails.getUser();
+
+		Document document = documentService.getByFileKey(fileKey)
+				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+
+		Incident incident = incidentService.getById(document.getIncident().getId(), user);
+		if (incident == null) {
+			throw new ResponseStatusException(HttpStatus.NOT_FOUND);
+		}
+
+		MediaType mediaType = document.getContentType() != null
+				? MediaType.parseMediaType(document.getContentType())
+				: MediaType.APPLICATION_OCTET_STREAM;
+
+		InputStream stream = minioService.getFile(fileKey);
+		return ResponseEntity.ok()
+				.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + document.getFileName() + "\"")
+				.contentType(mediaType)
+				.body(new InputStreamResource(stream));
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/team6backend/document/controller/DocumentController.java`
around lines 83 - 97, downloadFile in DocumentController currently dereferences
userDetails without null-check, never validates the caller owns the incident,
and can pass a null contentType into MediaType.parseMediaType; update
downloadFile to mirror getFile: null-check userDetails before calling
userDetails.getUser(), fetch the document via
documentService.getByFileKey(fileKey), then call
incidentService.getById(document.getIncident().getId(), user) (or the same
ownership-check helper used by getFile) to enforce authorization, and guard
against null document.getContentType() before calling MediaType.parseMediaType
(use a default or omit contentType header if null). Consider extracting a
private helper used by getFile and downloadFile to return the validated
Document/MediaType/InputStreamResource to avoid divergence.
src/main/java/org/example/team6backend/document/service/MinioService.java (1)

56-62: ⚠️ Potential issue | 🟡 Minor

Use a proper logger and reconsider silently swallowing MinIO deletion failures.

Two concerns on the new behavior:

  1. System.err.println should be replaced with an SLF4J logger (the rest of the codebase uses @Slf4j, e.g. DocumentService). System.err bypasses log formatting, levels, MDC, and log aggregation.
  2. Unconditionally suppressing the exception turns a previously loud failure into a silent one. The caller IncidentService (lines 94‑102 of IncidentService.java) already wraps deleteFile in try/catch expecting exceptions during rollback cleanup — now that branch will never log anything useful and rollback failures become invisible. Consider keeping the exception-throwing contract (and letting callers decide), or at minimum log at WARN/ERROR via SLF4J with the stack trace.
🛠️ Suggested change
+import lombok.extern.slf4j.Slf4j;
 ...
+@Slf4j
 `@Service`
 `@RequiredArgsConstructor`
 public class MinioService {
 ...
 	public void deleteFile(String fileKey) {
 		try {
 			minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileKey).build());
 		} catch (Exception e) {
-			System.err.println("MinIO delete failed for " + fileKey + ": " + e.getMessage());
+			log.warn("MinIO delete failed for fileKey={}", fileKey, e);
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/team6backend/document/service/MinioService.java`
around lines 56 - 62, The deleteFile method currently swallows exceptions using
System.err; replace that with SLF4J logging and preserve the exception contract
so callers (like IncidentService) can handle rollback failures. Add/ensure
`@Slf4j` on MinioService, change the catch to log.error("MinIO delete failed for
{}", fileKey, e) (include the stack trace) and then rethrow the exception (or
wrap and throw a RuntimeException) instead of silently returning; alternatively,
remove the try/catch so the original exception propagates.
🧹 Nitpick comments (1)
src/test/java/org/example/team6backend/document/controller/DocumentControllerTest.java (1)

62-88: Consider adding coverage for the new /{fileKey}/download endpoint and the 401 path.

The existing tests exercise GET /api/documents/{fileKey} (inline response) and the 404 path, which align with the controller changes. However, the PR introduces:

  • A new GET /api/documents/{fileKey}/download endpoint (attachment) which is completely untested.
  • An explicit ResponseStatusException(HttpStatus.UNAUTHORIZED) when userDetails == null in getFile, which isn’t exercised (the @BeforeEach always installs an authenticated principal).

Adding tests for both would lock in the new REST semantics.

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

In
`@src/test/java/org/example/team6backend/document/controller/DocumentControllerTest.java`
around lines 62 - 88, Add two tests in DocumentControllerTest: one named like
getFile_download_shouldReturnAttachment that calls GET
"/api/documents/abc/download" and verifies the mocked Document returned by
documentService.getByFileKey("abc"), incidentService.getById(1L, ..) and
minioService.getFile("abc") produce status 200 and header Content-Disposition
equals "attachment; filename=\"test.pdf\"" (mirror the existing getFile test but
expect "attachment" instead of "inline"); and another named
getFile_shouldReturn401_whenUnauthenticated that invokes the existing GET
"/api/documents/abc" without the authenticated principal (use
SecurityMockMvcRequestPostProcessors.anonymous() or omit the default
authenticated setup used in `@BeforeEach`) and assert status is 401, ensuring
documentService.getByFileKey and minioService are not required for that test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/org/example/team6backend/document/controller/DocumentController.java`:
- Around line 24-25: Update viewincident.html to reference the new endpoint by
replacing the hardcoded "/documents/" file URL with "/api/documents/" so
document retrieval uses the new REST path; then in SecurityConfig (the
HttpSecurity configuration method where "/api/incidents/**" is authorized) add
an explicit authorization rule for "/api/documents/**" with the same
role(s)/hasAuthority settings used for "/api/incidents/**" so the endpoint is
matched before anyRequest().authenticated().

In
`@src/main/java/org/example/team6backend/document/service/DocumentService.java`:
- Around line 61-69: The current DocumentService.deleteFile method catches
Throwable and swallows repository failures; change it so the DB delete
(documentRepository.delete(document)) is invoked without a blanket catch so
repository exceptions propagate (retain the `@Transactional` behavior), then call
minioService.deleteFile(document.getFileKey()) inside a separate try/catch that
catches Exception (not Throwable) and logs the MinIO failure; reference
DocumentService.deleteFile, documentRepository.delete, and
minioService.deleteFile, and note that if MinioService.deleteFile already
swallows exceptions you can remove the inner try/catch entirely.

---

Outside diff comments:
In
`@src/main/java/org/example/team6backend/document/controller/DocumentController.java`:
- Around line 68-81: The uploadFile method calls userDetails.getUser() without
checking for a null principal; mirror the null-check used in getFile by first
verifying CustomUserDetails userDetails is not null and returning a
401/Unauthorized (or throwing the same AuthenticationException used in getFile)
when it is null, before calling userDetails.getUser() and proceeding to call
incidentService.getById and documentService.uploadFile.
- Around line 83-97: downloadFile in DocumentController currently dereferences
userDetails without null-check, never validates the caller owns the incident,
and can pass a null contentType into MediaType.parseMediaType; update
downloadFile to mirror getFile: null-check userDetails before calling
userDetails.getUser(), fetch the document via
documentService.getByFileKey(fileKey), then call
incidentService.getById(document.getIncident().getId(), user) (or the same
ownership-check helper used by getFile) to enforce authorization, and guard
against null document.getContentType() before calling MediaType.parseMediaType
(use a default or omit contentType header if null). Consider extracting a
private helper used by getFile and downloadFile to return the validated
Document/MediaType/InputStreamResource to avoid divergence.

In `@src/main/java/org/example/team6backend/document/service/MinioService.java`:
- Around line 56-62: The deleteFile method currently swallows exceptions using
System.err; replace that with SLF4J logging and preserve the exception contract
so callers (like IncidentService) can handle rollback failures. Add/ensure
`@Slf4j` on MinioService, change the catch to log.error("MinIO delete failed for
{}", fileKey, e) (include the stack trace) and then rethrow the exception (or
wrap and throw a RuntimeException) instead of silently returning; alternatively,
remove the try/catch so the original exception propagates.

---

Nitpick comments:
In
`@src/test/java/org/example/team6backend/document/controller/DocumentControllerTest.java`:
- Around line 62-88: Add two tests in DocumentControllerTest: one named like
getFile_download_shouldReturnAttachment that calls GET
"/api/documents/abc/download" and verifies the mocked Document returned by
documentService.getByFileKey("abc"), incidentService.getById(1L, ..) and
minioService.getFile("abc") produce status 200 and header Content-Disposition
equals "attachment; filename=\"test.pdf\"" (mirror the existing getFile test but
expect "attachment" instead of "inline"); and another named
getFile_shouldReturn401_whenUnauthenticated that invokes the existing GET
"/api/documents/abc" without the authenticated principal (use
SecurityMockMvcRequestPostProcessors.anonymous() or omit the default
authenticated setup used in `@BeforeEach`) and assert status is 401, ensuring
documentService.getByFileKey and minioService are not required for that test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa73fcd5-5883-4e0a-a578-7bd0f3a63ea3

📥 Commits

Reviewing files that changed from the base of the PR and between 9e864f5 and 0288abe.

📒 Files selected for processing (4)
  • src/main/java/org/example/team6backend/document/controller/DocumentController.java
  • src/main/java/org/example/team6backend/document/service/DocumentService.java
  • src/main/java/org/example/team6backend/document/service/MinioService.java
  • src/test/java/org/example/team6backend/document/controller/DocumentControllerTest.java

@SandraNelj
SandraNelj merged commit 43fca1e into main Apr 22, 2026
3 checks passed
@SandraNelj
SandraNelj deleted the fix/refactorRESTapi branch April 22, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant