Skip to content

Feature/employment - #81

Merged
gitnes94 merged 22 commits into
mainfrom
feature/employment
Apr 22, 2026
Merged

Feature/employment#81
gitnes94 merged 22 commits into
mainfrom
feature/employment

Conversation

@Ericthilen

@Ericthilen Ericthilen commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

🚀 Employment Management + Role Update

🧩 Vad som har gjorts

Denna PR introducerar en ny Employment-sektion i dashboarden samt uppdaterar roller/struktur för användare.


👥 Employment (ny funktion)

Ny sida: /pages/employment.html

Funktionalitet:

  • Visa alla anställda

  • Sök + filtrera på roll och department

  • Skapa ny staff (endast HR & ADMIN)

  • Approval-flöde:

    • CTO & CEO kan approve/reject

    • Vid approve skapas en riktig user i systemet


🔐 Rollbaserad access

Funktion | Roller -- | -- Skapa staff | HR, ADMIN Approve staff | CEO, CTO Se anställda | Alla

🧱 Strukturförbättring (frontend)

  • Separat sida för employment

  • Tydligare uppdelning av funktioner i JS

  • Återanvänder befintliga API-endpoints (/forms, /staff)



🧪 Hur man testar

1. HR / ADMIN

  • Gå till Employment

  • Klicka “Lägg till staff”

  • Skapa ny person

  • Kontrollera att den hamnar i Pending

2. CEO / CTO

  • Gå till Employment

  • Se Pending approvals

  • Klicka Approve

  • Verifiera att personen dyker upp i “Alla anställda”


📦 Påverkan

  • Ingen backend-ändring

  • Endast frontend + databasupdate (role/department)


✅ Resultat

  • Fullt fungerande employment flow

  • Tydlig rollhantering

  • Bättre struktur i dashboarden

Summary by CodeRabbit

  • New Features

    • OAuth sign-in now captures profile pictures and sets a default ONLINE status.
    • New current-user endpoints to view profile and update status.
    • New Employment page for submitting and approving staff requests; client-side employment workflows added.
  • Improvements

    • Frontend reorganized into shared utilities and per-page scripts; navbar and profile UI enhanced.
    • Staff API and database now include profile picture and status; sidebar and employment styles updated.
  • Other

    • Logout and routing behavior updated; service/controller authorization and tests adjusted.

… UI for navbar and async rendering; update API endpoints with Staff status management
…staff management; implement real-time status updates, ticket CRUD functionality, and employment handling
@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@gitnes94 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 29 minutes and 24 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 29 minutes and 24 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: fbe50cb0-d009-4cd0-901f-3f5ac8920174

📥 Commits

Reviewing files that changed from the base of the PR and between d3d57d2 and 1152aaa.

📒 Files selected for processing (3)
  • src/main/resources/static/js/common.js
  • src/main/resources/static/js/dashboard.js
  • src/main/resources/static/js/ticket-detail.js
📝 Walkthrough

Walkthrough

Adds profilePictureUrl and status to Staff (DB, entity, DTO, mapper, migration); OAuth2 success handler persists picture and ensures default "ONLINE"; exposes current-user endpoints and status update; replaces monolithic frontend bundle with shared common.js + page scripts; changes employment controller/service to accept Staff principal and updates tests and routing.

Changes

