Skip to content

Feature/extend employment form - #55

Merged
codebyNorthsteep merged 20 commits into
mainfrom
feature/extendEmploymentForm
Apr 10, 2026
Merged

Feature/extend employment form#55
codebyNorthsteep merged 20 commits into
mainfrom
feature/extendEmploymentForm

Conversation

@codebyNorthsteep

@codebyNorthsteep codebyNorthsteep commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces several significant enhancements to the employment form workflow, including secure password handling, improved form update and approval processes, and integration with AWS S3 for archiving. It also adds new endpoints and DTOs to support updating and filtering employment forms, and improves security practices by using password hashing.

Employment Form Workflow Enhancements:

  • Added support for updating employment forms before approval, including a new UpdateEmploymentDTO, an updateFormBeforeApproval service method, and a corresponding endpoint in EmploymentFormController to allow HR to edit pending forms. [1] [2] [3] [4]
  • Implemented stricter validation and error handling for form creation, updates, and deletions, including checks for duplicate social security numbers and authorization for modifying or deleting forms.
  • Added endpoints to fetch pending and approved forms, and a search/filter service for forms by SSN, department, and status. [1] [2]

Security Improvements:

  • Integrated password hashing using PasswordEncoder (BCrypt) for staff accounts, both in the data initializer and when creating new staff from approved forms. [1] [2] [3] [4] [5] [6]
  • Updated the pom.xml to include the Apache Commons Lang3 library, used for secure random password generation. [1] [2]

Archiving and Data Management:

  • Upon approval, employment forms are now archived as JSON files to AWS S3 using the S3Service, and the S3 key is stored in the form entity. [1] [2]
  • Improved logging throughout the employment form service for better traceability of actions like creation, update, approval, rejection, and deletion.

Codebase and Dependency Updates:

  • Refactored EmploymentFormService to use Lombok's @RequiredArgsConstructor for cleaner dependency injection and updated imports accordingly.
  • Added dependency for Apache Commons Lang3 in pom.xml for utility functions.

These changes collectively improve the security, maintainability, and usability of the employment form management features.

Summary by CodeRabbit

  • New Features

    • Expanded employment-form lifecycle: create/update before approval, approve (returns generated password), reject, delete, and fetch pending/approved/specific forms; approved/rejected forms archived to long-term storage.
  • Improvements

    • Secure password handling and storage; staff emails now unique; stricter role-based access for form actions; staff and forms reference archived S3 data.
  • Chores

    • Added commons-lang3 dependency; DB migrations for new columns and constraints.
  • Tests

    • New unit tests covering create, update authorization, reject, and approve flows.

…ploymentForm for S3 support; and apply Flyway migrations
…dling for new staff, and enhance EmploymentFormController with approved form retrieval functionality.
…lity, update/reject/delete workflows, and secure password generation; update dependencies to include Apache Commons Lang.
…ploymentMapper` for updating forms; add `UpdateEmploymentDTO` model with validation.
… duplicate SSN validation logic, streamline form retrieval methods, and enhance code readability.
…m` endpoint in `EmploymentFormController`, rename `cv_s3_key` to `employed_s3_key` in `Staff`, and enhance `DataInitializer` with password encoding.
@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b2374809-acee-463c-a52e-0da921ebc01f

📥 Commits

Reviewing files that changed from the base of the PR and between 911b3ce and ce4912e.

📒 Files selected for processing (1)
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java

📝 Walkthrough

Walkthrough

Adds commons‑lang3; registers BCrypt PasswordEncoder and encodes seeded staff passwords; extends EmploymentForm and Staff with S3 key and password fields; implements S3 JSON archival and expands employment-form lifecycle (create/update/approve/reject/delete) with DB migrations and unit tests.

Changes

Cohort / File(s) Summary
Dependency
pom.xml
Added org.apache.commons:commons-lang3:3.20.0 to compile classpath.
Security Config
src/main/java/org/example/cyberwatch/config/SecurityConfig.java
Added PasswordEncoder bean returning BCryptPasswordEncoder.
Data Initialization
src/main/java/org/example/cyberwatch/config/DataInitializer.java
Constructor now accepts PasswordEncoder; seeded staff raw passwords are encoded before persisting.
Entities & Migrations
src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java, src/main/java/org/example/cyberwatch/features/staff/model/Staff.java, src/main/resources/db/migration/V3__update_employment_form_for_s3.sql, src/main/resources/db/migration/V4__update_staff_with_s3_and_password.sql
Added employedS3Key to EmploymentForm; added password and employedS3Key to Staff and marked email unique/non-null; migrations add corresponding columns and unique constraint.
DTOs & Mapper
src/main/java/org/example/cyberwatch/features/form/model/UpdateEmploymentDTO.java, src/main/java/org/example/cyberwatch/features/form/model/EmploymentMapper.java
Introduced UpdateEmploymentDTO with validation; added updateEntity(UpdateEmploymentDTO, EmploymentForm) to mapper.
Service Layer
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java, src/main/java/org/example/cyberwatch/features/ticket/service/S3Service.java
Converted to constructor injection; added S3 JSON upload (uploadJsonData); added ObjectMapper and PasswordEncoder usage; approval now archives JSON to S3, generates/encodes a temp password, creates Staff, and adds update/reject/delete operations and helper methods.
Controller Layer
src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java
Fixed @PreAuthorize expressions; approve now accepts Authentication and returns message; added PUT update, GET approved, GET by id, POST reject, and DELETE endpoints; adjusted pending/approved endpoints and responses.
Config (S3/Jackson) & Imports
src/main/java/org/example/cyberwatch/config/S3Config.java
Import reordering and whitespace tweaks only; no functional changes to S3Client bean.
Tests
src/test/java/org/example/cyberwatch/features/form/EmploymentFormControllerTests.java, src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java
Adjusted test package; added unit tests for EmploymentFormService (create, unauthorized update, reject, approve/archive/save-staff).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Controller as EmploymentFormController
    participant Service as EmploymentFormService
    participant DB as Database
    participant JSON as ObjectMapper
    participant S3 as S3Service
    participant Encoder as PasswordEncoder

    Client->>Controller: POST /employment (create)
    Controller->>Service: createForm(dto, hrEmail)
    Service->>DB: save(employmentForm)
    DB-->>Service: saved form
    Service-->>Controller: EmploymentFormDTO
    Controller-->>Client: 201 Created

    Client->>Controller: POST /{id}/approve
    Controller->>Service: approveAndFinalizeEmployment(id, approverEmail)
    Service->>DB: findById(id)
    DB-->>Service: EmploymentForm (PENDING)
    Service->>JSON: serialize(form)
    JSON-->>Service: jsonData
    Service->>S3: uploadJsonData(key, jsonData)
    S3-->>Service: s3Key
    Service->>Encoder: generate & encode(password)
    Encoder-->>Service: hashedPassword
    Service->>DB: staffRepository.save(new Staff with hashedPassword)
    DB-->>Service: saved staff
    Service->>DB: save(updated EmploymentForm with s3Key/status)
    DB-->>Service: saved form
    Service-->>Controller: approval message (contains temp password)
    Controller-->>Client: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Ericthilen
  • gitnes94
  • alicewersen-rgb

"🐰
I dug a burrow of code so neat,
Passwords hashed and S3 keys meet,
Forms tucked safe in JSON clay,
New staff hop in to start their day,
Tests give carrot — time to greet!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.50% 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 title 'Feature/extend employment form' is vague and uses a generic prefix that does not meaningfully convey the specific enhancements made in this changeset. Consider a more descriptive title that highlights the primary change, such as 'Add employment form approval workflow with S3 archiving and password hashing' or 'Enhance employment form lifecycle with archiving and security controls'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/extendEmploymentForm

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