Cohort / File(s) Summary
Auth & Staff model
src/main/java/org/example/cyberwatch/config/security/OAuth2SuccessHandler.java, src/main/java/org/example/cyberwatch/features/staff/model/Staff.java, src/main/java/org/example/cyberwatch/features/staff/model/StaffDTO.java, src/main/java/org/example/cyberwatch/features/staff/model/StaffMapper.java, src/main/resources/db/migration/V10__add_profile_picture_and_status_to_staff.sql
Added profilePictureUrl and status fields and migration; OAuth2SuccessHandler now extracts picture, sets default "ONLINE" when blank, and persists Staff before replacing principal; DTO and mapper updated.
Staff API & service
src/main/java/org/example/cyberwatch/features/staff/controller/StaffRestController.java, src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java
Added GET /api/staff/me and PATCH /api/staff/me/status; introduced ALLOWED_STATUSES and updateStatus(...) with validation and persistence; controller endpoints use authenticated Staff principal.
Employment backend
src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java, src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
Controllers resolve Staff principal and pass Staff to service; service signatures changed from email String to Staff, removed internal email lookups, adjusted ownership/role checks (including ADMIN for some approvals), and updated logging.
Security & routing
src/main/java/org/example/cyberwatch/config/SecurityConfig.java, src/main/java/org/example/cyberwatch/config/WebConfig.java
Permit anonymous access to /login and /dashboard; configure logout redirect; add MVC view-controller mappings for /, /login, and /dashboard.
Frontend shared utilities
src/main/resources/static/js/common.js
New shared JS for CSRF handling, safe HTML escaping, apiFetch() (401→logout), renderNavbar() (calls /api/staff/me, shows profile/status) and loadStaffList().
Frontend feature scripts
src/main/resources/static/js/create-ticket.js, src/main/resources/static/js/dashboard.js, src/main/resources/static/js/edit-ticket.js, src/main/resources/static/js/ticket-detail.js, src/main/resources/static/js/employment.js
Replaced monolithic app.js with page-specific modules implementing create, dashboard, edit, ticket-detail (comments/uploads/assign), and employment behaviors.
Frontend pages & bootstrap
src/main/resources/static/pages/*.html, src/main/resources/static/js/app.js
Pages now load common.js + page scripts and initialize via DOMContentLoaded init* functions; removed large app.js bundle and moved some globals into DOM-ready scope.
Styling
src/main/resources/static/css/styles.css
Added CSS for sidebar profile block, status dropdown, employment list/cards, and employment form layout.
Tests
src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java, src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java
Updated tests to include new Staff fields/status and to pass Staff objects to employment service tests; added updateStatus unit test; removed tests relying on repository email lookups.
Client JS removal/addition
src/main/resources/static/js/app.js, src/main/resources/static/js/common.js
Removed legacy app.js (−410 lines); introduced common.js and multiple focused page scripts that reimplement client flows.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Client (Browser)
    participant OAuth as OAuth2 Provider
    participant App as Server (App)
    participant DB as Database (staff table)

    Browser->>OAuth: authenticate (OAuth2)
    OAuth->>App: callback with OAuth2User info
    App->>App: OAuth2SuccessHandler extracts picture & email
    App->>DB: find Staff by email
    DB-->>App: Staff entity (or null)
    alt Staff exists
        App->>App: if picture present -> set profilePictureUrl
        App->>App: if status blank -> set status = "ONLINE"
        App->>DB: save Staff
    end
    App-->>Browser: establish session / replace principal

    Browser->>App: GET /api/staff/me
    App->>DB: load Staff by id (from principal)
    DB-->>App: Staff data (incl. profilePictureUrl,status)
    App-->>Browser: 200 OK with StaffDTO
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • gitnes94
  • alicewersen-rgb

Poem

🐰 I nibbled through the code with care,

Saved a picture, set status fair,
Navbar blooms with profile light,
Pages wake when DOM's polite,
A little rabbit claps—hoppity hooray!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Feature/employment' is vague and generic, using a branch naming convention rather than a descriptive summary of the actual changes. Use a more specific title that summarizes the main feature, such as 'Add employment management page with role-based approval workflow' or 'Implement staff employment form submission and approval flow'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 feature/employment

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: 14

Caution

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

⚠️ Outside diff range comments (1)
src/main/resources/static/js/app.js (1)

218-245: ⚠️ Potential issue | 🟠 Major

Avoid dropping existing assignees from multi-assignee tickets.

The UI marks assigned staff with selected, but this is a single-select and the update sends only [staffId]. Changing one dashboard assignment will replace all existing assignees with one person, even though the backend DTO accepts a List<Long> in src/main/java/org/example/cyberwatch/features/ticket/model/AssignTicketDTO.java:1-12.

🐛 Proposed fix using a multi-select
-                    <select class="dashboard-assignment-select" data-id="${t.id}" style="max-width: 150px;">
-                        <option value="">Tilldela...</option>
+                    <select class="dashboard-assignment-select" data-id="${t.id}" multiple style="max-width: 150px;">
+                        <option value="" disabled>Tilldela...</option>
                         ${allStaff.map(staff => {
                 const isAssigned = assignedIds.includes(staff.id);
                 return `<option value="${staff.id}" ${isAssigned ? 'selected' : ''}>
                                 ${isAssigned ? '✓ ' : ''}${escapeHtml(staff.fullName)}
                             </option>`;
             }).join('')}
                     </select>
-                const staffId = parseInt(e.target.value);
-                if (!staffId) return;
+                const staffIds = Array.from(e.target.selectedOptions)
+                    .map(o => parseInt(o.value, 10))
+                    .filter(Number.isFinite);
+                if (staffIds.length === 0) return;
 
                 const res = await apiFetch(`/tickets/${ticketId}/assign`, {
                     method: "PUT",
-                    body: JSON.stringify({staffIds: [staffId]})
+                    body: JSON.stringify({staffIds})
                 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/js/app.js` around lines 218 - 245, The assignment
UI and handler drop existing assignees because .dashboard-assignment-select is
single-select and the change handler sends only [staffId] to
/tickets/${ticketId}/assign (which expects a List<Long> via AssignTicketDTO);
update the select element to allow multiple selection (make it a multi-select)
and modify the change handler to collect all selected option values (parse to
integers, filter out invalid) and send that array as staffIds in the PUT body to
preserve all assignees; keep existing option rendering (including selected
markers) but ensure options use value=staff.id and adjust any UI text if needed.
🧹 Nitpick comments (2)
src/main/java/org/example/cyberwatch/config/security/OAuth2SuccessHandler.java (1)

65-73: Avoid unconditional save on every login.

staffRepository.save(staff) runs on every successful OAuth2 login even when neither profilePictureUrl nor status changed, producing a needless DB write per login. Save only when a field was actually mutated.

♻️ Proposed change
-        if (picture != null && !picture.isBlank()) {
-            staff.setProfilePictureUrl(picture);
-        }
-
-        if (staff.getStatus() == null || staff.getStatus().isBlank()) {
-            staff.setStatus("ONLINE");
-        }
-
-        staffRepository.save(staff);
+        boolean dirty = false;
+        if (picture != null && !picture.isBlank()
+                && !picture.equals(staff.getProfilePictureUrl())) {
+            staff.setProfilePictureUrl(picture);
+            dirty = true;
+        }
+        if (staff.getStatus() == null || staff.getStatus().isBlank()) {
+            staff.setStatus("ONLINE");
+            dirty = true;
+        }
+        if (dirty) {
+            staffRepository.save(staff);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/cyberwatch/config/security/OAuth2SuccessHandler.java`
around lines 65 - 73, The current OAuth2SuccessHandler unconditionally calls
staffRepository.save(staff) on every login; change this to only persist when a
field actually changes by tracking mutations: read existing values, compare
incoming picture to staff.getProfilePictureUrl() and incoming status logic to
staff.getStatus(), set via staff.setProfilePictureUrl(...) and
staff.setStatus(...) only if different, maintain a boolean dirty flag (e.g.,
mutated) and call staffRepository.save(staff) only when mutated is true; update
references to staffRepository.save, staff.setProfilePictureUrl, and
staff.setStatus in OAuth2SuccessHandler accordingly.
src/main/java/org/example/cyberwatch/features/staff/model/Staff.java (1)

64-65: Consider modelling status as an enum.

role and department are typed as enums with @Enumerated(EnumType.STRING), but status is a free-form String. This diverges from the existing convention and lets any value be persisted (see related comment in StaffService.updateStatus). A StaffStatus enum (ONLINE, OFFLINE, AWAY, BUSY) would provide compile-time safety and DB-level consistency.

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

In `@src/main/java/org/example/cyberwatch/features/staff/model/Staff.java` around
lines 64 - 65, Change the free-form String status in the Staff model into a
typed enum: create a StaffStatus enum with values (ONLINE, OFFLINE, AWAY, BUSY),
update the Staff class field signature from String status to StaffStatus status
and annotate it with `@Enumerated`(EnumType.STRING) (same pattern as role and
department), update the Staff#getStatus/setStatus signatures and any usage in
StaffService.updateStatus to accept or convert to StaffStatus (validate inputs
or map incoming strings to enum values), and ensure persistence/migration will
store the enum as string for DB compatibility.
🤖 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/cyberwatch/features/staff/controller/StaffRestController.java`:
- Around line 34-40: The updateMyStatus controller accepts an unchecked raw
status string; validate it against the allowed values (ONLINE, BUSY, AWAY,
OFFLINE) before calling staffService.updateStatus to prevent persisting
unsupported values. In updateMyStatus(`@AuthenticationPrincipal` Staff staff,
`@RequestParam` String status) check the incoming status against the allowed set
(or a StaffStatus enum if one exists), and return
ResponseEntity.status(400).body(...) or ResponseEntity.badRequest().build() when
invalid; only call staffService.updateStatus(staff.getId(), status) when the
value is valid. Ensure the error response is clear and consistent with other
controller validations.

In
`@src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java`:
- Around line 91-96: The updateStatus method currently accepts any String; add
validation and an enum to constrain allowed values: create a StaffStatus enum
(ONLINE, OFFLINE, AWAY, BUSY), change the Staff.status field to use
`@Enumerated`(EnumType.STRING), and update updateStatus(Long staffId, String
status) to null-check staffId (consistent with other methods), reject null/blank
status, convert the incoming String to StaffStatus (throw
IllegalArgumentException for unknown values), set staff.setStatus(statusEnum)
and then save via staffRepository.save(staff) as before; ensure error messages
clearly identify invalid input.

In `@src/main/resources/static/js/app.js`:
- Around line 66-89: The profilePictureUrl is injected directly into
navbar.innerHTML via the profileImage variable, creating an XSS risk; add a
helper function (e.g., safeImageUrl) that validates and normalizes the value
(returning a safe fallback when null, non-HTTP(S), or parsing fails) and use
that helper to compute profileImage before injecting into the src attribute, and
ensure you use the normalized parsed href (not the raw string) so quotes/event
handlers cannot break the attribute.
- Around line 205-216: Replace the clickable <div> used for navigation inside
item.innerHTML with a real anchor element: remove the inline onclick handler and
render an <a> element (preserving the "ticket-main" class and inner structure)
with its href set to '/pages/ticket-detail.html?id=${t.id}' so keyboard and
assistive tech get native link semantics; ensure existing content (badge, title,
meta) and injected values (escapeHtml(t.title), priorityIcon,
assignedStaffNames) remain unchanged and the link styling/behavior is preserved
via CSS for the "ticket-main" class.

In `@src/main/resources/static/js/common.js`:
- Around line 56-79: Profile image URLs from currentUser.profilePictureUrl are
interpolated directly into navbar.innerHTML via profileImage and can contain
unsafe or non-HTTP(S) schemes; add a sanitizer function (e.g., safeImageUrl)
next to escapeHtml that validates and normalizes the URL using the URL
constructor and only allows http: and https: (falling back to
"https://via.placeholder.com/40" on invalid input), then use
safeImageUrl(currentUser?.profilePictureUrl) to set profileImage before
inserting into navbar.innerHTML so only safe image URLs are rendered.

In `@src/main/resources/static/js/dashboard.js`:
- Around line 55-66: The ticket card uses a div with an onclick handler (inside
item.innerHTML and class "ticket-main") which loses native link semantics and
keyboard accessibility; replace the clickable <div class="ticket-main"
onclick="..."> with a semantic anchor (<a
href="/pages/ticket-detail.html?id=${t.id}" class="ticket-main">) so the href
carries navigation, remove the inline onclick, preserve the inner structure
(ticket-header, ticket-meta, escaped title via escapeHtml(t.title), badges, and
assignedStaffNames) and ensure any styling that targeted .ticket-main still
applies; optionally add an aria-label to the anchor for clearer screen-reader
text.
- Line 39: The query string is built by string interpolation which allows
user-controlled searchInput (q) to inject characters like & = #; replace the
inline template with a URLSearchParams-based builder so values are properly
encoded: create a URLSearchParams with keys status, priority, search,
assignedStaffId (using the existing variables s, p, q, staffId) and then call
apiFetch(`/tickets?${params.toString()}`) instead of the current template;
update the code around the apiFetch call to use params.toString() so searchInput
is safely encoded.
- Around line 68-95: The assignment select currently renders multiple assigned
users as selected but the change handler in dashboard.js (the listener attached
to '.dashboard-assignment-select') reads only e.target.value and sends a single
staffId to the /tickets/{ticketId}/assign endpoint, which overwrites existing
assignees; update the UI and handler to submit the full list of selected
assignee IDs matching the backend AssignTicketDTO.staffIds: either convert the
<select> to a multi-select (allow multiple) and collect all selected option
values, or (if keeping single select UI) merge the newly chosen id with the
existing assignedIds for that ticket before sending; ensure the change handler
builds an array of ints and sends JSON.stringify({staffIds: [...]}) to the same
PUT endpoint so AssignTicketDTO receives the complete list.

In `@src/main/resources/static/js/edit-ticket.js`:
- Around line 27-33: The code builds a full edit payload in the body variable
(title, description, priority, status) but then calls apiFetch only to PATCH the
status via `/tickets/${id}/status?status=...`, so edited fields are dropped;
either send the full body to the ticket edit endpoint or restrict the form to
status-only. Fix by changing the apiFetch call to PATCH the full ticket (e.g.
apiFetch(`/tickets/${id}`, { method: "PATCH", body: JSON.stringify(body),
headers: { "Content-Type": "application/json" } })) so
title/description/priority are persisted, or remove creation of
title/description/priority and update the UI and form to only allow status edits
and keep the existing `/tickets/${id}/status` call.

In `@src/main/resources/static/js/employment.js`:
- Around line 226-230: The approval handler currently shows server response text
(variable text from the fetch response) in a browser alert which may contain a
generated password; change the UX to stop displaying credentials directly:
replace the alert("Godkänd!\n" + text) use with a generic success notification
(e.g. "Godkänd!" only) and implement a secure delivery mechanism for credentials
(e.g. server-sent email or a one-time link) or, if you must surface
non-credential info, parse the response from the fetch to strip any password
before displaying; update the same handler that calls
loadPendingEmploymentForms() so it no longer exposes text in the client alert.
- Around line 122-128: The staff list currently renders sensitive PII via
staff.socialSecurityNumber inside the employment-card-body; remove that
unconditional exposure by either removing the `<p><strong>Personnummer:</strong>
${escapeHtml(staff.socialSecurityNumber || "-")}</p>` line entirely or wrapping
it in an explicit role check (e.g., only render when current user has HR/Admin
privileges using your app's auth helper such as isUserInRole/currentUser.roles)
and otherwise render a masked value like "-" or "•••". Ensure you reference and
use the existing escapeHtml helper when conditionally rendering to avoid
introducing XSS.

In `@src/main/resources/static/js/ticket-detail.js`:
- Around line 53-78: The loadComments function can throw unhandled rejections on
network failures; wrap the fetch/JSON parsing logic inside loadComments in a
try/catch (around the calls to apiFetch and res.json()) and on error set the
comments container (list.innerHTML) to the error state (e.g. "<p>Kunde inte
hämta kommentarer.</p>") and optionally console.error the exception; also ensure
the caller (where loadComments(id) is invoked) either awaits the promise or
appends .catch(...) so failures don't become unhandled rejections — target the
loadComments function, the apiFetch call, the res.json() call, and the code that
calls loadComments(id).

In `@src/main/resources/static/pages/create-ticket.html`:
- Around line 92-100: The code uses badge.innerHTML with dynamic staffName which
enables XSS; change this to create text and control nodes instead: set
badge.className as before, append a text node or assign badge.textContent =
staffName, create a separate closeSpan via document.createElement('span'), set
closeSpan.style and closeSpan.textContent = '×', attach the click handler to
closeSpan that deletes selectedStaffIds, removes the badge, and calls
updateRequiredStatus(), then append closeSpan to badge and badge to
badgeContainer (replace the existing badge.innerHTML usage and current
badge.onclick logic).

In `@src/main/resources/static/pages/ticket-detail.html`:
- Around line 70-80: The badge creation in window.addStaffBadge currently uses
badge.innerHTML with untrusted name which can run markup; replace that by
creating child text/span nodes and a separate remove-button element instead of
injecting HTML: use a nameNode (set textContent) for the staff name, create a
removeIcon element (e.g., a span with textContent '×' and cursor styling),
attach the click handler to the removeIcon (or use addEventListener) to delete
the id from window.currentAssignedIds and remove the badge, and append nameNode
and removeIcon to badgeContainer; update references to badge.onclick and
badge.innerHTML accordingly in window.addStaffBadge.

---

Outside diff comments:
In `@src/main/resources/static/js/app.js`:
- Around line 218-245: The assignment UI and handler drop existing assignees
because .dashboard-assignment-select is single-select and the change handler
sends only [staffId] to /tickets/${ticketId}/assign (which expects a List<Long>
via AssignTicketDTO); update the select element to allow multiple selection
(make it a multi-select) and modify the change handler to collect all selected
option values (parse to integers, filter out invalid) and send that array as
staffIds in the PUT body to preserve all assignees; keep existing option
rendering (including selected markers) but ensure options use value=staff.id and
adjust any UI text if needed.

---

Nitpick comments:
In
`@src/main/java/org/example/cyberwatch/config/security/OAuth2SuccessHandler.java`:
- Around line 65-73: The current OAuth2SuccessHandler unconditionally calls
staffRepository.save(staff) on every login; change this to only persist when a
field actually changes by tracking mutations: read existing values, compare
incoming picture to staff.getProfilePictureUrl() and incoming status logic to
staff.getStatus(), set via staff.setProfilePictureUrl(...) and
staff.setStatus(...) only if different, maintain a boolean dirty flag (e.g.,
mutated) and call staffRepository.save(staff) only when mutated is true; update
references to staffRepository.save, staff.setProfilePictureUrl, and
staff.setStatus in OAuth2SuccessHandler accordingly.

In `@src/main/java/org/example/cyberwatch/features/staff/model/Staff.java`:
- Around line 64-65: Change the free-form String status in the Staff model into
a typed enum: create a StaffStatus enum with values (ONLINE, OFFLINE, AWAY,
BUSY), update the Staff class field signature from String status to StaffStatus
status and annotate it with `@Enumerated`(EnumType.STRING) (same pattern as role
and department), update the Staff#getStatus/setStatus signatures and any usage
in StaffService.updateStatus to accept or convert to StaffStatus (validate
inputs or map incoming strings to enum values), and ensure persistence/migration
will store the enum as string for DB compatibility.
🪄 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: 57e29567-c92b-4dec-a6ed-c502736ca6a4

📥 Commits

Reviewing files that changed from the base of the PR and between e5e0087 and b239c30.

📒 Files selected for processing (20)
  • src/main/java/org/example/cyberwatch/config/security/OAuth2SuccessHandler.java
  • src/main/java/org/example/cyberwatch/features/staff/controller/StaffRestController.java
  • src/main/java/org/example/cyberwatch/features/staff/model/Staff.java
  • src/main/java/org/example/cyberwatch/features/staff/model/StaffDTO.java
  • src/main/java/org/example/cyberwatch/features/staff/model/StaffMapper.java
  • src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java
  • src/main/resources/static/css/styles.css
  • src/main/resources/static/js/app.js
  • src/main/resources/static/js/common.js
  • src/main/resources/static/js/create-ticket.js
  • src/main/resources/static/js/dashboard.js
  • src/main/resources/static/js/edit-ticket.js
  • src/main/resources/static/js/employment.js
  • src/main/resources/static/js/ticket-detail.js
  • src/main/resources/static/pages/create-ticket.html
  • src/main/resources/static/pages/dashboard.html
  • src/main/resources/static/pages/edit-ticket.html
  • src/main/resources/static/pages/employment.html
  • src/main/resources/static/pages/ticket-detail.html
  • src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java

Comment thread src/main/resources/static/js/app.js Outdated
Comment thread src/main/resources/static/js/app.js Outdated
Comment thread src/main/resources/static/js/common.js Outdated
Comment thread src/main/resources/static/js/employment.js
Comment thread src/main/resources/static/js/employment.js
Comment thread src/main/resources/static/js/ticket-detail.js
Comment thread src/main/resources/static/pages/create-ticket.html
Comment thread src/main/resources/static/pages/ticket-detail.html
Ericthilen and others added 4 commits April 21, 2026 16:02
…validations in `StaffService` and `StaffRestController`
…iew controllers in WebConfig; update SecurityConfig with new endpoints and logout handling
…ove and view pending employment forms; update logging to use Staff object for better clarity.
…aff object for authentication; improve logging and authorization checks for form approval, rejection, and deletion.

@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: 5

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/cyberwatch/features/form/service/EmploymentFormService.java (2)

189-205: ⚠️ Potential issue | 🟠 Major

Avoid writing to S3 before the database transaction is durable.

archiveToS3(form) runs before the staff/form saves. If either DB save fails or the transaction later rolls back, S3 can contain an archive for an approval that never committed. Prefer persisting the approval state/key first and moving the S3 upload to an after-commit hook or outbox/retry flow.

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

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`
around lines 189 - 205, The code calls archiveToS3(form) before the DB changes
are durable; move S3 writes to after the transaction commits to avoid orphaned
S3 objects. Persist the updated form and newStaff first (use
employmentFormRepository.save(form) and staffRepository.save(newStaff) after
setting form.setApprovedBy(...) and form.setStatus(...)), then trigger
archiveToS3(form) from an after-commit hook/event or an outbox/retry mechanism
that runs only when the transaction succeeds; ensure
newStaff.setEmployedS3Key(...) is set from the result of the after-commit upload
or updated in a follow-up transactional step so the DB always reflects the
canonical state.

207-209: ⚠️ Potential issue | 🟠 Major

Don’t return the generated password in the API response.

This exposes a live credential to the browser and any intermediaries/client logs. Return a generic success response and deliver onboarding through a reset-token/email flow.

Proposed immediate containment
-        //No need to worry, this will be replaced with an email service
-        return "Employment has been approved, generated password for new employee: " + rawPassword;
+        // TODO: Send onboarding/reset instructions through the email service.
+        return "Employment has been approved.";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`
around lines 207 - 209, The method in EmploymentFormService that logs and
returns the generated rawPassword (the return line returning "Employment has
been approved, generated password for new employee: " + rawPassword) must be
changed to avoid exposing credentials: stop including rawPassword in the API
response and in logs, return a generic success message such as "Employment
approved; onboarding email sent" from the approving method (e.g.,
approveEmploymentForm or whatever method contains logger.info("Form {} approved
by {}", formId, loggedInManagement)); instead generate a one-time reset token,
persist only hashed credentials/password, and invoke the onboarding/email
workflow (e.g., EmailService.sendOnboardingEmail(formId, resetToken) or
OnboardingService.sendResetToken) to deliver the initial login link; ensure any
logging (logger.info/logger.debug) never prints rawPassword and only logs
non-sensitive identifiers.
src/main/java/org/example/cyberwatch/config/SecurityConfig.java (1)

60-65: ⚠️ Potential issue | 🟠 Major

Allow self-status updates before the broad staff write rule.

PATCH /api/staff/me/status only updates the caller’s own status, but it currently falls through to .requestMatchers("/api/staff/**").hasAnyRole("HR", "CEO", "CTO", "ADMIN"), so non-HR/ADMIN/CEO/CTO users cannot update their own presence.

🔐 Proposed matcher ordering fix
                         ).permitAll()
+                        // Alla inloggade får uppdatera sin egen status
+                        .requestMatchers(HttpMethod.PATCH, "/api/staff/me/status").authenticated()
                         // Alla inloggade får läsa staff (behövs för ticket-dropdowns)
                         .requestMatchers(HttpMethod.GET, "/api/staff/**").authenticated()
                         // Endast HR, CEO, CTO & ADMIN får skriva/ändra staff
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/cyberwatch/config/SecurityConfig.java` around lines
60 - 65, The PATCH endpoint for users to update their own status is being
blocked by the broader staff write rule in SecurityConfig; add a specific
matcher for HttpMethod.PATCH on "/api/staff/me/status" that allows authenticated
users (e.g., .requestMatchers(HttpMethod.PATCH,
"/api/staff/me/status").authenticated()) and place it before the existing
.requestMatchers("/api/staff/**").hasAnyRole("HR","CEO","CTO","ADMIN") rule so
the specific self-update rule takes precedence over the broad staff write rule.
🤖 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/cyberwatch/config/SecurityConfig.java`:
- Around line 90-93: Frontend triggers logout with GET (common.js uses
window.location.href = "/logout") but SecurityConfig.java currently relies on
Spring Security's default POST logout (CSRF enabled via CsrfConfigurer::spa) and
only sets logoutSuccessUrl(); fix by either making the frontend issue a POST
with the CSRF token/header or changing the backend logout matcher to accept
GETs: update the code in SecurityConfig.java where .logout(...) is configured
(the logout() block and logoutSuccessUrl(...) usage) to call
logoutRequestMatcher(...) or permit GET (e.g., configure an
AntPathRequestMatcher for "/logout" with HttpMethod.GET) if GET logout is
intentional, or change the frontend common.js to send a POST to "/logout" with
the XSRF token/header so CSRF protection is satisfied.

In
`@src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java`:
- Line 48: The log message in EmploymentFormController at the approval endpoint
is misleading — it currently logs "Creating employment form for HR staff" via
logger.info; update that log to reflect the approval action (e.g., "Approved
employment form" or "Approving employment form") and include identifying details
such as the form id and approver email (use the same local variables used in the
method, e.g., form.getId() and staff.getEmail()) so audit/debug traces correctly
show the approval event.
- Around line 31-33: Replace the inline principal checks in
EmploymentFormController (currently using "Object principal =
authentication.getPrincipal()" and throwing IllegalArgumentException) with a
centralized server-side staff extraction that throws AccessDeniedException on
failure, modeled after TicketController.getAuthenticatedStaff(); add a private
helper in EmploymentFormController (e.g., getAuthenticatedStaff(Authentication))
or call the existing TicketController.getAuthenticatedStaff(...) to obtain
org.example.cyberwatch.features.staff.model.Staff for all five endpoints
(create, approve, update, reject, delete), remove any actor resolution from
caller-controlled parameters and use the resolved Staff for actor fields, and
ensure every location that formerly threw IllegalArgumentException now throws
org.springframework.security.access.AccessDeniedException so the endpoints
return 403.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 128-139: The runtime role checks in
EmploymentFormService.rejectForm (and the similar method around lines 177-183)
are inconsistent with the `@PreAuthorize` that permits ADMIN; update the checks to
allow Role.ADMIN as well (e.g., include loggedInRejecter.getRole() != Role.ADMIN
in the allowed roles) and adjust the IllegalStateException message to reflect
the correct allowed roles (CEO, CTO, ADMIN) so both the annotation and service
pre-checks match; apply the same change to the counterpart approval method
referenced in the comment.

In
`@src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java`:
- Around line 251-263: The test is not exercising the non-PENDING branch because
approver.getRole() is null; set a valid management role on the Staff approver
before invoking service.approveAndFinalizeEmployment so the service checks the
form status (which is APPROVED) rather than failing on role null. Update the
test's approver setup (the Staff approver instance in EmploymentFormServiceTest)
to call approver.setRole(...) with a valid management role (for example
ManagementRole.MANAGER or the appropriate enum/constant used in your codebase)
and then keep the rest of the test (form with ApprovalStatus.APPROVED,
when(formRepository.findById(formId))...) and the assertThrows unchanged.

---

Outside diff comments:
In `@src/main/java/org/example/cyberwatch/config/SecurityConfig.java`:
- Around line 60-65: The PATCH endpoint for users to update their own status is
being blocked by the broader staff write rule in SecurityConfig; add a specific
matcher for HttpMethod.PATCH on "/api/staff/me/status" that allows authenticated
users (e.g., .requestMatchers(HttpMethod.PATCH,
"/api/staff/me/status").authenticated()) and place it before the existing
.requestMatchers("/api/staff/**").hasAnyRole("HR","CEO","CTO","ADMIN") rule so
the specific self-update rule takes precedence over the broad staff write rule.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 189-205: The code calls archiveToS3(form) before the DB changes
are durable; move S3 writes to after the transaction commits to avoid orphaned
S3 objects. Persist the updated form and newStaff first (use
employmentFormRepository.save(form) and staffRepository.save(newStaff) after
setting form.setApprovedBy(...) and form.setStatus(...)), then trigger
archiveToS3(form) from an after-commit hook/event or an outbox/retry mechanism
that runs only when the transaction succeeds; ensure
newStaff.setEmployedS3Key(...) is set from the result of the after-commit upload
or updated in a follow-up transactional step so the DB always reflects the
canonical state.
- Around line 207-209: The method in EmploymentFormService that logs and returns
the generated rawPassword (the return line returning "Employment has been
approved, generated password for new employee: " + rawPassword) must be changed
to avoid exposing credentials: stop including rawPassword in the API response
and in logs, return a generic success message such as "Employment approved;
onboarding email sent" from the approving method (e.g., approveEmploymentForm or
whatever method contains logger.info("Form {} approved by {}", formId,
loggedInManagement)); instead generate a one-time reset token, persist only
hashed credentials/password, and invoke the onboarding/email workflow (e.g.,
EmailService.sendOnboardingEmail(formId, resetToken) or
OnboardingService.sendResetToken) to deliver the initial login link; ensure any
logging (logger.info/logger.debug) never prints rawPassword and only logs
non-sensitive identifiers.
🪄 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: 27adf71d-55e9-41c0-bafd-b89900ee195b

📥 Commits

Reviewing files that changed from the base of the PR and between b239c30 and 1775181.

📒 Files selected for processing (10)
  • src/main/java/org/example/cyberwatch/config/SecurityConfig.java
  • src/main/java/org/example/cyberwatch/config/WebConfig.java
  • src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
  • src/main/java/org/example/cyberwatch/features/staff/controller/StaffRestController.java
  • src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java
  • src/main/resources/db/migration/V10__add_profile_picture_and_status_to_staff.sql
  • src/main/resources/static/js/app.js
  • src/main/resources/static/js/employment.js
  • src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java
💤 Files with no reviewable changes (1)
  • src/main/resources/static/js/app.js
✅ Files skipped from review due to trivial changes (1)
  • src/main/resources/db/migration/V10__add_profile_picture_and_status_to_staff.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java
  • src/main/resources/static/js/employment.js

Comment thread src/main/java/org/example/cyberwatch/config/SecurityConfig.java
…non-Staff principals; update logging messages for form creation and approval
…nd deletions; handle exceptions for non-existent staff
Comment thread src/main/resources/static/js/dashboard.js
…t fetching with URLSearchParams for better query handling

@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: 3

🤖 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/resources/static/js/common.js`:
- Around line 109-124: The change listener on statusSelect sets the UI before
confirming the server PATCH succeeded, so on failure the select still shows the
new value; capture the previous value (e.g., const prev = statusSelect.value or
e.target.value before awaiting apiFetch) and if the response is not ok or the
request throws (res.ok === false or in the catch), reset the select back to prev
(statusSelect.value = prev) and show the existing error alerts; update the
listener attached to statusSelect and references to apiFetch to perform this
rollback on both non-OK responses and exceptions.
- Around line 139-142: Normalize selectedIds to a consistent type (e.g., map to
strings or build a Set of String values) and then mark options as selected when
their IDs match; update the staffOptions generation (the staffOptions mapping,
the isAssigned check against s.id, and the className usage) to compare
String(s.id) to the normalized selectedIds and include a selected attribute
(e.g., ' selected') in the returned <option> when isAssigned, while still adding
the badge-assigned class when appropriate.
- Around line 44-45: The logout() function currently does a GET by setting
window.location.href; change it to send a POST to "/logout" so Spring Security
will invalidate the session — implement logout() to issue a POST (e.g., using
fetch or XMLHttpRequest) to "/logout" with credentials: "same-origin" and
include the CSRF token header (read the token from your page’s CSRF meta tags or
cookie, e.g., the _csrf token/meta or the header name like X-CSRF-TOKEN) so the
request is accepted by .csrf(CsrfConfigurer::spa) and triggers session
invalidation.
🪄 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: 28f404e3-dbb3-41bf-91dd-30ab0a6f12be

📥 Commits

Reviewing files that changed from the base of the PR and between eb0c691 and 281b9a4.

📒 Files selected for processing (2)
  • src/main/resources/static/js/common.js
  • src/main/resources/static/js/dashboard.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/resources/static/js/dashboard.js

Comment thread src/main/resources/static/js/common.js Outdated
Comment thread src/main/resources/static/js/common.js
Comment thread src/main/resources/static/js/common.js Outdated
gitnes94 and others added 2 commits April 22, 2026 11:01
…led when no staff is selected; allow multiple staff assignments for tickets.

@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: 4

Caution

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

⚠️ Outside diff range comments (2)
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (2)

210-211: ⚠️ Potential issue | 🟠 Major

Do not return the generated password in the API response.

This exposes a reusable credential through HTTP clients, browser/devtool history, logs, and monitoring. Return a generic success message and deliver onboarding credentials via a reset-link/email flow instead.

🔐 Minimal response hardening
-        //No need to worry, this will be replaced with an email service
-        return "Employment has been approved, generated password for new employee: " + rawPassword;
+        // TODO: Send onboarding/reset instructions via the email service.
+        return "Employment has been approved";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`
around lines 210 - 211, The API currently returns the generated rawPassword in
the response string inside EmploymentFormService (the approval flow method),
which leaks credentials; remove rawPassword from the returned message and
replace it with a generic success message (e.g., "Employment approved;
onboarding email sent"), and instead invoke the onboarding/email/reset-link flow
from the same method (call your EmailService.sendOnboardingLink or
PasswordResetService.createAndSendResetLink using the new user's id/email). Also
ensure no rawPassword is logged or included in any return type from methods like
approveEmployment / createNewEmployee and that any audit logs store only
non-sensitive identifiers.

143-151: ⚠️ Potential issue | 🟠 Major

Move S3 archiving out of the rollback-sensitive path.

Both reject and approve upload the archived form to S3 before the final database writes complete. If save(...) fails afterward, the transaction rolls back but the S3 object remains, potentially archiving PII for a state change that never committed. Prefer an after-commit/outbox flow, or add compensating cleanup around failed post-upload persistence.

Also applies to: 197-207

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

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`
around lines 143 - 151, The S3 upload (archiveToS3) is currently executed before
the DB commit in EmploymentFormService (e.g., in the reject/approve flow
surrounding employmentFormRepository.save), which can leave S3 objects when the
transaction later rolls back; move the archiveToS3 call out of the
rollback-sensitive path by invoking it after the transaction commits (use a
transaction synchronization/afterCommit callback or an outbox event dispatched
from EmploymentFormService) or, if you must upload synchronously, perform the
upload only after employmentFormRepository.save succeeds and add compensating
cleanup (delete from S3) if a subsequent persistence step fails; update both
places referenced (the block around form.setStatus(...)/archiveToS3 and the
similar block at lines 197-207) accordingly.
🧹 Nitpick comments (3)
src/main/resources/static/js/common.js (1)

34-42: apiFetch fires-and-forgets logout() on 401.

logout() is async and issues a POST before redirecting, but here it's invoked without await. The function that received the 401 keeps executing against a stale session, callers get back the 401 response and may alert(...)/res.json() on it, and in some flows loadDashboardTickets/loadTicketDetail render "Kunde inte hämta..." error states just before the redirect. Consider await logout(); return response; (or throw a sentinel) so the redirect wins the race and callers don't render transient error UI.

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

In `@src/main/resources/static/js/common.js` around lines 34 - 42, apiFetch
currently calls the async logout() without awaiting, causing callers to continue
working with a stale 401 response; update apiFetch to await logout() when
response.status === 401 (e.g., await logout(); return response;) or
alternatively throw a clear sentinel error after awaiting so callers don't
continue rendering transient error UI. Locate the apiFetch function and modify
the 401 branch to await the async logout() (or await then throw) to ensure the
redirect completes before callers proceed.
src/main/resources/static/js/ticket-detail.js (1)

104-131: setupUploadForm bypasses apiFetch and loses 401 handling.

Using the raw fetch here is necessary to avoid the JSON Content-Type from getHeaders(), but it means a session that expires during upload won't trigger the shared logout() redirect, and any future cross-cutting concern added to apiFetch will silently skip this call. Consider extending apiFetch to accept an option like { multipart: true } (or a skipJsonContentType flag) so all API calls go through one wrapper and keep the 401 contract.

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

In `@src/main/resources/static/js/ticket-detail.js` around lines 104 - 131, Modify
the upload to go through the shared wrapper: extend apiFetch to accept an option
like { multipart: true } (or skipJsonContentType) so it will omit setting
"Content-Type" and will still include credentials, CSRF headers and the existing
401/logout handling; then update setupUploadForm to call
apiFetch(`/api/tickets/${id}/upload`, { method: "POST", body: formData,
multipart: true }) instead of raw fetch and remove the manual getCsrfToken/fetch
header logic, ensuring form.reset() and loadTicketDetail() remain on success and
the existing apiFetch error handling runs on failure.
src/main/resources/static/js/dashboard.js (1)

21-45: Redundant "stats" fetch of the full ticket list.

loadDashboardTickets fires two back-to-back GET /tickets requests on every keystroke of #searchInput (plus another to /staff): one unfiltered fetch purely to compute four counters, and one filtered fetch for the list. On a large ticket table this doubles load and amplifies any search-as-you-type latency.

Consider either (a) computing counters from the already-fetched filtered list when filters are empty and caching the "all tickets" response, or (b) adding a dedicated lightweight /tickets/stats endpoint. Debouncing the input listener would also help.

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

In `@src/main/resources/static/js/dashboard.js` around lines 21 - 45, The code is
doing two back-to-back GET /tickets calls (the "stats" fetch using apiFetch and
the filtered fetch using apiFetch(`/tickets?${params}`)), which duplicates work
and doubles latency; remove the redundant statsRes fetch and instead compute the
four counters (totalCount, openCount, inProgressCount, closedCount) from the
already-fetched ticket list returned by the main fetch (res.json()), and/or
cache a single "allTickets" result in a module-level variable so subsequent
calls can reuse it when filters are empty; update the DOM using the same element
IDs ("totalTickets", "openTickets", "inProgressTickets", "closedTickets") after
parsing the main response, and add debouncing to the search input listener to
reduce request frequency.
🤖 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/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 107-109: The ownership check in EmploymentFormService currently
calls existingForm.getCreatedBy().getId().equals(loggedInHr.getId()), which can
NPE if createdBy or its id is null; change the isCreator logic to be null-safe
by first checking existingForm.getCreatedBy() != null and then using a null-safe
equality (e.g., Objects.equals(existingForm.getCreatedBy().getId(),
loggedInHr.getId())) to compare IDs; apply the same null-safe pattern to the
equivalent ownership/delete checks referenced around lines 167-168 so all
authorization checks avoid dereferencing potentially-null Staff.id.

In `@src/main/resources/static/js/common.js`:
- Around line 8-13: The escapeHtml function only encodes & < > via the
textContent -> innerHTML trick and therefore leaves " and ' unescaped, making
attribute contexts vulnerable; update escapeHtml to also replace double-quote
(") with &quot; and single-quote (') with &#39; (in addition to existing &, <, >
handling) so any use in attributes (e.g., the ticket card aria-label in
dashboard.js and attachment href in ticket-detail.js) is safe; keep the function
name escapeHtml and ensure callers continue to receive a string (no DOM writes)
so existing usages need no other changes.

In `@src/main/resources/static/js/dashboard.js`:
- Around line 62-78: The aria-label and inline handlers in the ticket card HTML
(the template building code that sets item.innerHTML and uses onclick/onkeydown
on the .ticket-main element) are vulnerable because escapeHtml does not escape
quotes; fix this by either (A) hardening escapeHtml (in common.js) to also
replace double-quote (") with &quot; and single-quote (') with &#39; so
interpolation into attributes is safe, or (B — preferred) stop using innerHTML
and inline onclick/onkeydown: build the card using createElement, setAttribute
(for aria-label), textContent for titles and IDs, addEventListener for
click/keydown handlers, and use element.classList to set badges so no user data
is ever injected into an HTML string; update references to escapeHtml,
item.innerHTML, onclick, onkeydown, and .ticket-main accordingly.

In `@src/main/resources/static/js/ticket-detail.js`:
- Around line 19-32: The attachment links currently only use escapeHtml and can
still render unsafe schemes; add a URL-sanitizer helper in common.js (mirroring
safeImageUrl) that parses the URL and returns it only if parsed.protocol is
"http:" or "https:" (otherwise return null/empty), then update the template that
builds attachmentsHtml to call that helper for a.downloadUrl and, on
invalid/empty result, render a non-clickable filename or use "#" as the href and
avoid target/rel attributes; reference the attachmentsHtml code and escapeHtml
usage so you replace escapeHtml(a.downloadUrl) with the new safe URL helper
result and keep escapeHtml(a.fileName) for the label.

---

Outside diff comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 210-211: The API currently returns the generated rawPassword in
the response string inside EmploymentFormService (the approval flow method),
which leaks credentials; remove rawPassword from the returned message and
replace it with a generic success message (e.g., "Employment approved;
onboarding email sent"), and instead invoke the onboarding/email/reset-link flow
from the same method (call your EmailService.sendOnboardingLink or
PasswordResetService.createAndSendResetLink using the new user's id/email). Also
ensure no rawPassword is logged or included in any return type from methods like
approveEmployment / createNewEmployee and that any audit logs store only
non-sensitive identifiers.
- Around line 143-151: The S3 upload (archiveToS3) is currently executed before
the DB commit in EmploymentFormService (e.g., in the reject/approve flow
surrounding employmentFormRepository.save), which can leave S3 objects when the
transaction later rolls back; move the archiveToS3 call out of the
rollback-sensitive path by invoking it after the transaction commits (use a
transaction synchronization/afterCommit callback or an outbox event dispatched
from EmploymentFormService) or, if you must upload synchronously, perform the
upload only after employmentFormRepository.save succeeds and add compensating
cleanup (delete from S3) if a subsequent persistence step fails; update both
places referenced (the block around form.setStatus(...)/archiveToS3 and the
similar block at lines 197-207) accordingly.

---

Nitpick comments:
In `@src/main/resources/static/js/common.js`:
- Around line 34-42: apiFetch currently calls the async logout() without
awaiting, causing callers to continue working with a stale 401 response; update
apiFetch to await logout() when response.status === 401 (e.g., await logout();
return response;) or alternatively throw a clear sentinel error after awaiting
so callers don't continue rendering transient error UI. Locate the apiFetch
function and modify the 401 branch to await the async logout() (or await then
throw) to ensure the redirect completes before callers proceed.

In `@src/main/resources/static/js/dashboard.js`:
- Around line 21-45: The code is doing two back-to-back GET /tickets calls (the
"stats" fetch using apiFetch and the filtered fetch using
apiFetch(`/tickets?${params}`)), which duplicates work and doubles latency;
remove the redundant statsRes fetch and instead compute the four counters
(totalCount, openCount, inProgressCount, closedCount) from the already-fetched
ticket list returned by the main fetch (res.json()), and/or cache a single
"allTickets" result in a module-level variable so subsequent calls can reuse it
when filters are empty; update the DOM using the same element IDs
("totalTickets", "openTickets", "inProgressTickets", "closedTickets") after
parsing the main response, and add debouncing to the search input listener to
reduce request frequency.

In `@src/main/resources/static/js/ticket-detail.js`:
- Around line 104-131: Modify the upload to go through the shared wrapper:
extend apiFetch to accept an option like { multipart: true } (or
skipJsonContentType) so it will omit setting "Content-Type" and will still
include credentials, CSRF headers and the existing 401/logout handling; then
update setupUploadForm to call apiFetch(`/api/tickets/${id}/upload`, { method:
"POST", body: formData, multipart: true }) instead of raw fetch and remove the
manual getCsrfToken/fetch header logic, ensuring form.reset() and
loadTicketDetail() remain on success and the existing apiFetch error handling
runs on failure.
🪄 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: d62b355c-53dd-4bfe-b89e-cd2987767ed6

📥 Commits

Reviewing files that changed from the base of the PR and between 281b9a4 and bed12d7.

⛔ Files ignored due to path filters (1)
  • src/main/resources/static/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (7)
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
  • src/main/resources/static/js/common.js
  • src/main/resources/static/js/dashboard.js
  • src/main/resources/static/js/ticket-detail.js
  • src/main/resources/static/pages/create-ticket.html
  • src/main/resources/static/pages/ticket-detail.html
  • src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java

Comment thread src/main/resources/static/js/common.js
Comment thread src/main/resources/static/js/dashboard.js
Comment on lines +19 to +32
const attachmentsHtml = t.attachments && t.attachments.length > 0
? `<div style="margin-top: 1rem;">
<strong>Bilagor:</strong>
<ul style="margin-top: 0.5rem; padding-left: 1.2rem;">
${t.attachments.map(a => `
<li style="margin-bottom: 0.4rem;">
<a href="${escapeHtml(a.downloadUrl)}" target="_blank" rel="noopener noreferrer">
${escapeHtml(a.fileName)}
</a>
</li>
`).join('')}
</ul>
</div>`
: `<p class="muted" style="margin-top: 1rem;">Inga bilagor uppladdade.</p>`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is implemented

…nction for better security and use safeHttpUrl for download links in ticket details
Comment on lines +19 to +32
const attachmentsHtml = t.attachments && t.attachments.length > 0
? `<div style="margin-top: 1rem;">
<strong>Bilagor:</strong>
<ul style="margin-top: 0.5rem; padding-left: 1.2rem;">
${t.attachments.map(a => `
<li style="margin-bottom: 0.4rem;">
<a href="${escapeHtml(a.downloadUrl)}" target="_blank" rel="noopener noreferrer">
${escapeHtml(a.fileName)}
</a>
</li>
`).join('')}
</ul>
</div>`
: `<p class="muted" style="margin-top: 1rem;">Inga bilagor uppladdade.</p>`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is implemented

@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: 6

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/cyberwatch/features/form/service/EmploymentFormService.java (3)

144-152: ⚠️ Potential issue | 🟠 Major

Avoid S3 writes before the database transaction commits.

Both reject and approve upload the archive inside the transaction before the final DB write/commit. If the later save or commit fails, S3 can contain an archive for a state that rolled back. Prefer a transactional outbox or an AFTER_COMMIT event with retry/compensation.

Also applies to: 201-209

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

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`
around lines 144 - 152, The S3 upload (archiveToS3) is happening inside the DB
transaction and must be deferred until after the transaction commits to avoid
orphaned S3 objects; refactor EmploymentFormService to remove direct calls to
archiveToS3 from the reject/approve flow (the blocks that call
form.setStatus(...), form.setApprovedBy(...),
employmentFormRepository.save(form)) and instead publish a post-commit
action—either register a TransactionSynchronization via
TransactionSynchronizationManager.registerSynchronization or emit an event
handled by a method annotated with `@TransactionalEventListener`(phase =
AFTER_COMMIT) that calls archiveToS3(formId) and handles retries/errors; apply
the same change to the approve path (lines referenced 201-209) so both flows
archive only after successful commit.

213-213: ⚠️ Potential issue | 🔴 Critical

Do not return the generated password in the API response.

rawPassword is a credential; returning it exposes it to browser history, proxies, logs, and any caller with approval access. Return a generic approval result and deliver account setup through a reset/invite flow.

🛡️ Proposed fix
-        return "Employment has been approved, generated password for new employee: " + rawPassword;
+        return "Employment has been approved";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`
at line 213, The method in EmploymentFormService currently returns the generated
credential by concatenating rawPassword into the response; remove any exposure
of rawPassword from the API return value and instead return a generic success
message (e.g., "Employment approved; account setup initiated") from the method
that contains the return statement referencing rawPassword, and trigger a secure
invite/reset flow (for example by calling the existing invite/email or
createPasswordResetToken method) to deliver credentials out-of-band; ensure
rawPassword is only used internally for account creation and not included in
logs, responses, or exceptions.

193-198: ⚠️ Potential issue | 🟠 Major

Add SSN re-validation in approveAndFinalizeEmployment() before creating the Staff record.

Between form creation and approval, another employee with the same SSN could be added through a different flow. The approval method bypasses the existing validateSsnNotExists() check, creating a race condition. Call validateSsnNotExists(form.getSocialSecurityNumber()) before line 195 to validate against both the forms and staff tables, preventing a database-level exception.

Proposed fix
        if (form.getStatus() != ApprovalStatus.PENDING) {
            throw new IllegalStateException("Only PENDING forms can be approved. Current status: " + form.getStatus());
        }

+       validateSsnNotExists(form.getSocialSecurityNumber());
+
        // Create and save new staff first (all DB operations before S3 write)
         Staff newStaff = employmentMapper.formToStaff(form);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`
around lines 193 - 198, In approveAndFinalizeEmployment(), re-run the SSN
uniqueness check before creating the Staff record: call
validateSsnNotExists(form.getSocialSecurityNumber()) immediately before
constructing the Staff (before employmentMapper.formToStaff(form) / newStaff
creation) so the method verifies the SSN against current forms and staff records
and aborts with the same validation error if a duplicate was added in the
meantime.
♻️ Duplicate comments (3)
src/main/resources/static/js/ticket-detail.js (1)

53-63: ⚠️ Potential issue | 🟡 Minor

loadComments(id) is still fire-and-forget; the outer catch won't see its failures.

loadComments is now properly wrapped in try/catch internally, but the call on Line 53 is not awaited, so any error thrown before it sets up its own try/catch (e.g. the element lookup, or a thrown error from an earlier microtask) will surface as an unhandled rejection rather than falling into loadTicketDetail's catch on Line 61. Also, the outer catch swallows the error silently without logging — making production debugging hard.

🛠️ Small fix
-        loadComments(id);
+        await loadComments(id);
@@
-    } catch (e) {
-        container.innerHTML = "<p>Något gick fel.</p>";
-    }
+    } catch (e) {
+        console.error("Error loading ticket detail", e);
+        container.innerHTML = "<p>Något gick fel.</p>";
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/js/ticket-detail.js` around lines 53 - 63, The call
to loadComments(id) is not awaited so its early failures can escape
loadTicketDetail's catch and become unhandled rejections; change the call to
await loadComments(id) in the same async function and update the outer catch to
log the caught error (e.g., console.error or your logger) before setting
container.innerHTML so failures are visible; locate the call to loadComments and
the surrounding try/catch (references: loadComments(id), t.assignedStaff,
container.innerHTML) and make these two changes.
src/main/resources/static/js/dashboard.js (2)

80-115: ⚠️ Potential issue | 🟠 Major

Assignment dropdown is still single-select — previous assignees get overwritten.

The <select class="dashboard-assignment-select"> does not have a multiple attribute, so it is a single-select regardless of how many selected options the template emits (browsers keep only the last selected visible). When the user picks anyone, Array.from(e.target.selectedOptions) yields exactly one id, and the PUT /tickets/{id}/assign body becomes {staffIds: [chosenId]} — silently wiping out all other assignees on that ticket. This is the same regression that was previously flagged; the handler was updated to read selectedOptions, but the multiple attribute never made it into the rendered markup.

🐛 Proposed fix
-                    <select class="dashboard-assignment-select" data-id="${t.id}" style="max-width: 150px;">
-                        <option value="" disabled>Tilldela...</option>
+                    <select class="dashboard-assignment-select" data-id="${t.id}" multiple style="max-width: 150px;">
+                        <option value="" disabled>Tilldela...</option>

If a multi-select control doesn't fit the dashboard UX, at minimum merge the chosen id with assignedIds before PUT-ing so existing assignees are preserved.

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

In `@src/main/resources/static/js/dashboard.js` around lines 80 - 115, The
assignment select is rendered as a single-select so choosing a staff overwrites
other assignees; update the template that builds the <select
class="dashboard-assignment-select" data-id="${t.id}" ...> (the code that maps
allStaff and uses assignedIds) to include the multiple attribute so it becomes a
true multi-select, keeping the existing selected options, and leave the existing
change handler (which reads e.target.selectedOptions, maps to staffIds, calls
apiFetch(`/tickets/${ticketId}/assign`, ...) and then loadDashboardTickets())
unchanged; alternatively, if you prefer single-select UX, modify the change
handler to merge the newly chosen id with the ticket's existing assignedIds
before sending the PUT instead of replacing them.

62-78: ⚠️ Potential issue | 🟡 Minor

Inline onclick/onkeydown interpolate raw t.id and block a strict CSP.

Two issues in this template:

  1. t.id is embedded directly inside a JS string literal inside an HTML attribute (onclick="window.location.href='/pages/ticket-detail.html?id=${t.id}'"). escapeHtml is not applied here, and even if it were, &#39; wouldn't be interpreted as an apostrophe-closing character in JS. If t.id is ever not a plain number (UUID string, slug, etc.) this becomes injection-prone. Same for ${t.status} in badge-${t.status} and data-id="${t.id}" on the selects below.
  2. The inline handlers make it impossible to adopt a strict script-src CSP for the app.

Prefer building the card with DOM APIs (or at minimum, delegating clicks via a single listener that reads data-ticket-id + encodeURIComponent):

🛠️ Sketch
-            item.innerHTML = `
-    <div class="ticket-main"
-         role="link"
-         tabindex="0"
-         aria-label="Ticket ${t.id}: ${escapeHtml(t.title)}"
-         onclick="window.location.href='/pages/ticket-detail.html?id=${t.id}'"
-         onkeydown="if(event.key==='Enter'||event.key===' ')window.location.href='/pages/ticket-detail.html?id=${t.id}'">
+            const href = `/pages/ticket-detail.html?id=${encodeURIComponent(t.id)}`;
+            item.innerHTML = `
+    <a class="ticket-main" href="${escapeHtml(href)}"
+       aria-label="Ticket ${escapeHtml(String(t.id))}: ${escapeHtml(t.title)}">

…and drop the onclick/onkeydown entirely (an <a> is keyboard-accessible by default).

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

In `@src/main/resources/static/js/dashboard.js` around lines 62 - 78, The template
currently injects raw values via item.innerHTML (notably t.id in
onclick/onkeydown and badge-${t.status}/badge-${t.priority}), which risks
injection and blocks strict CSP; replace the HTML string construction by
creating DOM nodes (or at minimum set data-ticket-id on the root element and
remove the inline onclick/onkeydown), assign textContent for titles and use
classList.add with a whitelist/sanitizer for status/priority values (referencing
ticket-main, ticket-header, badge-${t.status}, badge-${t.priority}, escapeHtml,
and t.id), and wire navigation via an external event listener or an <a> element
that builds the URL using encodeURIComponent(data-ticket-id) to avoid inline JS
and ensure safe encoding of t.id.
🤖 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/cyberwatch/features/form/service/EmploymentFormService.java`:
- Line 62: Replace any logging that prints Staff objects or PII (e.g., hrStaff,
staff.getEmail(), or direct Staff instances) with the staff identifier only;
locate logger calls in EmploymentFormService that currently use
savedForm.getId() with hrStaff.getEmail() and other logger.* calls that pass
Staff objects, and change them to use staff.getId() or a staffId variable (e.g.,
logger.info("... by staffId: {}", staff.getId())). Ensure no logger statements
pass the Staff instance or email/password/SSN fields so Lombok's toString()
cannot leak credentials.

In `@src/main/resources/static/js/common.js`:
- Around line 37-45: In apiFetch, when response.status === 401 you should
short-circuit after calling logout() so callers cannot continue using the
response; modify the response.status === 401 branch in apiFetch to call logout()
and then immediately throw a rejected error (e.g., throw new
Error('Unauthorized') or return Promise.reject(...)) instead of returning the
response, ensuring callers will not attempt to read the body after navigation.
Use the existing apiFetch and logout symbol names so the change is easy to find.
- Around line 18-28: The fallback URL in safeImageUrl should be replaced with a
local asset or inline SVG data URL to avoid third-party failure; update the
fallback constant inside the safeImageUrl function (currently set to
"https://via.placeholder.com/40") to point to your local default avatar path
(e.g., "/images/default-avatar.svg") or an inline data:image/svg+xml;base64,...
string, keep the existing URL parsing/validation logic (const parsed = new
URL(...), protocol check for "http:"/"https:"), and ensure the catch branch also
returns the same new fallback so all failure paths use the local/data fallback.

In `@src/main/resources/static/js/dashboard.js`:
- Around line 6-45: loadDashboardTickets currently causes duplicate ticket
fetches on each keystroke and refetches staff every render; debounce the search
input by wiring `#searchInput` to a 250–300ms debounced call to
loadDashboardTickets (create a debounce helper and use it when adding the
"input" listener), move any /staff fetch out of loadDashboardTickets into a
cached module-level allStaff variable populated once on init (e.g.,
populateAllStaff() called on page load and have loadDashboardTickets read from
allStaff), stop fetching the entire ticket list for client-side counts by
calling a dedicated stats endpoint (replace the
apiFetch(`/tickets?status=&priority=&search=&assignedStaffId=`) call inside
loadDashboardTickets with
apiFetch(`/tickets/stats?status=${s}&priority=${p}&search=${q}&assignedStaffId=${staffId}`)
or a single /tickets/stats call), and add simple request sequencing/cancellation
(use an AbortController or incremental requestId in loadDashboardTickets to
ignore out-of-order responses from apiFetch) so slow responses cannot overwrite
newer UI state.

In `@src/main/resources/static/js/ticket-detail.js`:
- Line 17: The assignment to document.getElementById("editTicketLink").href can
throw if the element is missing; change the code to first retrieve the element
into a variable (e.g., const editLink =
document.getElementById("editTicketLink")), check for null (if (editLink) { ...
}) or use optional chaining before setting href, and ensure you also guard t.id
exists before interpolating; alternatively move this logic to run after
DOMContentLoaded so the element is present.
- Around line 19-32: The attachments rendering calls an undefined safeHttpUrl,
causing a runtime ReferenceError; either replace safeHttpUrl(a.downloadUrl) with
the existing safeImageUrl(a.downloadUrl) call or add a generic safeHttpUrl
helper to common.js and use it here (ensure it mirrors safeImageUrl behavior for
http/https and returns a safe fallback like "#"); update the template in the
attachmentsHtml construction (and keep escapeHtml around the filename) so the
anchor href uses the defined safe helper.

---

Outside diff comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 144-152: The S3 upload (archiveToS3) is happening inside the DB
transaction and must be deferred until after the transaction commits to avoid
orphaned S3 objects; refactor EmploymentFormService to remove direct calls to
archiveToS3 from the reject/approve flow (the blocks that call
form.setStatus(...), form.setApprovedBy(...),
employmentFormRepository.save(form)) and instead publish a post-commit
action—either register a TransactionSynchronization via
TransactionSynchronizationManager.registerSynchronization or emit an event
handled by a method annotated with `@TransactionalEventListener`(phase =
AFTER_COMMIT) that calls archiveToS3(formId) and handles retries/errors; apply
the same change to the approve path (lines referenced 201-209) so both flows
archive only after successful commit.
- Line 213: The method in EmploymentFormService currently returns the generated
credential by concatenating rawPassword into the response; remove any exposure
of rawPassword from the API return value and instead return a generic success
message (e.g., "Employment approved; account setup initiated") from the method
that contains the return statement referencing rawPassword, and trigger a secure
invite/reset flow (for example by calling the existing invite/email or
createPasswordResetToken method) to deliver credentials out-of-band; ensure
rawPassword is only used internally for account creation and not included in
logs, responses, or exceptions.
- Around line 193-198: In approveAndFinalizeEmployment(), re-run the SSN
uniqueness check before creating the Staff record: call
validateSsnNotExists(form.getSocialSecurityNumber()) immediately before
constructing the Staff (before employmentMapper.formToStaff(form) / newStaff
creation) so the method verifies the SSN against current forms and staff records
and aborts with the same validation error if a duplicate was added in the
meantime.

---

Duplicate comments:
In `@src/main/resources/static/js/dashboard.js`:
- Around line 80-115: The assignment select is rendered as a single-select so
choosing a staff overwrites other assignees; update the template that builds the
<select class="dashboard-assignment-select" data-id="${t.id}" ...> (the code
that maps allStaff and uses assignedIds) to include the multiple attribute so it
becomes a true multi-select, keeping the existing selected options, and leave
the existing change handler (which reads e.target.selectedOptions, maps to
staffIds, calls apiFetch(`/tickets/${ticketId}/assign`, ...) and then
loadDashboardTickets()) unchanged; alternatively, if you prefer single-select
UX, modify the change handler to merge the newly chosen id with the ticket's
existing assignedIds before sending the PUT instead of replacing them.
- Around line 62-78: The template currently injects raw values via
item.innerHTML (notably t.id in onclick/onkeydown and
badge-${t.status}/badge-${t.priority}), which risks injection and blocks strict
CSP; replace the HTML string construction by creating DOM nodes (or at minimum
set data-ticket-id on the root element and remove the inline onclick/onkeydown),
assign textContent for titles and use classList.add with a whitelist/sanitizer
for status/priority values (referencing ticket-main, ticket-header,
badge-${t.status}, badge-${t.priority}, escapeHtml, and t.id), and wire
navigation via an external event listener or an <a> element that builds the URL
using encodeURIComponent(data-ticket-id) to avoid inline JS and ensure safe
encoding of t.id.

In `@src/main/resources/static/js/ticket-detail.js`:
- Around line 53-63: The call to loadComments(id) is not awaited so its early
failures can escape loadTicketDetail's catch and become unhandled rejections;
change the call to await loadComments(id) in the same async function and update
the outer catch to log the caught error (e.g., console.error or your logger)
before setting container.innerHTML so failures are visible; locate the call to
loadComments and the surrounding try/catch (references: loadComments(id),
t.assignedStaff, container.innerHTML) and make these two changes.
🪄 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: 9bdd3e36-ec70-4aff-a47b-03946f28fbf5

📥 Commits

Reviewing files that changed from the base of the PR and between 281b9a4 and ac7232e.

⛔ Files ignored due to path filters (1)
  • src/main/resources/static/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (7)
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
  • src/main/resources/static/js/common.js
  • src/main/resources/static/js/dashboard.js
  • src/main/resources/static/js/ticket-detail.js
  • src/main/resources/static/pages/create-ticket.html
  • src/main/resources/static/pages/ticket-detail.html
  • src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/resources/static/pages/create-ticket.html
  • src/main/resources/static/pages/ticket-detail.html
  • src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java

Comment thread src/main/resources/static/js/common.js
Comment thread src/main/resources/static/js/common.js
Comment thread src/main/resources/static/js/dashboard.js
Comment thread src/main/resources/static/js/ticket-detail.js Outdated
Comment thread src/main/resources/static/js/ticket-detail.js

@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.

🧹 Nitpick comments (1)
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (1)

138-142: Consider dropping the redundant runtime role checks.

@PreAuthorize("hasAnyRole('CEO', 'CTO', 'ADMIN')") on rejectForm (L129) and approveAndFinalizeEmployment (L181) already enforces exactly the same role set that's re-validated at L138–141 and L185–186. Unless you want explicit defense-in-depth against @PreAuthorize being bypassed (e.g., direct method invocation from another service), these blocks are dead branches and duplicate the allow-list in two places, which makes future role changes error-prone (the past ADMIN-inconsistency bug is a good example).

If kept intentionally as defense-in-depth, consider a short comment to that effect; otherwise they can be removed. Applies equally to L185–186.

Also applies to: 185-187

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

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`
around lines 138 - 142, The runtime role-check block that inspects
loggedInRejecter.getRole() inside rejectForm (and the analogous check inside
approveAndFinalizeEmployment) duplicates the
`@PreAuthorize`("hasAnyRole('CEO','CTO','ADMIN')") annotation and should be
removed to avoid duplicate allow-lists; locate the conditional that throws
IllegalStateException in EmploymentFormService (the loggedInRejecter.getRole()
comparisons) and delete that entire if/throw branch, or if you want
defense-in-depth keep the branch but replace it with a one-line comment stating
it exists intentionally for defense-in-depth to avoid silent duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 138-142: The runtime role-check block that inspects
loggedInRejecter.getRole() inside rejectForm (and the analogous check inside
approveAndFinalizeEmployment) duplicates the
`@PreAuthorize`("hasAnyRole('CEO','CTO','ADMIN')") annotation and should be
removed to avoid duplicate allow-lists; locate the conditional that throws
IllegalStateException in EmploymentFormService (the loggedInRejecter.getRole()
comparisons) and delete that entire if/throw branch, or if you want
defense-in-depth keep the branch but replace it with a one-line comment stating
it exists intentionally for defense-in-depth to avoid silent duplication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a75a7c39-7ccb-4040-a083-5530e8b9058f

📥 Commits

Reviewing files that changed from the base of the PR and between ac7232e and d3d57d2.

📒 Files selected for processing (1)
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java

@gitnes94
gitnes94 merged commit c1d3349 into main Apr 22, 2026
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 22, 2026
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.

3 participants