🧹 Nitpick comments (9)
pom.xml (1)

166-166: Optional: Remove redundant scope declaration.

The <scope>compile</scope> declaration is redundant since compile is the default scope in Maven. You can safely remove this line for cleaner configuration.

♻️ Simplify dependency declaration
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
             <version>3.20.0</version>
-            <scope>compile</scope>
         </dependency>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` at line 166, The dependency declaration contains an explicit
<scope>compile</scope> which is redundant because Maven's default scope is
compile; remove the <scope>compile</scope> element from the dependency block(s)
(the XML element containing the dependency where <scope>compile</scope> appears)
so the pom.xml remains functionally identical but cleaner and less verbose.
src/main/java/org/example/cyberwatch/features/form/model/EmploymentMapper.java (1)

45-46: Consider throwing on null inputs for void methods.

Unlike toDTO/toEntity which return null signaling no result, updateEntity silently does nothing when either parameter is null. This could mask bugs in callers that accidentally pass null.

♻️ Proposed defensive approach
     public void updateEntity(UpdateEmploymentDTO dto, EmploymentForm entity) {
-        if (dto == null || entity == null) return;
+        if (dto == null || entity == null) {
+            throw new IllegalArgumentException("DTO and entity must not be null");
+        }
 
         entity.setSocialSecurityNumber(dto.getSocialSecurityNumber());
🤖 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/model/EmploymentMapper.java`
around lines 45 - 46, The updateEntity method currently returns silently when
dto or entity is null; change it to validate inputs and throw a descriptive
unchecked exception instead (e.g., throw new IllegalArgumentException or use
Objects.requireNonNull) so callers immediately fail fast; update the
updateEntity(UpdateEmploymentDTO dto, EmploymentForm entity) implementation to
check dto and entity and throw with clear messages like "dto must not be null" /
"entity must not be null".
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (2)

144-145: Semantic confusion: Using approvedBy to store rejector.

Setting approvedBy for a rejected form is semantically misleading. Consider adding a separate rejectedBy field or renaming the field to something more generic like processedBy.

This is a data model consideration that may require a migration, so flagging for awareness rather than immediate action.

🤖 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 - 145, The code in EmploymentFormService sets
ApprovalStatus.REJECTED and then calls form.setApprovedBy(rejector), which is
semantically confusing; update the data model and service to avoid misusing
approvedBy by either adding a new rejectedBy field (and use
form.setRejectedBy(rejector) when ApprovalStatus.REJECTED) or renaming
approvedBy to a neutral processedBy and use form.setProcessedBy(rejector) for
both approvals and rejections; update the EmploymentFormService usage (the
setStatus(...) call sites and any getters) and prepare a data migration plan to
populate the new field from existing approvedBy values if you change the schema
name.

84-93: Performance: Loading all forms into memory for filtering.

searchAndFilterForms calls findAll() and filters in-memory. This won't scale well as the number of forms grows. Consider implementing repository query methods with proper filtering.

♻️ Recommended approach

Add a custom query method to EmploymentFormRepository:

`@Query`("SELECT f FROM EmploymentForm f WHERE " +
       "(:ssn IS NULL OR f.socialSecurityNumber LIKE %:ssn%) AND " +
       "(:department IS NULL OR f.department = :department) AND " +
       "(:status IS NULL OR f.status = :status)")
List<EmploymentForm> searchForms(`@Param`("ssn") String ssn, 
                                  `@Param`("department") Department department,
                                  `@Param`("status") ApprovalStatus status);
🤖 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 84 - 93, searchAndFilterForms currently loads all entities via
employmentFormRepository.findAll() and filters in-memory, which doesn't scale;
add a repository query method (e.g. in EmploymentFormRepository define
searchForms(String ssn, Department department, ApprovalStatus status) using a
JPQL/@Query with optional parameters for ssn (LIKE), department and status),
then replace the findAll() call in searchAndFilterForms with a call to
employmentFormRepository.searchForms(...) and keep mapping with
employmentMapper::toDTO so filtering happens in the database rather than
in-memory.
src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java (3)

95-113: Test doesn't verify the rejector assignment.

The test verifies that status changes to REJECTED and save is called, but doesn't assert that form.getApprovedBy() was set to the manager. This is an important part of the rejection flow for audit purposes.

💚 Proposed enhancement
         // Assert
         assertEquals(ApprovalStatus.REJECTED, form.getStatus());
+        assertEquals(manager, form.getApprovedBy());
         verify(formRepository).save(form);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java`
around lines 95 - 113, The test rejectForm_Success in EmploymentFormServiceTest
currently verifies status and save but doesn't assert the rejector assignment;
update the test to assert that the EmploymentForm's approver field was set to
the manager returned by staffRepository.findByEmail (verify form.getApprovedBy()
equals the local manager instance used in the Arrange), and keep the existing
verify(formRepository).save(form) and status assertion; reference the rejectForm
call on service, the manager variable, and form.getApprovedBy()/EmploymentForm
to locate where to add the assertion.

85-85: Remove non-English comment.

The comment // Rättat här: existingForm istället för existingDto appears to be a development note in Swedish. Consider removing it or translating to English for consistency.

✏️ Proposed fix
         EmploymentForm existingForm = new EmploymentForm();
         existingForm.setCreatedBy(creator);
-        existingForm.setStatus(ApprovalStatus.PENDING); // Rättat här: existingForm istället för existingDto
+        existingForm.setStatus(ApprovalStatus.PENDING);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java`
at line 85, Remove or translate the Swedish inline comment after the call to
existingForm.setStatus(ApprovalStatus.PENDING) in the EmploymentFormServiceTest
class; locate the occurrence of existingForm.setStatus(...) and either delete
the comment `// Rättat här: existingForm istället för existingDto` or replace it
with a brief English comment such as `// fixed: use existingForm instead of
existingDto` to keep test code comments consistent.

45-143: Consider adding test coverage for edge cases and remaining methods.

The current tests cover the happy paths well. Consider adding tests for:

  • deleteForm authorization and status checks
  • searchAndFilterForms filtering logic
  • SSN validation when SSN changes during update
  • Duplicate SSN rejection in createForm

Would you like me to generate additional test cases for these scenarios?

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

In
`@src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java`
around lines 45 - 143, Add unit tests covering the missing edge cases: create
tests for deleteForm to assert authorization and status checks (e.g., call
deleteForm with non-owner and with non-deletable statuses to expect exceptions),
add tests for searchAndFilterForms to verify filtering/sorting/pagination
behavior with various criteria, add tests for updateFormBeforeApproval to assert
SSN validation when SSN is changed (reject when duplicate via
staffRepository.existsBySocialSecurityNumber or
formRepository.existsBySocialSecurityNumber), and add a test for createForm to
assert duplicate SSN rejection (mock formRepository.existsBySocialSecurityNumber
and staffRepository.existsBySocialSecurityNumber to return true and expect
exception). Reference test targets: deleteForm, searchAndFilterForms,
updateFormBeforeApproval, and createForm; mock repositories (formRepository,
staffRepository), mapper methods (mapper.toEntity/formToStaff), and assert
expected exceptions and repository interactions.
src/main/java/org/example/cyberwatch/features/ticket/service/S3Service.java (1)

41-58: Consider adding input validation for key and jsonData parameters.

The method doesn't validate inputs before attempting the S3 upload. Null or empty values would result in less informative errors from the S3 SDK.

🛡️ Proposed defensive validation
     public String uploadJsonData(String key, String jsonData) {
+        if (key == null || key.isBlank()) {
+            throw new IllegalArgumentException("S3 key cannot be null or blank");
+        }
+        if (jsonData == null) {
+            throw new IllegalArgumentException("JSON data cannot be null");
+        }
         try {
             PutObjectRequest putObjectRequest = PutObjectRequest.builder()

Additionally, be aware that this method will silently overwrite existing objects at the same key if S3 versioning is not enabled on the bucket.

🤖 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/ticket/service/S3Service.java`
around lines 41 - 58, The uploadJsonData method lacks input validation and can
pass null/empty values into s3Client.putObject; add defensive checks at the
start of uploadJsonData for key and jsonData (e.g., null or blank key, null or
empty jsonData) and throw a clear IllegalArgumentException with context (include
the parameter name and expected format). Optionally validate jsonData is not
just whitespace or use a lightweight JSON sanity check before creating
RequestBody.fromString(jsonData). Keep existing logging (log.info) and preserve
exception wrapping for S3 errors; reference uploadJsonData, bucketName, s3Client
and RequestBody when adding the checks so they sit before calling
s3Client.putObject.
src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java (1)

26-27: Remove or translate the development comment.

The comment //get form att fylla i, används även när man updaterar? is in Swedish and appears to be a development note. Consider removing it or providing a clear English comment.

✏️ Proposed fix
-    //get form att fylla i, används även när man updaterar?
-
     `@PostMapping`(value = "/employment")
🤖 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/controller/EmploymentFormController.java`
around lines 26 - 27, The inline Swedish developer comment in
EmploymentFormController ("//get form att fylla i, används även när man
updaterar?") should be removed or replaced with a clear English comment; locate
the comment in the EmploymentFormController class and either delete it or
replace it with a concise English description of the method/behavior it was
annotating (e.g., explaining that the endpoint returns the employment form for
create/update scenarios) so the codebase contains no non-English development
notes.
🤖 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/controller/EmploymentFormController.java`:
- Around line 45-68: Add two endpoints in EmploymentFormController: a POST
mapping for "/{id}/reject" annotated with `@PreAuthorize`("hasRole('CEO') or
hasRole('CTO')") that accepts `@PathVariable` Long id, `@RequestBody` String
rejectionReason (or a small DTO) and Authentication auth, then calls
employmentFormService.rejectForm(id, rejectionReason, auth.getName()) and
returns the appropriate ResponseEntity (e.g., OK or no-content with payload as
needed); and a DELETE mapping for "/{id}" with authorization that allows the
form creator or MANAGEMENT role (e.g., `@PreAuthorize`("hasRole('MANAGEMENT') or
`@authenticationPrincipal`?.username == ???" or simply accept Authentication and
rely on employmentFormService.deleteForm(id, auth.getName()) to enforce creator
check), implement method signature deleteForm(`@PathVariable` Long id,
Authentication auth) that calls employmentFormService.deleteForm(id,
auth.getName()) and returns ResponseEntity.noContent() or appropriate response.
Ensure method names and service calls reference
employmentFormService.rejectForm(...) and employmentFormService.deleteForm(...).

In
`@src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java`:
- Around line 67-68: The entity field in EmploymentForm named employedS3Key is
annotated with `@Column`(name = "cv_s3_key") which mismatches the database
migration V3__update_employment_form_for_s3.sql that creates employed_s3_key;
update the `@Column` on the employedS3Key field (in class EmploymentForm) to use
name = "employed_s3_key" so the JPA mapping matches the migration and S3 keys
persist, or alternatively adjust the migration to create cv_s3_key if you prefer
the entity name.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 200-201: The method in EmploymentFormService currently returns the
rawPassword and logs it; remove the raw password from the returned string and
from any logs (replace with masked value or omit), and instead deliver the
password via a secure channel: call an existing EmailService (e.g.,
EmailService.sendTemporaryPassword or sendPasswordToEmployee) with the
form/employee identifier and rawPassword, or create and invoke a
securePasswordDelivery method to handle emailing and one-time retrieval; update
the return value to a safe message or a minimal secure response object (no
password field) and ensure logger.info("Form {} approved by {}", formId,
loggedInManagement) remains but does not include rawPassword.
- Around line 113-114: In EmploymentFormService, the check that compares
existingForm.getCreatedBy().getEmail() to loggedInHrEmail can NPE if
getCreatedBy() is null; change the logic to first verify
existingForm.getCreatedBy() != null before calling getEmail(), and if createdBy
is null throw a clear IllegalStateException (or a custom exception) with a
message like "Form has no creator; only the HR staff who created this form can
update it" so the intent remains the same but avoids a NullPointerException.
- Line 232: Replace the use of the Social Security Number in the S3 object key
to avoid embedding PII: in EmploymentFormService locate the s3Key construction
(String s3Key = "archive/employments/" + form.getSocialSecurityNumber() +
".json") and change it to use a non-PII identifier such as form.getId(), a
generated UUID, or a hashed token derived from the form ID (e.g.,
UUID.randomUUID() or form.getId().toString()) so the path becomes
archive/employments/{nonPiiId}.json; ensure any downstream code that reads or
deletes the object uses the same non-PII key.
- Around line 163-165: The delete authorization check in EmploymentFormService
currently compares requester.getRole().name() to the non-existent "MANAGEMENT"
role, so update the condition to compare against actual Role enum values (for
example Role.ADMIN and/or other managerial roles like Role.CEO or
Role.PROJECT_MANAGER) instead of the string "MANAGEMENT"; modify the if to
something like checking requester.getRole() == Role.ADMIN (or use a set:
EnumSet.of(Role.ADMIN, Role.CEO,
Role.PROJECT_MANAGER).contains(requester.getRole())) to allow intended
management roles to delete, and add an import for the Role enum if not already
imported.
- Around line 205-208: The generateSecurePassword method currently uses
RandomStringUtils.random(...), which is not cryptographically secure; replace
that call in EmploymentFormService.generateSecurePassword with the secure API by
calling RandomStringUtils.secure().nextAlphanumeric(12) so the method uses
SecureRandom-backed generation for passwords instead of the static random(...)
variant.

In `@src/main/resources/db/migration/V3__update_employment_form_for_s3.sql`:
- Around line 1-3: Migration added column employed_s3_key but the JPA entity
EmploymentForm (field annotated with `@Column`(name = "cv_s3_key")) expects
cv_s3_key, causing Hibernate to read/write a non-existent column; update the SQL
in V3__update_employment_form_for_s3.sql to add column cv_s3_key VARCHAR(500)
(or alternatively change the `@Column` name in EmploymentForm.java to
"employed_s3_key") so the DB column name and the entity mapping (the `@Column` on
the EmploymentForm field at line ~67) match.

---

Nitpick comments:
In `@pom.xml`:
- Line 166: The dependency declaration contains an explicit
<scope>compile</scope> which is redundant because Maven's default scope is
compile; remove the <scope>compile</scope> element from the dependency block(s)
(the XML element containing the dependency where <scope>compile</scope> appears)
so the pom.xml remains functionally identical but cleaner and less verbose.

In
`@src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java`:
- Around line 26-27: The inline Swedish developer comment in
EmploymentFormController ("//get form att fylla i, används även när man
updaterar?") should be removed or replaced with a clear English comment; locate
the comment in the EmploymentFormController class and either delete it or
replace it with a concise English description of the method/behavior it was
annotating (e.g., explaining that the endpoint returns the employment form for
create/update scenarios) so the codebase contains no non-English development
notes.

In
`@src/main/java/org/example/cyberwatch/features/form/model/EmploymentMapper.java`:
- Around line 45-46: The updateEntity method currently returns silently when dto
or entity is null; change it to validate inputs and throw a descriptive
unchecked exception instead (e.g., throw new IllegalArgumentException or use
Objects.requireNonNull) so callers immediately fail fast; update the
updateEntity(UpdateEmploymentDTO dto, EmploymentForm entity) implementation to
check dto and entity and throw with clear messages like "dto must not be null" /
"entity must not be null".

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 144-145: The code in EmploymentFormService sets
ApprovalStatus.REJECTED and then calls form.setApprovedBy(rejector), which is
semantically confusing; update the data model and service to avoid misusing
approvedBy by either adding a new rejectedBy field (and use
form.setRejectedBy(rejector) when ApprovalStatus.REJECTED) or renaming
approvedBy to a neutral processedBy and use form.setProcessedBy(rejector) for
both approvals and rejections; update the EmploymentFormService usage (the
setStatus(...) call sites and any getters) and prepare a data migration plan to
populate the new field from existing approvedBy values if you change the schema
name.
- Around line 84-93: searchAndFilterForms currently loads all entities via
employmentFormRepository.findAll() and filters in-memory, which doesn't scale;
add a repository query method (e.g. in EmploymentFormRepository define
searchForms(String ssn, Department department, ApprovalStatus status) using a
JPQL/@Query with optional parameters for ssn (LIKE), department and status),
then replace the findAll() call in searchAndFilterForms with a call to
employmentFormRepository.searchForms(...) and keep mapping with
employmentMapper::toDTO so filtering happens in the database rather than
in-memory.

In `@src/main/java/org/example/cyberwatch/features/ticket/service/S3Service.java`:
- Around line 41-58: The uploadJsonData method lacks input validation and can
pass null/empty values into s3Client.putObject; add defensive checks at the
start of uploadJsonData for key and jsonData (e.g., null or blank key, null or
empty jsonData) and throw a clear IllegalArgumentException with context (include
the parameter name and expected format). Optionally validate jsonData is not
just whitespace or use a lightweight JSON sanity check before creating
RequestBody.fromString(jsonData). Keep existing logging (log.info) and preserve
exception wrapping for S3 errors; reference uploadJsonData, bucketName, s3Client
and RequestBody when adding the checks so they sit before calling
s3Client.putObject.

In
`@src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java`:
- Around line 95-113: The test rejectForm_Success in EmploymentFormServiceTest
currently verifies status and save but doesn't assert the rejector assignment;
update the test to assert that the EmploymentForm's approver field was set to
the manager returned by staffRepository.findByEmail (verify form.getApprovedBy()
equals the local manager instance used in the Arrange), and keep the existing
verify(formRepository).save(form) and status assertion; reference the rejectForm
call on service, the manager variable, and form.getApprovedBy()/EmploymentForm
to locate where to add the assertion.
- Line 85: Remove or translate the Swedish inline comment after the call to
existingForm.setStatus(ApprovalStatus.PENDING) in the EmploymentFormServiceTest
class; locate the occurrence of existingForm.setStatus(...) and either delete
the comment `// Rättat här: existingForm istället för existingDto` or replace it
with a brief English comment such as `// fixed: use existingForm instead of
existingDto` to keep test code comments consistent.
- Around line 45-143: Add unit tests covering the missing edge cases: create
tests for deleteForm to assert authorization and status checks (e.g., call
deleteForm with non-owner and with non-deletable statuses to expect exceptions),
add tests for searchAndFilterForms to verify filtering/sorting/pagination
behavior with various criteria, add tests for updateFormBeforeApproval to assert
SSN validation when SSN is changed (reject when duplicate via
staffRepository.existsBySocialSecurityNumber or
formRepository.existsBySocialSecurityNumber), and add a test for createForm to
assert duplicate SSN rejection (mock formRepository.existsBySocialSecurityNumber
and staffRepository.existsBySocialSecurityNumber to return true and expect
exception). Reference test targets: deleteForm, searchAndFilterForms,
updateFormBeforeApproval, and createForm; mock repositories (formRepository,
staffRepository), mapper methods (mapper.toEntity/formToStaff), and assert
expected exceptions and repository interactions.
🪄 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: 7426f4be-b0ed-42e8-b46a-44ff0733178d

📥 Commits

Reviewing files that changed from the base of the PR and between 81cbf95 and 581f274.

📒 Files selected for processing (14)
  • pom.xml
  • src/main/java/org/example/cyberwatch/config/DataInitializer.java
  • src/main/java/org/example/cyberwatch/config/SecurityConfig.java
  • src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java
  • src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java
  • src/main/java/org/example/cyberwatch/features/form/model/EmploymentMapper.java
  • src/main/java/org/example/cyberwatch/features/form/model/UpdateEmploymentDTO.java
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
  • src/main/java/org/example/cyberwatch/features/staff/model/Staff.java
  • src/main/java/org/example/cyberwatch/features/ticket/service/S3Service.java
  • src/main/resources/db/migration/V3__update_employment_form_for_s3.sql
  • src/main/resources/db/migration/V4__update_staff_with_s3_and_password.sql
  • src/test/java/org/example/cyberwatch/features/form/EmploymentFormControllerTests.java
  • src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java

Comment thread src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java Outdated
… secure password generation, S3 archiving update, and `employed_s3_key` renaming; add `ObjectMapper` to `SecurityConfig`.

@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

♻️ Duplicate comments (2)
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (2)

203-205: ⚠️ Potential issue | 🟠 Major

Do not return the generated password from this service.

This still leaks a credential into HTTP logs, traces, proxies, and client storage. Return a neutral success message and deliver the temporary password through a one-time secure channel instead.

🤖 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 203 - 205, The service method in EmploymentFormService currently
logs approval and returns the generated rawPassword (see logger.info("Form {}
approved by {}", formId, loggedInManagement) and the return that appends
rawPassword); remove the rawPassword from the HTTP response and instead return a
neutral success message (e.g., "Employment approved, temporary password
delivered") and invoke a secure one-time delivery mechanism (call an existing
EmailService/NotificationService or create a OneTimeSecretService method such as
sendTemporaryPassword(formId, userId, rawPassword) or storeOneTimeSecret(...))
to deliver the temporary password; ensure no password value is included in logs,
responses, or exceptions and replace the current return that exposes rawPassword
with the neutral message.

164-166: ⚠️ Potential issue | 🟡 Minor

Guard createdBy before comparing emails.

A null creator will throw before the CEO/CTO fallback can run, so malformed or legacy rows turn this authorization check into a 500.

Suggested fix
-        if (!form.getCreatedBy().getEmail().equals(loggedInEmail)
+        if ((form.getCreatedBy() == null || !form.getCreatedBy().getEmail().equals(loggedInEmail))
                 && requester.getRole() != Role.CEO
                 && requester.getRole() != Role.CTO) {
             throw new IllegalStateException("Only the HR staff who created this form or management can delete it");
         }
🤖 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 164 - 166, The authorization check in EmploymentFormService
currently calls form.getCreatedBy().getEmail() without guarding for a null
createdBy, causing an NPE for legacy/malformed rows; change the condition so you
first test whether form.getCreatedBy() is null (treating null as “not the
creator”) or the creator's email does not equal loggedInEmail, and only then
apply the requester role fallback checks (requester.getRole() != Role.CEO and
requester.getRole() != Role.CTO); update the existing if-condition that
references form.getCreatedBy().getEmail(), loggedInEmail, requester.getRole(),
Role.CEO and Role.CTO to perform the null check before any getEmail() call.
🧹 Nitpick comments (1)
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (1)

85-93: Push search/filtering down to the repository.

findAll() materializes the full table and then filters SSN/department/status in memory, so this path will not benefit from DB-side filtering or pagination as the dataset grows.

🤖 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 85 - 93, searchAndFilterForms currently calls
employmentFormRepository.findAll() and does in-memory filtering; change it to
push filtering into the data layer by adding repository-level query(s) or a JPA
Specification so the DB performs the SSN/department/status predicates (and
supports pagination). Update EmploymentFormRepository to provide either dynamic
query methods (e.g. findBySocialSecurityNumberContainingAndDepartmentAndStatus
with nullable params handled) or implement a Specification/Criteria-based method
(e.g. findAll(Specification<EmploymentForm>, Pageable)), then call that new repo
method from searchAndFilterForms (instead of findAll()) and map results with
employmentMapper::toDTO.
🤖 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 189-201: The S3 archive call (archiveToS3(form)) happens before
the database persists and can roll back, causing orphaned S3 writes; move the
archiveToS3 invocation to run only after the DB transaction successfully
commits—either by calling it after employmentFormRepository.save(form) and
staffRepository.save(newStaff) within a non-transactional context or by
registering a transaction synchronization / publishing a post-commit event
(e.g., using TransactionSynchronizationManager or an
`@TransactionalEventListener`) that invokes archiveToS3(form); also ensure
archiveToS3 is idempotent or handle exceptions/logging separately so failures do
not affect DB state.
- Around line 198-201: EmploymentFormService creates a Staff via
employmentMapper.formToStaff(form) but never sets the employedS3Key, so the
newStaff saved by staffRepository.save(newStaff) has no archive reference; fix
by assigning the archive key from the form to the Staff before saving (e.g.,
ensure EmploymentMapper.formToStaff copies employedS3Key or explicitly call
newStaff.setEmployedS3Key(form.getEmployedS3Key()) after mapping and before
staffRepository.save(newStaff)) so the persisted Staff row contains the
employedS3Key.

---

Duplicate comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 203-205: The service method in EmploymentFormService currently
logs approval and returns the generated rawPassword (see logger.info("Form {}
approved by {}", formId, loggedInManagement) and the return that appends
rawPassword); remove the rawPassword from the HTTP response and instead return a
neutral success message (e.g., "Employment approved, temporary password
delivered") and invoke a secure one-time delivery mechanism (call an existing
EmailService/NotificationService or create a OneTimeSecretService method such as
sendTemporaryPassword(formId, userId, rawPassword) or storeOneTimeSecret(...))
to deliver the temporary password; ensure no password value is included in logs,
responses, or exceptions and replace the current return that exposes rawPassword
with the neutral message.
- Around line 164-166: The authorization check in EmploymentFormService
currently calls form.getCreatedBy().getEmail() without guarding for a null
createdBy, causing an NPE for legacy/malformed rows; change the condition so you
first test whether form.getCreatedBy() is null (treating null as “not the
creator”) or the creator's email does not equal loggedInEmail, and only then
apply the requester role fallback checks (requester.getRole() != Role.CEO and
requester.getRole() != Role.CTO); update the existing if-condition that
references form.getCreatedBy().getEmail(), loggedInEmail, requester.getRole(),
Role.CEO and Role.CTO to perform the null check before any getEmail() call.

---

Nitpick comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 85-93: searchAndFilterForms currently calls
employmentFormRepository.findAll() and does in-memory filtering; change it to
push filtering into the data layer by adding repository-level query(s) or a JPA
Specification so the DB performs the SSN/department/status predicates (and
supports pagination). Update EmploymentFormRepository to provide either dynamic
query methods (e.g. findBySocialSecurityNumberContainingAndDepartmentAndStatus
with nullable params handled) or implement a Specification/Criteria-based method
(e.g. findAll(Specification<EmploymentForm>, Pageable)), then call that new repo
method from searchAndFilterForms (instead of findAll()) and map results with
employmentMapper::toDTO.
🪄 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: 3ebef616-3b3b-46fd-9d95-c8e8dc76a023

📥 Commits

Reviewing files that changed from the base of the PR and between 581f274 and 1d64b04.

📒 Files selected for processing (3)
  • src/main/java/org/example/cyberwatch/config/SecurityConfig.java
  • src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java
  • src/main/java/org/example/cyberwatch/config/SecurityConfig.java

… reordering database operations before S3 archiving; improve password generation and staff creation logic.
…, retrieval by ID, rejection with reason, and deletion; improve pending and approved forms handling.
…ate `approveForm` to return success response with service result.

@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

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

84-94: In-memory filtering will not scale.

searchAndFilterForms loads all forms via findAll() and filters in Java. This is inefficient for large datasets and bypasses database indexing.

Consider using a repository query with @Query or Spring Data JPA Specifications to push filtering to the database.

♻️ Example: Push filtering to repository
// In EmploymentFormRepository
`@Query`("SELECT f FROM EmploymentForm f WHERE " +
       "(:ssn IS NULL OR f.socialSecurityNumber LIKE %:ssn%) AND " +
       "(:department IS NULL OR f.department = :department) AND " +
       "(:status IS NULL OR f.status = :status)")
List<EmploymentForm> searchForms(
    `@Param`("ssn") String socialSecurityNumber,
    `@Param`("department") Department department,
    `@Param`("status") ApprovalStatus status);

Then in the service:

 public List<EmploymentFormDTO> searchAndFilterForms(String socialSecurityNumber, Department department, ApprovalStatus status) {
-    List<EmploymentForm> forms = employmentFormRepository.findAll();
-
-    return forms.stream()
-            .filter(form -> socialSecurityNumber == null || form.getSocialSecurityNumber().contains(socialSecurityNumber))
-            .filter(form -> department == null || form.getDepartment().equals(department))
-            .filter(form -> status == null || form.getStatus().equals(status))
-            .map(employmentMapper::toDTO)
-            .toList();
+    return employmentMapper.toDTOList(
+            employmentFormRepository.searchForms(socialSecurityNumber, department, status));
 }
🤖 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 84 - 94, The method searchAndFilterForms currently loads all
records via employmentFormRepository.findAll() and filters in-memory, which
won't scale; change this to delegate filtering to the database by adding a
repository query or Specification in EmploymentFormRepository (e.g., a method
like searchForms or a Specification-based finder) that accepts
socialSecurityNumber, Department and ApprovalStatus and performs the conditional
WHERE predicates, then update searchAndFilterForms to call that new repository
method and map results with employmentMapper::toDTO.
src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java (1)

81-90: Consider PII exposure in query parameters.

Passing socialSecurityNumber as a query parameter means SSN values may appear in server access logs, browser history, and proxy logs. For sensitive PII searches, consider using a POST request with a request body instead.

🤖 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/controller/EmploymentFormController.java`
around lines 81 - 90, The current GET endpoint searchForms in
EmploymentFormController accepts socialSecurityNumber as a query parameter
(risking PII in logs); change the API to accept a POST (e.g.,
`@PostMapping`("/search")) that consumes a request body DTO (create e.g.,
EmploymentFormSearchRequest with socialSecurityNumber, department, status) and
have searchForms accept that DTO instead of `@RequestParam`; update the call to
employmentFormService.searchAndFilterForms to take the DTO (or its fields) and
keep the same `@PreAuthorize` check and response type, ensuring any
client/consumer updates and controller tests are adjusted accordingly.
🤖 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/S3Config.java`:
- Around line 36-39: S3Config currently defines a plain ObjectMapper in the
objectMapper() bean which causes LocalDateTime to be serialized as arrays;
either delete the objectMapper() bean to let Spring Boot inject its
auto-configured ObjectMapper, or if you must keep the custom bean, update the
objectMapper() method to register Jackson's JavaTimeModule and disable
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS so LocalDateTime is serialized as
ISO‑8601 strings.

In
`@src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java`:
- Around line 38-45: The approveForm method in EmploymentFormController is
calling employmentFormService.approveAndFinalizeEmployment(id,
authentication.getName()) twice which causes a failure on the second call; fix
by calling employmentFormService.approveAndFinalizeEmployment(...) only once,
capture its return value in a local variable, and return that variable in the
ResponseEntity instead of invoking the service a second time (adjust the method
signature/return handling in approveForm accordingly).

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 192-211: The S3 key is being set after both EmploymentForm and
Staff are persisted, so persist the S3 key first: call archiveToS3(form) before
converting/saving Staff and before employmentFormRepository.save, then after
archiveToS3 returns set form.getEmployedS3Key and copy that value into newStaff
via newStaff.setEmployedS3Key(...); finally encode password and persist newStaff
and employmentFormRepository.save(form). Update the flow in
EmploymentFormService so archiveToS3(form) runs prior to
employmentMapper.formToStaff(form), staffRepository.save(...), and
employmentFormRepository.save(...), while keeping the existing error logging
around archiveToS3.
- Around line 170-174: The current check in EmploymentFormService uses
form.getCreatedBy().getEmail() which can NPE if createdBy is null; update the
conditional to first handle a null createdBy (e.g., treat null as "not created
by the requester") before calling getEmail(): check form.getCreatedBy() != null
&& form.getCreatedBy().getEmail().equals(loggedInEmail) (or invert logic to test
createdBy == null || !createdBy.getEmail().equals(loggedInEmail)) combined with
the existing role checks on requester.getRole() against Role.CEO and Role.CTO,
and only throw the IllegalStateException when the requester is neither the
creator (safely evaluated) nor a manager.
- Around line 145-153: EmploymentFormService currently saves the form
(employmentFormRepository.save(form)) before calling archiveToS3(form), but
archiveToS3 sets form.setEmployedS3Key(s3Key) so the S3 key is never persisted;
move the save to after archiving (or call employmentFormRepository.save(form)
again after archiveToS3 returns) so the updated employedS3Key is persisted,
ensuring any exceptions from archiveToS3 are still logged via the existing
try/catch around archiveToS3.

---

Nitpick comments:
In
`@src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java`:
- Around line 81-90: The current GET endpoint searchForms in
EmploymentFormController accepts socialSecurityNumber as a query parameter
(risking PII in logs); change the API to accept a POST (e.g.,
`@PostMapping`("/search")) that consumes a request body DTO (create e.g.,
EmploymentFormSearchRequest with socialSecurityNumber, department, status) and
have searchForms accept that DTO instead of `@RequestParam`; update the call to
employmentFormService.searchAndFilterForms to take the DTO (or its fields) and
keep the same `@PreAuthorize` check and response type, ensuring any
client/consumer updates and controller tests are adjusted accordingly.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 84-94: The method searchAndFilterForms currently loads all records
via employmentFormRepository.findAll() and filters in-memory, which won't scale;
change this to delegate filtering to the database by adding a repository query
or Specification in EmploymentFormRepository (e.g., a method like searchForms or
a Specification-based finder) that accepts socialSecurityNumber, Department and
ApprovalStatus and performs the conditional WHERE predicates, then update
searchAndFilterForms to call that new repository method and map results with
employmentMapper::toDTO.
🪄 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: 298703c8-9996-49b7-9375-129d39a76892

📥 Commits

Reviewing files that changed from the base of the PR and between ac5d675 and e47dd6c.

📒 Files selected for processing (4)
  • src/main/java/org/example/cyberwatch/config/S3Config.java
  • src/main/java/org/example/cyberwatch/config/SecurityConfig.java
  • src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/example/cyberwatch/config/SecurityConfig.java

Comment thread src/main/java/org/example/cyberwatch/config/S3Config.java Outdated
…mproved transactional integrity, fix role-based validation logic, and adjust `EmploymentFormController` for consistent service method usage.

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

♻️ Duplicate comments (3)
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (3)

211-213: ⚠️ Potential issue | 🔴 Critical

The approval response still returns the raw password.

Line 213 includes the generated password in the service response, and the controller returns it verbatim. That exposes credentials to HTTP logs, proxies, and client storage. Return a neutral success message here and deliver the password only through the planned secure channel.

🤖 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 211 - 213, The service currently returns the generated rawPassword
string in the response (see EmploymentFormService where logger.info("Form {}
approved by {}", formId, loggedInManagement) and the return that concatenates
rawPassword); remove the raw password from the return value and instead return a
neutral success message (e.g., "Employment approved; credentials delivered via
secure channel"), and ensure no logging of rawPassword anywhere in
EmploymentFormService or related controller methods (update the controller
method that consumes the service return to stop expecting/propagating the
password). Leave the password delivery responsibility to the planned
secure/email flow so credentials are never exposed in service responses, logs,
or HTTP payloads.

147-153: ⚠️ Potential issue | 🔴 Critical

S3 archival still happens before the transaction is durable.

Lines 148 and 202 upload to S3 while the DB transaction is still open. Any later failure in staffRepository.save(...), employmentFormRepository.save(...), or the commit leaves orphaned archives and mismatched state. Move the upload to an after-commit/outbox path.

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 147 - 153, The S3 upload (archiveToS3) is currently executed while
the DB transaction is still open, which can leave orphaned S3 objects if
subsequent saves or the commit fail; move the upload to run only after a
successful commit by deferring archiveToS3 until transaction completion (for
example use Spring's
TransactionSynchronizationManager.registerSynchronization/afterCommit or publish
an event and handle it with `@TransactionalEventListener`(phase = AFTER_COMMIT)),
or persist an outbox record alongside
employmentFormRepository.save/staffRepository.save and have a separate worker
send to S3; update EmploymentFormService to save entities inside the transaction
first (employmentFormRepository.save, staffRepository.save) and trigger
archiveToS3 only in the after-commit/outbox path.

171-174: ⚠️ Potential issue | 🟠 Major

Null createdBy now opens a delete-authorization bypass.

Because Line 171 guards the whole check with form.getCreatedBy() != null, a form without a creator skips authorization entirely, so any HR caller that reaches this method can delete it. Treat createdBy == null as “not creator” and require isCreator || isManagement.

🤖 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 171 - 174, The current check lets forms with null createdBy bypass
authorization; change it to treat null createdBy as "not creator" by computing a
boolean isCreator = form.getCreatedBy() != null &&
form.getCreatedBy().getEmail().equals(loggedInEmail) (or equivalent) and then
require isCreator OR management; i.e. replace the existing compound guarded-if
with a check like if (!isCreator && requester.getRole() != Role.CEO &&
requester.getRole() != Role.CTO) throw new IllegalStateException(...),
referencing form.getCreatedBy(), getEmail(), loggedInEmail, requester.getRole(),
Role.CEO and Role.CTO.
🧹 Nitpick comments (1)
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (1)

85-93: findAll() makes every search a full table scan in the app.

Line 86 loads the entire table and Lines 88-93 filter in memory, so even selective searches pay O(N) DB + heap cost. Push these predicates down to the database before this endpoint starts seeing real volume.

🤖 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 85 - 93, The current searchAndFilterForms in EmploymentFormService
calls employmentFormRepository.findAll and filters in memory (using
employmentMapper::toDTO after filtering), causing full table scans; change this
to push predicates into the DB by adding repository query support (either
concrete query methods or a JPA Specification/Criteria query) that accepts
optional parameters for socialSecurityNumber (use "contains"/LIKE), department
and status, then call that new repository method from searchAndFilterForms and
map results with employmentMapper.toDTO; update EmploymentFormRepository (e.g.,
add findBySocialSecurityNumberContainingAndDepartmentAndStatus or a method that
applies nullable parameters) or implement a Specification builder used by
EmploymentFormService to build DB-side predicates.
🤖 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/controller/EmploymentFormController.java`:
- Around line 81-88: Change the public API to accept a JSON body instead of
query params: update EmploymentFormController.searchForms to be a POST mapping
(replace `@GetMapping` with `@PostMapping`) and accept a request body DTO (e.g.,
EmploymentFormSearchDTO containing socialSecurityNumber, department, and status)
annotated with `@RequestBody` instead of `@RequestParam`; forward that DTO to
employmentFormService.searchAndFilterForms (or add an overload that accepts the
DTO) and update any method signatures and tests accordingly so SSNs are no
longer exposed in URL query strings.

---

Duplicate comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 211-213: The service currently returns the generated rawPassword
string in the response (see EmploymentFormService where logger.info("Form {}
approved by {}", formId, loggedInManagement) and the return that concatenates
rawPassword); remove the raw password from the return value and instead return a
neutral success message (e.g., "Employment approved; credentials delivered via
secure channel"), and ensure no logging of rawPassword anywhere in
EmploymentFormService or related controller methods (update the controller
method that consumes the service return to stop expecting/propagating the
password). Leave the password delivery responsibility to the planned
secure/email flow so credentials are never exposed in service responses, logs,
or HTTP payloads.
- Around line 147-153: The S3 upload (archiveToS3) is currently executed while
the DB transaction is still open, which can leave orphaned S3 objects if
subsequent saves or the commit fail; move the upload to run only after a
successful commit by deferring archiveToS3 until transaction completion (for
example use Spring's
TransactionSynchronizationManager.registerSynchronization/afterCommit or publish
an event and handle it with `@TransactionalEventListener`(phase = AFTER_COMMIT)),
or persist an outbox record alongside
employmentFormRepository.save/staffRepository.save and have a separate worker
send to S3; update EmploymentFormService to save entities inside the transaction
first (employmentFormRepository.save, staffRepository.save) and trigger
archiveToS3 only in the after-commit/outbox path.
- Around line 171-174: The current check lets forms with null createdBy bypass
authorization; change it to treat null createdBy as "not creator" by computing a
boolean isCreator = form.getCreatedBy() != null &&
form.getCreatedBy().getEmail().equals(loggedInEmail) (or equivalent) and then
require isCreator OR management; i.e. replace the existing compound guarded-if
with a check like if (!isCreator && requester.getRole() != Role.CEO &&
requester.getRole() != Role.CTO) throw new IllegalStateException(...),
referencing form.getCreatedBy(), getEmail(), loggedInEmail, requester.getRole(),
Role.CEO and Role.CTO.

---

Nitpick comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 85-93: The current searchAndFilterForms in EmploymentFormService
calls employmentFormRepository.findAll and filters in memory (using
employmentMapper::toDTO after filtering), causing full table scans; change this
to push predicates into the DB by adding repository query support (either
concrete query methods or a JPA Specification/Criteria query) that accepts
optional parameters for socialSecurityNumber (use "contains"/LIKE), department
and status, then call that new repository method from searchAndFilterForms and
map results with employmentMapper.toDTO; update EmploymentFormRepository (e.g.,
add findBySocialSecurityNumberContainingAndDepartmentAndStatus or a method that
applies nullable parameters) or implement a Specification builder used by
EmploymentFormService to build DB-side predicates.
🪄 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: 6f0aca08-06ae-4dad-9735-39fc6bd45b6e

📥 Commits

Reviewing files that changed from the base of the PR and between e47dd6c and 24dc713.

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

…tabind.ObjectMapper` with `tools.jackson.databind.ObjectMapper` and remove redundant `ObjectMapper` bean from `S3Config`.
…endpoint from `EmploymentFormService` and `EmploymentFormController`.
@codebyNorthsteep

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@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

♻️ Duplicate comments (2)
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (2)

198-200: ⚠️ Potential issue | 🔴 Critical

Do not return the generated password in the response body.

This still exposes a credential through the API path, where it can end up in logs, proxies, APM tooling, and client storage. Return a neutral success message and deliver the password only through the planned secure channel.

Minimal safe return value
-        return "Employment has been approved, generated password for new employee: " + rawPassword;
+        return "Employment approved. Credentials will be delivered through a secure channel.";
🤖 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 198 - 200, In EmploymentFormService (the approval flow around the
logger.info("Form {} approved by {}", formId, loggedInManagement) and return
statement), stop returning rawPassword in the HTTP response: replace the
returned string with a neutral success message (e.g., "Employment approved") and
ensure no password is logged or included in the response; instead hand off
rawPassword to the planned secure delivery mechanism (email/service) or a
separate secure method for delivering credentials, and update any callers/tests
that assert the previous response.

134-140: ⚠️ Potential issue | 🔴 Critical

Archive after commit, not inside the transaction.

archiveToS3 is an irreversible external write here. If a later DB save or the transaction commit fails, S3 still contains an archive for a rejection/approval that never became durable in the database. Move the archive step to an AFTER_COMMIT event/synchronization and persist the key separately.

Also applies to: 188-196

🤖 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 134 - 140, Remove the direct call to archiveToS3(form) from the
transactional flow in EmploymentFormService and instead perform the S3 upload
after the DB transaction successfully commits: in the method that currently
calls archiveToS3(form) and employmentFormRepository.save(form), persist any
archive metadata you need (e.g., a placeholder key or "pending" flag) via
employmentFormRepository.save(form), then register a TransactionSynchronization
(TransactionSynchronizationManager.registerSynchronization or use
`@TransactionalEventListener`(phase = AFTER_COMMIT)) that invokes
archiveToS3(form) after commit and updates the persisted entity with the final
S3 key if needed. Apply the same change for the other occurrence referenced
(around lines 188-196) so all irreversible external writes happen in
AFTER_COMMIT synchronization.
🤖 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/controller/EmploymentFormController.java`:
- Around line 81-84: The rejectForm method currently accepts the free-text
reason as a `@RequestParam` (in rejectForm), which exposes HR data in URLs/logs;
change the API to accept a small DTO in the request body (e.g., create a record
RejectEmploymentFormRequest(`@NotBlank` String reason)) and update rejectForm to
take `@RequestBody` `@Valid` RejectEmploymentFormRequest request instead of
`@RequestParam` String reason, then use request.reason() (or request.getReason())
inside the method; add the jakarta.validation.@NotBlank import and `@Valid` import
and ensure controller has validation enabled so incoming reason is validated and
no longer appears in URLs/access logs.

In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 216-223: The duplicate-checking in validateSsnNotExists currently
compares raw strings; normalize the incoming SSN first (e.g., convert
YYYYMMDD-NNNN to YYMMDD-NNNN or otherwise canonicalize/remove century and
non-digit characters to a single canonical format) and then call
employmentFormRepository.existsBySocialSecurityNumber(normalizedSsn) and
staffRepository.existsBySocialSecurityNumber(normalizedSsn); update any related
persistence/DTO handling to use the same normalize function so saved values and
future checks use the same canonical representation (add a helper
normalizeSsn(String) and call it from validateSsnNotExists).
- Around line 52-56: The current creation code in EmploymentFormService uses
employmentMapper.toEntity(form) but only sets ApprovalStatus.PENDING when the
mapped entity's status is null, allowing clients to supply APPROVED/REJECTED;
change this so that after mapping you always set
formEntity.setStatus(ApprovalStatus.PENDING) (remove the null-check) to ignore
any client-supplied status and ensure all new forms persist with PENDING status;
update any related comments and keep use of the ApprovalStatus enum and
EmploymentFormService method where the mapping occurs.
- Around line 158-161: In EmploymentFormService (the delete/check block using
form.getCreatedBy()), change the guard so a null createdBy is treated as "not
the creator": replace the current if that only runs when getCreatedBy() != null
with a condition like "if ((form.getCreatedBy() == null ||
!form.getCreatedBy().getEmail().equals(loggedInEmail)) && requester.getRole() !=
Role.CEO && requester.getRole() != Role.CTO) throw ...", so only the actual
creator or CEO/CTO can bypass the delete restriction.

---

Duplicate comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 198-200: In EmploymentFormService (the approval flow around the
logger.info("Form {} approved by {}", formId, loggedInManagement) and return
statement), stop returning rawPassword in the HTTP response: replace the
returned string with a neutral success message (e.g., "Employment approved") and
ensure no password is logged or included in the response; instead hand off
rawPassword to the planned secure delivery mechanism (email/service) or a
separate secure method for delivering credentials, and update any callers/tests
that assert the previous response.
- Around line 134-140: Remove the direct call to archiveToS3(form) from the
transactional flow in EmploymentFormService and instead perform the S3 upload
after the DB transaction successfully commits: in the method that currently
calls archiveToS3(form) and employmentFormRepository.save(form), persist any
archive metadata you need (e.g., a placeholder key or "pending" flag) via
employmentFormRepository.save(form), then register a TransactionSynchronization
(TransactionSynchronizationManager.registerSynchronization or use
`@TransactionalEventListener`(phase = AFTER_COMMIT)) that invokes
archiveToS3(form) after commit and updates the persisted entity with the final
S3 key if needed. Apply the same change for the other occurrence referenced
(around lines 188-196) so all irreversible external writes happen in
AFTER_COMMIT synchronization.
🪄 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: 5ac43642-8952-4601-9a7c-a410b2736593

📥 Commits

Reviewing files that changed from the base of the PR and between e47dd6c and 4fb8b19.

📒 Files selected for processing (3)
  • src/main/java/org/example/cyberwatch/config/S3Config.java
  • src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/org/example/cyberwatch/config/S3Config.java

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
✅ Actions performed

Reviews resumed.

… remove rejection reason requirement, and adjust role-based validation logic; update tests and controller accordingly.

@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

♻️ Duplicate comments (2)
src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java (2)

193-195: ⚠️ Potential issue | 🟠 Major

Do not return the generated password in the response.

This still exposes a credential to API consumers, logs, proxies, and monitoring. Return a neutral success message and hand the password off to the planned secure delivery path instead.

🤖 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 - 195, The method in EmploymentFormService currently returns
the generated rawPassword and logs approval details (see variables formId,
loggedInManagement, rawPassword); remove the rawPassword from the response and
logs, replace the return value with a neutral success message like "Employment
approved" (or similar), and instead invoke the secure delivery path (e.g., call
the planned email/PasswordDeliveryService or enqueue a secure job) to hand off
rawPassword for out-of-band delivery; ensure no other logging or returned
objects include rawPassword.

129-135: ⚠️ Potential issue | 🔴 Critical

Archive to S3 only after the transaction commits.

Both workflows upload to S3 before the final database save/commit. If staffRepository.save(...), employmentFormRepository.save(...), or the transaction commit fails afterward, the DB rolls back but the archive is already published. Move the upload to an AFTER_COMMIT hook/event and keep it idempotent.

Also applies to: 183-191

🤖 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 129 - 135, The S3 upload (archiveToS3) is happening before the
database commit which can lead to published archives when the transaction rolls
back; change the flow to perform the archive only after the transaction
successfully commits by moving the upload into an AFTER_COMMIT callback/event
and make the upload idempotent. Specifically, remove direct calls to archiveToS3
around employmentFormRepository.save and staffRepository.save, register a
transactional synchronization or application event listener (e.g., using
TransactionSynchronizationManager.registerSynchronization or a
`@TransactionalEventListener`(phase = AFTER_COMMIT)) that receives the formId/form
and calls archiveToS3, and ensure archiveToS3 (or its caller) is safe to retry
(idempotent) so duplicate events or retries won’t create inconsistent state.
🤖 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 124-125: The service currently only verifies the email exists
(Staff rejector = staffRepository.findByEmail(loggedInManagementEmail)...); add
an explicit role/authorization check in EmploymentFormService after resolving
the Staff (same for the other occurrence around lines 168-169) to ensure only
management roles (CEO or CTO) can approve/reject: after retrieving the Staff
object, verify staff.getRole() (or the equivalent role field) is one of the
allowed enums/strings ("CEO","CTO") and throw an AuthorizationException (or
reuse the existing access-denied exception) if not; mirror the same guard logic
used in deleteForm so both approve/reject code paths enforce CEO/CTO
authorization.
- Line 18: Replace the incorrect import of ObjectMapper in EmploymentFormService
(currently referenced as tools.jackson.databind.ObjectMapper) with the standard
Jackson package; update the import to use
com.fasterxml.jackson.databind.ObjectMapper so the EmploymentFormService class
compiles and uses the proper Jackson ObjectMapper implementation.

---

Duplicate comments:
In
`@src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java`:
- Around line 193-195: The method in EmploymentFormService currently returns the
generated rawPassword and logs approval details (see variables formId,
loggedInManagement, rawPassword); remove the rawPassword from the response and
logs, replace the return value with a neutral success message like "Employment
approved" (or similar), and instead invoke the secure delivery path (e.g., call
the planned email/PasswordDeliveryService or enqueue a secure job) to hand off
rawPassword for out-of-band delivery; ensure no other logging or returned
objects include rawPassword.
- Around line 129-135: The S3 upload (archiveToS3) is happening before the
database commit which can lead to published archives when the transaction rolls
back; change the flow to perform the archive only after the transaction
successfully commits by moving the upload into an AFTER_COMMIT callback/event
and make the upload idempotent. Specifically, remove direct calls to archiveToS3
around employmentFormRepository.save and staffRepository.save, register a
transactional synchronization or application event listener (e.g., using
TransactionSynchronizationManager.registerSynchronization or a
`@TransactionalEventListener`(phase = AFTER_COMMIT)) that receives the formId/form
and calls archiveToS3, and ensure archiveToS3 (or its caller) is safe to retry
(idempotent) so duplicate events or retries won’t create inconsistent state.
🪄 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: f022b997-c60c-465b-a59c-4b388b9d6b7d

📥 Commits

Reviewing files that changed from the base of the PR and between 4fb8b19 and 10e7fa6.

📒 Files selected for processing (3)
  • src/main/java/org/example/cyberwatch/features/form/controller/EmploymentFormController.java
  • src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java
  • src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java

# Conflicts:
#	src/main/java/org/example/cyberwatch/config/SecurityConfig.java
@codebyNorthsteep
codebyNorthsteep merged commit bea96fb into main Apr 10, 2026
1 check passed
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