Skip to content

Refactor/restructuring(DONT MERGE)#39

Closed
mattknatt wants to merge 2 commits into
mainfrom
refactor/Restructuring
Closed

Refactor/restructuring(DONT MERGE)#39
mattknatt wants to merge 2 commits into
mainfrom
refactor/Restructuring

Conversation

@mattknatt

@mattknatt mattknatt commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Chores

    • Integrated Spring Modulith framework for enhanced modularity and architectural governance.
  • Refactor

    • Reorganized codebase into feature-based modules (audit, cases, identity) with improved separation of concerns.
    • Restructured data models to normalize accounts and user profiles, supporting more flexible role management.

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Major modularization refactor reorganizing code into domain-specific modules (audit, cases, identity), renaming entities (EmployeeEntity→AccountEntity, PatientEntity→PatientProfileEntity), introducing profile tables, adding Spring Modulith support, and implementing OAuth2 user provisioning with account auto-creation.

Changes

Cohort / File(s) Summary
Build Configuration
pom.xml
Added Spring Modulith BOM dependency and three Modulith starter dependencies for modular architecture support.
Audit Module Reorganization
src/main/java/org/example/projektarendehantering/audit/*
Relocated audit-related classes (AuditEventEntity, AuditEventRepository, AuditEventDTO, AuditEventMapper, AuditService, AuditInterceptor, AuditWebMvcConfig, AuditController, AuditUiController) from infrastructure/application/presentation into dedicated audit.domain, audit.application, and audit.infrastructure packages with updated imports.
Cases Module Reorganization
src/main/java/org/example/projektarendehantering/cases/*
Reorganized case-related classes into cases.domain, cases.application, and cases.presentation packages; refactored CaseService with Actor-based authorization; updated CaseEntity.patientCaseEntity.subject (now references PatientProfileEntity); moved DTOs and mappers to application layer.
Identity Module - Domain Layer
src/main/java/org/example/projektarendehantering/identity/domain/*
Introduced new identity domain structure: renamed EmployeeEntityAccountEntity mapped to accounts table; created EmployeeProfileEntity and PatientProfileEntity profile entities; added corresponding repositories (AccountRepository, EmployeeProfileRepository, PatientProfileRepository); PatientProfileEntity now derives primary key from AccountEntity via @MapsId.
Identity Module - Application Layer
src/main/java/org/example/projektarendehantering/identity/application/*
Moved DTOs and mappers to identity.application; refactored EmployeeMapper to map AccountEntity instead of EmployeeEntity; refactored PatientMapper to map PatientProfileEntity with createdAt sourced from account; added new EmployeeService and PatientService with manager/account provisioning logic.
Identity Module - Infrastructure
src/main/java/org/example/projektarendehantering/identity/infrastructure/*
Added CustomOAuth2UserService for OAuth2 user provisioning with auto-account creation (Role.PENDING default); updated SecurityActorAdapter to use AccountRepository and resolve roles from account entity; updated SecurityConfig to inject custom OAuth2 user service.
Identity Module - Presentation
src/main/java/org/example/projektarendehantering/identity/presentation/*
Relocated employee, patient, and employee UI controllers to identity.presentation with updated import paths.
Shared/Common Package Migration
src/main/java/org/example/projektarendehantering/shared/*, src/main/java/org/example/projektarendehantering/common/Role.java
Moved Actor, AppException, BadRequestException, NotAuthorizedException from common to shared package; added new Role enum to shared with five constants (PENDING, MANAGER, DOCTOR, NURSE, PATIENT); removed old Role from common.
Infrastructure Cleanup
src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java, src/main/java/org/example/projektarendehantering/infrastructure/persistence/PatientRepository.java
Removed obsolete EmployeeRepository and PatientRepository interfaces (replaced by domain-specific repositories in identity module).
Service Layer Removal
src/main/java/org/example/projektarendehantering/application/service/CaseService.java, src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java, src/main/java/org/example/projektarendehantering/application/service/PatientService.java
Removed old service implementations from root application.service package (functionality relocated to domain-specific modules with refactored logic).
Data Seeding & Migrations
src/main/resources/data.sql, src/test/resources/data-test.sql
Updated SQL seed scripts to match new schema: replaced patients/employees tables with accounts (storing github_username, display_name, role, created_at) and profile tables (patient_profiles, employee_profiles); adjusted case seeding to reference new IDs.
Test Updates
src/test/java/org/example/projektarendehantering/ProjektArendehanteringApplicationTests.java, src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java
Updated tests to use AuditEventRepository from audit domain; refactored SecurityActorAdapterTest to mock AccountRepository; adjusted expected role behavior (now defaults to Role.PENDING instead of Role.PATIENT when account not found).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #32: Reorganizes audit module structure following the same domain-specific modularization pattern with audit entity/service/controller relocation.
  • PR #23: Directly related entity transformations—converts PatientEntityPatientProfileEntity and updates corresponding mapper/repository/service logic.
  • PR #37: Overlapping user/role mapping refactors including githubUsername-based account lookup determinism and security adapter role derivation changes.

Poem

🐰 Modularization Meadow
The rabbit hops through newly formed halls,
Each domain now nestled in its own walls,
Accounts bloom where employees once lay,
Profiles flourish in the identity way,
OAuth's dance spins credentials anew—
Modulith magic makes structures shine true!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 is vague and generic, using non-descriptive terms like 'Refactor/restructuring' that don't convey meaningful information about the actual changeset. Replace with a more specific title that describes the main architectural changes, such as 'Reorganize code structure with modular domain packages' or 'Refactor services and entities into domain-driven modules'.
✅ 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 refactor/Restructuring

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

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/projektarendehantering/identity/infrastructure/SecurityConfig.java (1)

47-55: ⚠️ Potential issue | 🟠 Major

Remove hardcoded admin user or externalize to environment-specific configuration before production.

The in-memory admin/password user with {noop} encoding (line 51) is present unconditionally in all environments—no production profile override exists. The comment marking it as "fine for dev" is not enforced programmatically. Since OAuth2 is the primary authentication method, also disable formLogin (line 38) in production or make it conditional.

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

In
`@src/main/java/org/example/projektarendehantering/identity/infrastructure/SecurityConfig.java`
around lines 47 - 55, The code unconditionally registers a hardcoded in-memory
admin user in userDetailsService (User.builder / InMemoryUserDetailsManager)
with "{noop}password" and leaves formLogin enabled; remove or externalize this
before production by either deleting the userDetailsService bean and relying on
OAuth2, or making it conditional (use `@Profile`("dev") or a conditional bean) and
reading username/password from configuration/env vars (avoid {noop} in prod);
also wrap the formLogin() call behind the same dev-only condition or disable it
for non-dev profiles so form-based auth is not enabled in production.
src/main/java/org/example/projektarendehantering/audit/infrastructure/AuditInterceptor.java (1)

73-81: ⚠️ Potential issue | 🟠 Major

Don't store non-case IDs in audit caseId field.

The fallback from "caseId" to "id" captures employee IDs (from /api/employees/{id}) and patient IDs (from /api/patients/{id}) as caseId in the audit log, corrupting audit semantics and breaking any reporting that expects caseId to contain only actual case identifiers. Check the route pattern before applying the fallback—restrict the "id" fallback to case routes only.

Proposed fix
     private UUID extractCaseId(HttpServletRequest request) {
+        Object pattern = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
+        String routePattern = pattern != null ? pattern.toString() : "";
+        if (!routePattern.startsWith("/api/cases/") && !routePattern.startsWith("/ui/cases/")) {
+            return null;
+        }
+
         Object attr = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
         if (!(attr instanceof Map<?, ?> vars)) return null;

         Object caseId = vars.get("caseId");
         if (caseId == null) {
             caseId = vars.get("id");
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/audit/infrastructure/AuditInterceptor.java`
around lines 73 - 81, The current extractCaseId(HttpServletRequest) method falls
back from "caseId" to "id" and thus can capture non-case resources; modify
extractCaseId to only treat the "id" template variable as a caseId when the
matched route/pattern indicates a case endpoint (use
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE or request.getRequestURI() to
inspect the route), e.g., check that the pattern contains "case" or matches
"/api/cases/{id}" before using vars.get("id"), otherwise return null; keep using
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE to read vars and only apply the
"id" fallback when the route check passes.
🧹 Nitpick comments (8)
pom.xml (1)

55-59: Prefer spring-modulith-starter-test over direct spring-modulith-test.

The Spring Modulith documentation recommends using spring-modulith-starter-test for testing setup. This starter bundles spring-modulith-test, spring-modulith-docs, spring-boot-starter-test, and awaitility—the complete test toolkit—keeping your configuration aligned with Spring's idiomatic approach.

♻️ Suggested dependency adjustment
         <dependency>
             <groupId>org.springframework.modulith</groupId>
-            <artifactId>spring-modulith-test</artifactId>
+            <artifactId>spring-modulith-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` around lines 55 - 59, Replace the direct modulith test dependency
with the recommended starter: remove the dependency whose artifactId is
"spring-modulith-test" (groupId "org.springframework.modulith") and add
"spring-modulith-starter-test" with scope "test" so the project uses the bundled
test toolkit (which includes spring-modulith-test, spring-modulith-docs,
spring-boot-starter-test, and awaitility); ensure no conflicting duplicate test
dependencies remain and keep the JVM test scope as "test".
src/main/java/org/example/projektarendehantering/identity/infrastructure/CustomOAuth2UserService.java (1)

34-35: OAuth attribute names are GitHub-specific.

The attributes "login" and "name" are GitHub-specific. If this application ever supports other OAuth providers (Google, Azure AD, etc.), this will need adaptation. Consider documenting this coupling or extracting provider-specific attribute mapping.

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

In
`@src/main/java/org/example/projektarendehantering/identity/infrastructure/CustomOAuth2UserService.java`
around lines 34 - 35, The code in CustomOAuth2UserService uses GitHub-specific
attribute names ("login" and "name") via oAuth2User.getAttribute, which will
break for other OAuth providers; update the service to branch on the provider
(use OAuth2UserRequest.getClientRegistration().getRegistrationId() inside
loadUser) and map provider-specific attributes through a small mapper method or
class (e.g., mapAttributesForProvider or ProviderAttributeMapper) that returns
the canonical fields (login, fullName, email), or fall back to standard
attribute names when available; alternatively add a clear comment documenting
the GitHub coupling if you intentionally want to keep it GitHub-only.
src/main/java/org/example/projektarendehantering/identity/domain/AccountEntity.java (1)

12-23: Consider adding column constraints for data integrity.

The entity lacks explicit column definitions that could improve schema clarity and data integrity:

  • createdAt should likely be non-updatable
  • Fields like displayName and githubUsername may need nullable = false constraints
♻️ Suggested column annotations
     `@Id`
     private UUID id;

-    `@Column`(unique = true)
+    `@Column`(unique = true, nullable = false)
     private String githubUsername;

-    private String displayName;
-
+    `@Column`(nullable = false)
+    private String displayName;
+    
     `@Enumerated`(EnumType.STRING)
+    `@Column`(nullable = false)
     private Role role;

-    private Instant createdAt;
+    `@Column`(nullable = false, updatable = false)
+    private Instant createdAt;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/identity/domain/AccountEntity.java`
around lines 12 - 23, AccountEntity is missing explicit column constraints: mark
githubUsername and displayName as non-nullable by adding `@Column`(nullable =
false) to both (githubUsername can keep its unique = true), and mark createdAt
as non-updatable by adding `@Column`(updatable = false) (also consider nullable =
false for createdAt). Update the AccountEntity class annotations and, if
createdAt must be set automatically, add a `@PrePersist` method to initialize
createdAt; ensure imports for javax.persistence.Column and
javax.persistence.PrePersist are present.
src/main/java/org/example/projektarendehantering/identity/domain/PatientProfileEntity.java (1)

13-16: Remove public setId() and derive identifier solely from account relationship.

Line 35 exposes a public setId(UUID id) setter, while line 38's setAccount(AccountEntity account) does not synchronize the identifier. With @MapsId, the id is a derived identifier that should be read-only and sourced only from the associated account. This design prevents in-memory inconsistency where id and account.id could diverge. Drop the public id setter and move identifier synchronization into setAccount(). Also update the constructor at line 26 to use setAccount(...) instead of direct field assignment.

♻️ Suggested refactor
     public PatientProfileEntity(AccountEntity account, String firstName, String lastName, String personalIdentityNumber) {
-        this.account = account;
-        // Do NOT manually set this.id when using `@MapsId`
+        setAccount(account);
         this.firstName = firstName;
         this.lastName = lastName;
         this.personalIdentityNumber = personalIdentityNumber;
     }

     public UUID getId() { return id; }
-    public void setId(UUID id) { this.id = id; }

     public AccountEntity getAccount() { return account; }
-    public void setAccount(AccountEntity account) { this.account = account; }
+    public void setAccount(AccountEntity account) {
+        this.account = account;
+        this.id = account != null ? account.getId() : null;
+    }

Also applies to: 26-28, 34-38

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

In
`@src/main/java/org/example/projektarendehantering/identity/domain/PatientProfileEntity.java`
around lines 13 - 16, Remove the public setId(UUID) on PatientProfileEntity so
the derived identifier is read-only; update setAccount(AccountEntity account) to
synchronize the id by setting this.id = account.getId() (or null if account is
null) and use setAccount(...) inside the constructor instead of assigning the
account field directly; ensure no other code relies on the public setId (make it
private or delete it) so the `@MapsId` relationship remains the single source of
truth for the identifier.
src/main/java/org/example/projektarendehantering/audit/application/AuditService.java (1)

13-13: Prefer an exported read port over another module's repository.

audit.application now depends on cases.domain.CaseRepository for authorization checks. That couples audit to the cases module's persistence API and makes the new module boundary harder to enforce. An exported query interface from the cases module would keep this dependency directional.

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

In
`@src/main/java/org/example/projektarendehantering/audit/application/AuditService.java`
at line 13, AuditService currently depends directly on
cases.domain.CaseRepository which couples the audit module to the cases
persistence API; replace that concrete dependency with an exported read-only
port interface from the cases module (e.g., CasesReadPort or CaseQueryService)
and use that interface in AuditService (constructor injection of the new port)
for authorization checks; update any usages inside AuditService (methods
referencing CaseRepository) to call the port's read/query methods and remove the
direct import of CaseRepository so the audit module only depends on the cases
module's exported read port.
src/test/java/org/example/projektarendehantering/ProjektArendehanteringApplicationTests.java (1)

21-23: This assertion is effectively tautological.

count() cannot be negative, so this only distinguishes “threw” vs “didn't throw” and will still pass against an empty or mis-seeded database. Either make this an explicit no-throw smoke test or assert a known condition that this refactor should preserve.

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

In
`@src/test/java/org/example/projektarendehantering/ProjektArendehanteringApplicationTests.java`
around lines 21 - 23, The test repositoryIsAccessible() currently asserts that
auditEventRepository.count() is >= 0 which is tautological; change it to either
an explicit no-throw smoke test (e.g. use JUnit's assertDoesNotThrow or
AssertJ's assertThatCode(...).doesNotThrowAnyException() around
auditEventRepository.count()) or assert a concrete expected value that the
refactor should preserve (compare auditEventRepository.count() to a known
expectedCount). Update the assertion accordingly and keep the test name
repositoryIsAccessible() consistent.
src/main/java/org/example/projektarendehantering/audit/presentation/AuditController.java (1)

5-5: Avoid importing another module's infrastructure adapter here.

Depending on identity.infrastructure.SecurityActorAdapter makes the audit module reach into identity's implementation layer directly, which undercuts the modularization this PR is aiming for. Prefer a small application/shared interface for current-actor lookup and keep the adapter internal to the identity module.

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

In
`@src/main/java/org/example/projektarendehantering/audit/presentation/AuditController.java`
at line 5, AuditController currently imports the identity module's
SecurityActorAdapter which creates an unwanted implementation dependency;
instead define a small shared interface (e.g., CurrentActorProvider with a
method like getCurrentActor() or getCurrentActorId()) in the application/shared
API, change AuditController to depend on that interface (constructor/DI
injection) and remove the SecurityActorAdapter import, and keep
SecurityActorAdapter implementing that new interface inside the identity module
and bound in your DI configuration so the adapter remains internal to identity
while AuditController uses only the shared interface.
src/main/java/org/example/projektarendehantering/identity/domain/EmployeeProfileEntity.java (1)

27-31: Remove the public setId on this @MapsId entity.

Here id is derived from account. Keeping a public setter allows callers to put the entity into an impossible state where the primary key and the associated AccountEntity disagree. While no direct calls to setId() on profile entities were found, the constructor comment already warns against manual ID assignment, making the public setter contradictory and a source of confusion. The same issue exists in PatientProfileEntity—both should remove this method to enforce the invariant.

♻️ Proposed fix
     public UUID getId() { return id; }
-    public void setId(UUID id) { this.id = id; }

     public AccountEntity getAccount() { return account; }
     public void setAccount(AccountEntity account) { this.account = account; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/identity/domain/EmployeeProfileEntity.java`
around lines 27 - 31, Remove the public setId(UUID) method from
EmployeeProfileEntity (the `@MapsId-mapped` id derived from AccountEntity) so
callers cannot violate the primary-key/account invariant; delete the setId
method (or make it private if used internally) and ensure no external code
relies on EmployeeProfileEntity.setId, and apply the same change to
PatientProfileEntity to prevent manual ID assignment that would desynchronize
the mapped AccountEntity and the entity primary key.
🤖 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/projektarendehantering/cases/application/CaseService.java`:
- Around line 134-138: The role-check helpers (canCreate, isManager, isDoctor,
isNurse, isPatient) currently dereference actor and cause NPEs when called from
createCase, getCase, getAllCases, and getCasesForPatient for anonymous users;
update each helper to first guard against null (e.g., if actor == null) and
return false so anonymous access yields the existing NotAuthorizedException path
rather than a 500, keeping the rest of each predicate logic unchanged.
- Around line 44-51: The addNote method currently only null-checks the actor and
then saves a note, allowing any authenticated actor to append notes; update
addNote to enforce authorization by invoking requireCanRead(actor, caseEntity)
after loading caseEntity (or a stricter write-specific check method if you want
to distinguish read vs write) before creating/saving the CaseNoteEntity so only
authorized actors can add notes; preserve existing logic using
caseRepository.findById(...), then call requireCanRead(actor, caseEntity) (or
requireCanWrite(actor, caseEntity)) before mapping with caseNoteMapper and
saving with caseNoteRepository.
- Around line 59-66: CaseMapper.toEntity currently copies client-provided
workflow fields (status, createdAt) allowing tampering; modify create flow to
ensure server-controlled values by either introducing a dedicated creation
mapper (e.g., CaseMapper.toEntityForCreate/CaseCreateDTO that omits status and
createdAt) or, more simply, always overwrite those fields after mapping in
CaseService (replace the null-checks with unconditional sets): set
entity.setStatus("OPEN") and entity.setCreatedAt(Instant.now()) regardless of
incoming values, and continue to set owner via the existing
isDoctor/isManager(actor) check; reference CaseMapper.toEntity, CaseService
(create case block), entity.setStatus and entity.setCreatedAt to locate where to
apply the change.

In
`@src/main/java/org/example/projektarendehantering/cases/domain/CaseEntity.java`:
- Around line 25-27: The association change on CaseEntity.subject (the
patient_id join column) now points to PatientProfileEntity.id (which is a shared
account/profile UUID via `@MapsId`) and will break historical links; add a
Flyway/Liquibase migration that: 1) creates PatientProfileEntity rows for each
legacy patient/account missing a profile (populate required fields and link to
the existing account UUID), and 2) updates every cases.patient_id to the newly
created PatientProfileEntity.id for matching legacy patients before deploying
the entity change so existing CaseEntity.subject links remain intact. Ensure the
migration references the patient_id column and the PatientProfileEntity id
mapping (`@MapsId`) and runs prior to enabling the new association.

In
`@src/main/java/org/example/projektarendehantering/identity/application/EmployeeService.java`:
- Around line 54-65: Update EmployeeService to remove contradictory profile rows
when role changes: when dto.getRole() == Role.PATIENT, ensure you create a
PatientProfileEntity if missing and also delete any existing employee profile
for userId via employeeProfileRepository.deleteById(userId) (or
find-and-delete); when dto.getRole() != Role.PENDING (staff), create
EmployeeProfileEntity if missing and delete any existing patient profile via
patientProfileRepository.deleteById(userId); additionally, when role becomes
Role.PENDING remove both patient and employee profiles to keep state consistent;
use the existing patientProfileRepository, employeeProfileRepository,
savedAccount and userId identifiers to locate and update profiles.
- Around line 37-45: The code currently derives userId with
UUID.nameUUIDFromBytes(dto.getGithubUsername().getBytes(...)) which ties
identity to a mutable login; instead, use a stable provider subject/ID from the
DTO or OAuth response (e.g., dto.getGithubId() or dto.getOauthSubject()) as the
account key when creating/finding AccountEntity via accountRepository.findById,
and continue to store dto.getGithubUsername() only as mutable profile data on
AccountEntity; if the DTO/API does not provide a stable subject, obtain it from
the OAuth token exchange before creating the AccountEntity so renames/case
changes don’t create new accounts.

In
`@src/main/java/org/example/projektarendehantering/identity/application/PatientService.java`:
- Around line 32-47: The current read-before-write check in PatientService using
dto.getPersonalIdentityNumber() is TOCTOU and you also persist the raw value;
trim/normalize the personal identity number immediately (e.g.,
canonicalPersonalId = dto.getPersonalIdentityNumber() != null ?
dto.getPersonalIdentityNumber().trim() : null), use that canonical value for the
pre-check
(patientProfileRepository.findByPersonalIdentityNumber(canonicalPersonalId)) and
for creating PatientProfileEntity, and add a DB unique constraint/index on the
personalIdentityNumber column so the database enforces uniqueness; finally, wrap
the account/profile save block to catch the integrity exception (e.g.,
DataIntegrityViolationException or PersistenceException thrown by
accountRepository.save/profile save) and translate it to a
ResponseStatusException with HttpStatus.CONFLICT to return 409 when the DB
constraint is violated.

In
`@src/main/java/org/example/projektarendehantering/identity/domain/PatientProfileRepository.java`:
- Around line 7-8: The repository currently exposes raw PII via
PatientProfileRepository.findByPersonalIdentityNumber and the
PatientProfileEntity.personalIdentityNumber field; change the model to store
only an encryptedPersonalIdentity (ciphertext) plus a deterministic
blind-index/hmac column (e.g., personalIdentityHmac) and update the repository
to provide a lookup method like findByPersonalIdentityHmac(String
personalIdentityHmac) instead of findByPersonalIdentityNumber. Ensure callers
compute the HMAC/blind-index using a server-side secret before querying (use a
secure HMAC algorithm and key management) and persist the HMAC when
creating/updating PatientProfileEntity; remove or stop using plain
personalIdentityNumber in queries and storage.

In
`@src/main/java/org/example/projektarendehantering/identity/infrastructure/CustomOAuth2UserService.java`:
- Around line 39-68: The code silently proceeds when login is null; update
CustomOAuth2UserService to handle this explicitly by checking if (login == null)
at the start of the method and then either (a) log a warning via the class
logger (e.g., logger.warn("OAuth2 login is null for provider ...")) and throw a
runtime/authentication exception (e.g., OAuth2AuthenticationException or
IllegalStateException) to abort authentication, or (b) if you prefer rejecting
gracefully, log the warning and add a safe fallback authority like new
SimpleGrantedAuthority("ROLE_ANONYMOUS") before returning; ensure this check
occurs before UUID.nameUUIDFromBytes(login.getBytes(...)) to avoid NPE and
mention accountRepository, AccountEntity and authorities when updating behavior.
- Around line 30-69: The loadUser method in CustomOAuth2UserService performs
multiple DB operations (find, update, create) without a transaction; annotate
the method (or the CustomOAuth2UserService class) with `@Transactional` so the
account lookup/update/create in loadUser is executed atomically; ensure the
Spring transaction annotation is imported
(org.springframework.transaction.annotation.Transactional) and apply it to the
loadUser(OAuth2UserRequest) method (or the class) so that
accountRepository.findById(...), accountRepository.save(...) calls are wrapped
in a transaction.

In `@src/main/resources/data.sql`:
- Around line 2-10: The seed in data.sql inserts privileged OAuth-linked
accounts into the accounts table using real github_username values
('admin','alice','bob') which will be reapplied on every startup due to
spring.sql.init.mode=always; move the privileged rows (those with role
'MANAGER','DOCTOR','NURSE' and the github_username values 'admin','alice','bob')
out of src/main/resources/data.sql into a test-only seed file (e.g.,
src/main/resources/data-test.sql) that is only loaded by test profiles, or
replace their github_username values with non-matching placeholders so they
cannot collide with real GitHub identities; ensure the ON CONFLICT behavior
remains and that only non-privileged or placeholder rows remain in data.sql.

---

Outside diff comments:
In
`@src/main/java/org/example/projektarendehantering/audit/infrastructure/AuditInterceptor.java`:
- Around line 73-81: The current extractCaseId(HttpServletRequest) method falls
back from "caseId" to "id" and thus can capture non-case resources; modify
extractCaseId to only treat the "id" template variable as a caseId when the
matched route/pattern indicates a case endpoint (use
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE or request.getRequestURI() to
inspect the route), e.g., check that the pattern contains "case" or matches
"/api/cases/{id}" before using vars.get("id"), otherwise return null; keep using
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE to read vars and only apply the
"id" fallback when the route check passes.

In
`@src/main/java/org/example/projektarendehantering/identity/infrastructure/SecurityConfig.java`:
- Around line 47-55: The code unconditionally registers a hardcoded in-memory
admin user in userDetailsService (User.builder / InMemoryUserDetailsManager)
with "{noop}password" and leaves formLogin enabled; remove or externalize this
before production by either deleting the userDetailsService bean and relying on
OAuth2, or making it conditional (use `@Profile`("dev") or a conditional bean) and
reading username/password from configuration/env vars (avoid {noop} in prod);
also wrap the formLogin() call behind the same dev-only condition or disable it
for non-dev profiles so form-based auth is not enabled in production.

---

Nitpick comments:
In `@pom.xml`:
- Around line 55-59: Replace the direct modulith test dependency with the
recommended starter: remove the dependency whose artifactId is
"spring-modulith-test" (groupId "org.springframework.modulith") and add
"spring-modulith-starter-test" with scope "test" so the project uses the bundled
test toolkit (which includes spring-modulith-test, spring-modulith-docs,
spring-boot-starter-test, and awaitility); ensure no conflicting duplicate test
dependencies remain and keep the JVM test scope as "test".

In
`@src/main/java/org/example/projektarendehantering/audit/application/AuditService.java`:
- Line 13: AuditService currently depends directly on
cases.domain.CaseRepository which couples the audit module to the cases
persistence API; replace that concrete dependency with an exported read-only
port interface from the cases module (e.g., CasesReadPort or CaseQueryService)
and use that interface in AuditService (constructor injection of the new port)
for authorization checks; update any usages inside AuditService (methods
referencing CaseRepository) to call the port's read/query methods and remove the
direct import of CaseRepository so the audit module only depends on the cases
module's exported read port.

In
`@src/main/java/org/example/projektarendehantering/audit/presentation/AuditController.java`:
- Line 5: AuditController currently imports the identity module's
SecurityActorAdapter which creates an unwanted implementation dependency;
instead define a small shared interface (e.g., CurrentActorProvider with a
method like getCurrentActor() or getCurrentActorId()) in the application/shared
API, change AuditController to depend on that interface (constructor/DI
injection) and remove the SecurityActorAdapter import, and keep
SecurityActorAdapter implementing that new interface inside the identity module
and bound in your DI configuration so the adapter remains internal to identity
while AuditController uses only the shared interface.

In
`@src/main/java/org/example/projektarendehantering/identity/domain/AccountEntity.java`:
- Around line 12-23: AccountEntity is missing explicit column constraints: mark
githubUsername and displayName as non-nullable by adding `@Column`(nullable =
false) to both (githubUsername can keep its unique = true), and mark createdAt
as non-updatable by adding `@Column`(updatable = false) (also consider nullable =
false for createdAt). Update the AccountEntity class annotations and, if
createdAt must be set automatically, add a `@PrePersist` method to initialize
createdAt; ensure imports for javax.persistence.Column and
javax.persistence.PrePersist are present.

In
`@src/main/java/org/example/projektarendehantering/identity/domain/EmployeeProfileEntity.java`:
- Around line 27-31: Remove the public setId(UUID) method from
EmployeeProfileEntity (the `@MapsId-mapped` id derived from AccountEntity) so
callers cannot violate the primary-key/account invariant; delete the setId
method (or make it private if used internally) and ensure no external code
relies on EmployeeProfileEntity.setId, and apply the same change to
PatientProfileEntity to prevent manual ID assignment that would desynchronize
the mapped AccountEntity and the entity primary key.

In
`@src/main/java/org/example/projektarendehantering/identity/domain/PatientProfileEntity.java`:
- Around line 13-16: Remove the public setId(UUID) on PatientProfileEntity so
the derived identifier is read-only; update setAccount(AccountEntity account) to
synchronize the id by setting this.id = account.getId() (or null if account is
null) and use setAccount(...) inside the constructor instead of assigning the
account field directly; ensure no other code relies on the public setId (make it
private or delete it) so the `@MapsId` relationship remains the single source of
truth for the identifier.

In
`@src/main/java/org/example/projektarendehantering/identity/infrastructure/CustomOAuth2UserService.java`:
- Around line 34-35: The code in CustomOAuth2UserService uses GitHub-specific
attribute names ("login" and "name") via oAuth2User.getAttribute, which will
break for other OAuth providers; update the service to branch on the provider
(use OAuth2UserRequest.getClientRegistration().getRegistrationId() inside
loadUser) and map provider-specific attributes through a small mapper method or
class (e.g., mapAttributesForProvider or ProviderAttributeMapper) that returns
the canonical fields (login, fullName, email), or fall back to standard
attribute names when available; alternatively add a clear comment documenting
the GitHub coupling if you intentionally want to keep it GitHub-only.

In
`@src/test/java/org/example/projektarendehantering/ProjektArendehanteringApplicationTests.java`:
- Around line 21-23: The test repositoryIsAccessible() currently asserts that
auditEventRepository.count() is >= 0 which is tautological; change it to either
an explicit no-throw smoke test (e.g. use JUnit's assertDoesNotThrow or
AssertJ's assertThatCode(...).doesNotThrowAnyException() around
auditEventRepository.count()) or assert a concrete expected value that the
refactor should preserve (compare auditEventRepository.count() to a known
expectedCount). Update the assertion accordingly and keep the test name
repositoryIsAccessible() consistent.
🪄 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: dc292a5d-d6f3-4dad-b9bc-54bfb84bae6f

📥 Commits

Reviewing files that changed from the base of the PR and between 7416cec and 9641d1f.

📒 Files selected for processing (59)
  • pom.xml
  • src/main/java/org/example/projektarendehantering/application/service/CaseService.java
  • src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java
  • src/main/java/org/example/projektarendehantering/application/service/PatientService.java
  • src/main/java/org/example/projektarendehantering/audit/application/AuditEventDTO.java
  • src/main/java/org/example/projektarendehantering/audit/application/AuditEventMapper.java
  • src/main/java/org/example/projektarendehantering/audit/application/AuditService.java
  • src/main/java/org/example/projektarendehantering/audit/domain/AuditEventEntity.java
  • src/main/java/org/example/projektarendehantering/audit/domain/AuditEventRepository.java
  • src/main/java/org/example/projektarendehantering/audit/infrastructure/AuditInterceptor.java
  • src/main/java/org/example/projektarendehantering/audit/infrastructure/AuditWebMvcConfig.java
  • src/main/java/org/example/projektarendehantering/audit/presentation/AuditController.java
  • src/main/java/org/example/projektarendehantering/audit/presentation/AuditUiController.java
  • src/main/java/org/example/projektarendehantering/cases/application/CaseAssignmentDTO.java
  • src/main/java/org/example/projektarendehantering/cases/application/CaseDTO.java
  • src/main/java/org/example/projektarendehantering/cases/application/CaseMapper.java
  • src/main/java/org/example/projektarendehantering/cases/application/CaseNoteDTO.java
  • src/main/java/org/example/projektarendehantering/cases/application/CaseNoteMapper.java
  • src/main/java/org/example/projektarendehantering/cases/application/CaseService.java
  • src/main/java/org/example/projektarendehantering/cases/application/CreateCaseForm.java
  • src/main/java/org/example/projektarendehantering/cases/domain/CaseEntity.java
  • src/main/java/org/example/projektarendehantering/cases/domain/CaseNoteEntity.java
  • src/main/java/org/example/projektarendehantering/cases/domain/CaseNoteRepository.java
  • src/main/java/org/example/projektarendehantering/cases/domain/CaseRepository.java
  • src/main/java/org/example/projektarendehantering/cases/presentation/CaseController.java
  • src/main/java/org/example/projektarendehantering/cases/presentation/GlobalControllerAdvice.java
  • src/main/java/org/example/projektarendehantering/cases/presentation/UiController.java
  • src/main/java/org/example/projektarendehantering/common/Role.java
  • src/main/java/org/example/projektarendehantering/identity/application/EmployeeCreateDTO.java
  • src/main/java/org/example/projektarendehantering/identity/application/EmployeeDTO.java
  • src/main/java/org/example/projektarendehantering/identity/application/EmployeeMapper.java
  • src/main/java/org/example/projektarendehantering/identity/application/EmployeeService.java
  • src/main/java/org/example/projektarendehantering/identity/application/PatientCreateDTO.java
  • src/main/java/org/example/projektarendehantering/identity/application/PatientDTO.java
  • src/main/java/org/example/projektarendehantering/identity/application/PatientMapper.java
  • src/main/java/org/example/projektarendehantering/identity/application/PatientService.java
  • src/main/java/org/example/projektarendehantering/identity/domain/AccountEntity.java
  • src/main/java/org/example/projektarendehantering/identity/domain/AccountRepository.java
  • src/main/java/org/example/projektarendehantering/identity/domain/EmployeeProfileEntity.java
  • src/main/java/org/example/projektarendehantering/identity/domain/EmployeeProfileRepository.java
  • src/main/java/org/example/projektarendehantering/identity/domain/PatientProfileEntity.java
  • src/main/java/org/example/projektarendehantering/identity/domain/PatientProfileRepository.java
  • src/main/java/org/example/projektarendehantering/identity/infrastructure/CustomOAuth2UserService.java
  • src/main/java/org/example/projektarendehantering/identity/infrastructure/SecurityActorAdapter.java
  • src/main/java/org/example/projektarendehantering/identity/infrastructure/SecurityConfig.java
  • src/main/java/org/example/projektarendehantering/identity/presentation/EmployeeController.java
  • src/main/java/org/example/projektarendehantering/identity/presentation/EmployeeUiController.java
  • src/main/java/org/example/projektarendehantering/identity/presentation/PatientController.java
  • src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java
  • src/main/java/org/example/projektarendehantering/infrastructure/persistence/PatientRepository.java
  • src/main/java/org/example/projektarendehantering/shared/Actor.java
  • src/main/java/org/example/projektarendehantering/shared/AppException.java
  • src/main/java/org/example/projektarendehantering/shared/BadRequestException.java
  • src/main/java/org/example/projektarendehantering/shared/NotAuthorizedException.java
  • src/main/java/org/example/projektarendehantering/shared/Role.java
  • src/main/resources/data.sql
  • src/test/java/org/example/projektarendehantering/ProjektArendehanteringApplicationTests.java
  • src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java
  • src/test/resources/data-test.sql
💤 Files with no reviewable changes (6)
  • src/main/java/org/example/projektarendehantering/common/Role.java
  • src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java
  • src/main/java/org/example/projektarendehantering/infrastructure/persistence/PatientRepository.java
  • src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java
  • src/main/java/org/example/projektarendehantering/application/service/PatientService.java
  • src/main/java/org/example/projektarendehantering/application/service/CaseService.java

Comment on lines +44 to +51
public void addNote(UUID caseId, String content, Actor actor) {
if (actor == null) throw new NotAuthorizedException("Not allowed to add notes");
CaseEntity caseEntity = caseRepository.findById(caseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found"));

CaseNoteEntity note = caseNoteMapper.toEntity(actor, content);
note.setCaseEntity(caseEntity);
caseNoteRepository.save(note);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Adding notes currently bypasses case-level authorization.

After the null check on Line 45, any authenticated actor who knows a case id can append notes. Reuse requireCanRead(actor, caseEntity) here at minimum, or add a stricter write-specific check if patients should be read-only.

🔒 Suggested fix
     public void addNote(UUID caseId, String content, Actor actor) {
         if (actor == null) throw new NotAuthorizedException("Not allowed to add notes");
         CaseEntity caseEntity = caseRepository.findById(caseId)
                 .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found"));
+        requireCanRead(actor, caseEntity);
 
         CaseNoteEntity note = caseNoteMapper.toEntity(actor, content);
         note.setCaseEntity(caseEntity);
         caseNoteRepository.save(note);
     }
📝 Committable suggestion

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

Suggested change
public void addNote(UUID caseId, String content, Actor actor) {
if (actor == null) throw new NotAuthorizedException("Not allowed to add notes");
CaseEntity caseEntity = caseRepository.findById(caseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found"));
CaseNoteEntity note = caseNoteMapper.toEntity(actor, content);
note.setCaseEntity(caseEntity);
caseNoteRepository.save(note);
public void addNote(UUID caseId, String content, Actor actor) {
if (actor == null) throw new NotAuthorizedException("Not allowed to add notes");
CaseEntity caseEntity = caseRepository.findById(caseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found"));
requireCanRead(actor, caseEntity);
CaseNoteEntity note = caseNoteMapper.toEntity(actor, content);
note.setCaseEntity(caseEntity);
caseNoteRepository.save(note);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/cases/application/CaseService.java`
around lines 44 - 51, The addNote method currently only null-checks the actor
and then saves a note, allowing any authenticated actor to append notes; update
addNote to enforce authorization by invoking requireCanRead(actor, caseEntity)
after loading caseEntity (or a stricter write-specific check method if you want
to distinguish read vs write) before creating/saving the CaseNoteEntity so only
authorized actors can add notes; preserve existing logic using
caseRepository.findById(...), then call requireCanRead(actor, caseEntity) (or
requireCanWrite(actor, caseEntity)) before mapping with caseNoteMapper and
saving with caseNoteRepository.

Comment on lines +59 to +66
CaseEntity entity = caseMapper.toEntity(caseDTO);
PatientProfileEntity subject = patientProfileRepository.findById(caseDTO.getPatientId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Patient Profile not found"));
entity.setSubject(subject);

if (isDoctor(actor) || isManager(actor)) entity.setOwnerId(actor.userId());
if (entity.getStatus() == null) entity.setStatus("OPEN");
if (entity.getCreatedAt() == null) entity.setCreatedAt(Instant.now());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expected result: either CaseDTO does not expose workflow fields,
# or CaseMapper.toEntity(...) does not copy them from the request.
find src/main/java \( -name 'CaseDTO.java' -o -name 'CaseMapper.java' \) -print
rg -n -C3 --iglob 'CaseDTO.java' '\b(status|createdAt|ownerId|handlerId|otherId|patientId)\b' src/main/java
rg -n -C8 --iglob 'CaseMapper.java' '\btoEntity\s*\(' src/main/java
rg -n -C2 --iglob 'CaseMapper.java' '\b(setStatus|setCreatedAt|setOwnerId|setHandlerId|setOtherId)\b' src/main/java

Repository: ithsjava25/project-backend-code-trauma-team

Length of output: 8068


CaseMapper.toEntity() copies client-provided workflow fields without restriction.

The issue is confirmed. CaseDTO exposes status and createdAt fields, and CaseMapper.toEntity() (line 40 and 43) copies them directly to the entity. The null-checks in CaseService.createCase() (lines 65–66) only provide defaults if these fields are absent—clients sending status="CLOSED" or backdated timestamps will have those values persisted. Use a dedicated create DTO that excludes these fields, or explicitly overwrite them regardless of null state.

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

In
`@src/main/java/org/example/projektarendehantering/cases/application/CaseService.java`
around lines 59 - 66, CaseMapper.toEntity currently copies client-provided
workflow fields (status, createdAt) allowing tampering; modify create flow to
ensure server-controlled values by either introducing a dedicated creation
mapper (e.g., CaseMapper.toEntityForCreate/CaseCreateDTO that omits status and
createdAt) or, more simply, always overwrite those fields after mapping in
CaseService (replace the null-checks with unconditional sets): set
entity.setStatus("OPEN") and entity.setCreatedAt(Instant.now()) regardless of
incoming values, and continue to set owner via the existing
isDoctor/isManager(actor) check; reference CaseMapper.toEntity, CaseService
(create case block), entity.setStatus and entity.setCreatedAt to locate where to
apply the change.

Comment on lines +134 to +138
private boolean canCreate(Actor actor) { return isManager(actor) || isDoctor(actor); }
private boolean isManager(Actor actor) { return actor.role() == Role.MANAGER; }
private boolean isDoctor(Actor actor) { return actor.role() == Role.DOCTOR; }
private boolean isNurse(Actor actor) { return actor.role() == Role.NURSE; }
private boolean isPatient(Actor actor) { return actor.role() == Role.PATIENT; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Null actors still turn into 500s in the role helpers.

Lines 135-138 dereference actor directly. createCase, getCase, getAllCases, and getCasesForPatient all call these predicates before any null guard, so anonymous access becomes an NPE instead of NotAuthorizedException.

🛡 Suggested fix
-    private boolean canCreate(Actor actor) { return isManager(actor) || isDoctor(actor); }
-    private boolean isManager(Actor actor) { return actor.role() == Role.MANAGER; }
-    private boolean isDoctor(Actor actor) { return actor.role() == Role.DOCTOR; }
-    private boolean isNurse(Actor actor) { return actor.role() == Role.NURSE; }
-    private boolean isPatient(Actor actor) { return actor.role() == Role.PATIENT; }
+    private boolean canCreate(Actor actor) { return isManager(actor) || isDoctor(actor); }
+    private boolean isManager(Actor actor) { return actor != null && actor.role() == Role.MANAGER; }
+    private boolean isDoctor(Actor actor) { return actor != null && actor.role() == Role.DOCTOR; }
+    private boolean isNurse(Actor actor) { return actor != null && actor.role() == Role.NURSE; }
+    private boolean isPatient(Actor actor) { return actor != null && actor.role() == Role.PATIENT; }
📝 Committable suggestion

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

Suggested change
private boolean canCreate(Actor actor) { return isManager(actor) || isDoctor(actor); }
private boolean isManager(Actor actor) { return actor.role() == Role.MANAGER; }
private boolean isDoctor(Actor actor) { return actor.role() == Role.DOCTOR; }
private boolean isNurse(Actor actor) { return actor.role() == Role.NURSE; }
private boolean isPatient(Actor actor) { return actor.role() == Role.PATIENT; }
private boolean canCreate(Actor actor) { return isManager(actor) || isDoctor(actor); }
private boolean isManager(Actor actor) { return actor != null && actor.role() == Role.MANAGER; }
private boolean isDoctor(Actor actor) { return actor != null && actor.role() == Role.DOCTOR; }
private boolean isNurse(Actor actor) { return actor != null && actor.role() == Role.NURSE; }
private boolean isPatient(Actor actor) { return actor != null && actor.role() == Role.PATIENT; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/cases/application/CaseService.java`
around lines 134 - 138, The role-check helpers (canCreate, isManager, isDoctor,
isNurse, isPatient) currently dereference actor and cause NPEs when called from
createCase, getCase, getAllCases, and getCasesForPatient for anonymous users;
update each helper to first guard against null (e.g., if actor == null) and
return false so anonymous access yields the existing NotAuthorizedException path
rather than a 500, keeping the rest of each predicate logic unchanged.

Comment on lines 25 to +27
@ManyToOne(optional = true)
@JoinColumn(name = "patient_id", nullable = true) // Optional because the patient can be null
private PatientEntity patient;
@JoinColumn(name = "patient_id", nullable = true)
private PatientProfileEntity subject;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -HI --glob 'V*__*.sql'
fd -HI --glob '*changelog*'
rg -n -C2 --ignore-case \
  'patient_profiles|patient_id|alter table .*cases|update .*cases.*patient_id|foreign key.*patient_id|PatientProfileEntity|PatientEntity|@MapsId' \
  . || true

Repository: ithsjava25/project-backend-code-trauma-team

Length of output: 14674


Backfill cases.patient_id before switching this association.

patient_id now resolves against PatientProfileEntity.id, which is a shared account/profile UUID via @MapsId. Without a schema/data migration to rewrite existing cases.patient_id values, historical cases will lose their subject link after deploy. A search of the codebase found no migration files (Flyway or Liquibase) that perform this transformation.

Expected result: a schema/data migration that creates patient_profiles for all legacy patients and rewrites cases.patient_id rows to the matching account/profile IDs before this association is activated.

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

In
`@src/main/java/org/example/projektarendehantering/cases/domain/CaseEntity.java`
around lines 25 - 27, The association change on CaseEntity.subject (the
patient_id join column) now points to PatientProfileEntity.id (which is a shared
account/profile UUID via `@MapsId`) and will break historical links; add a
Flyway/Liquibase migration that: 1) creates PatientProfileEntity rows for each
legacy patient/account missing a profile (populate required fields and link to
the existing account UUID), and 2) updates every cases.patient_id to the newly
created PatientProfileEntity.id for matching legacy patients before deploying
the entity change so existing CaseEntity.subject links remain intact. Ensure the
migration references the patient_id column and the PatientProfileEntity id
mapping (`@MapsId`) and runs prior to enabling the new association.

Comment on lines +37 to +45
UUID userId = UUID.nameUUIDFromBytes(dto.getGithubUsername().getBytes(StandardCharsets.UTF_8));

// 1. Create or Update Account
AccountEntity account = accountRepository.findById(userId).orElseGet(() -> {
AccountEntity newAccount = new AccountEntity();
newAccount.setId(userId);
newAccount.setGithubUsername(dto.getGithubUsername());
newAccount.setCreatedAt(Instant.now());
return newAccount;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't derive the account id from githubUsername.

Line 37 makes identity depend on a mutable login. Renames, case-only changes, or stray whitespace will generate a different UUID and split the same person into a new account. Key the account off the stable OAuth subject/id and store the GitHub username separately as mutable profile data.

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

In
`@src/main/java/org/example/projektarendehantering/identity/application/EmployeeService.java`
around lines 37 - 45, The code currently derives userId with
UUID.nameUUIDFromBytes(dto.getGithubUsername().getBytes(...)) which ties
identity to a mutable login; instead, use a stable provider subject/ID from the
DTO or OAuth response (e.g., dto.getGithubId() or dto.getOauthSubject()) as the
account key when creating/finding AccountEntity via accountRepository.findById,
and continue to store dto.getGithubUsername() only as mutable profile data on
AccountEntity; if the DTO/API does not provide a stable subject, obtain it from
the OAuth token exchange before creating the AccountEntity so renames/case
changes don’t create new accounts.

Comment on lines +32 to +47
if (dto.getPersonalIdentityNumber() != null && !dto.getPersonalIdentityNumber().isBlank()) {
patientProfileRepository.findByPersonalIdentityNumber(dto.getPersonalIdentityNumber())
.ifPresent(existing -> {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Patient with personalIdentityNumber already exists");
});
}

UUID id = UUID.randomUUID();
String displayName = (dto.getFirstName() + " " + dto.getLastName()).trim();

// 1. Create central account (Manual Patients don't have GitHub login)
AccountEntity account = new AccountEntity(id, null, displayName, Role.PATIENT, Instant.now());
accountRepository.save(account);

// 2. Create profile
PatientProfileEntity profile = new PatientProfileEntity(account, dto.getFirstName(), dto.getLastName(), dto.getPersonalIdentityNumber());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Make personalIdentityNumber uniqueness database-backed and canonicalized.

The read-before-write check on Lines 32-36 is a TOCTOU race. Two concurrent requests can both pass it; without a unique constraint you'll store duplicates, and with one the loser currently bubbles as a 500 instead of the intended 409. Also, Line 47 persists the raw value, so whitespace variants can bypass the pre-check unless you trim/normalize once before lookup and save.

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

In
`@src/main/java/org/example/projektarendehantering/identity/application/PatientService.java`
around lines 32 - 47, The current read-before-write check in PatientService
using dto.getPersonalIdentityNumber() is TOCTOU and you also persist the raw
value; trim/normalize the personal identity number immediately (e.g.,
canonicalPersonalId = dto.getPersonalIdentityNumber() != null ?
dto.getPersonalIdentityNumber().trim() : null), use that canonical value for the
pre-check
(patientProfileRepository.findByPersonalIdentityNumber(canonicalPersonalId)) and
for creating PatientProfileEntity, and add a DB unique constraint/index on the
personalIdentityNumber column so the database enforces uniqueness; finally, wrap
the account/profile save block to catch the integrity exception (e.g.,
DataIntegrityViolationException or PersistenceException thrown by
accountRepository.save/profile save) and translate it to a
ResponseStatusException with HttpStatus.CONFLICT to return 409 when the DB
constraint is violated.

Comment on lines +7 to +8
public interface PatientProfileRepository extends JpaRepository<PatientProfileEntity, UUID> {
Optional<PatientProfileEntity> findByPersonalIdentityNumber(String personalIdentityNumber);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't expose raw personal identity numbers as a first-class lookup key.

Combined with the plain String mapping on PatientProfileEntity.personalIdentityNumber, this method means the full identifier is stored and queried as-is. For national-ID style PII, prefer an encrypted value plus a deterministic blind-index/HMAC column for uniqueness and lookup.

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

In
`@src/main/java/org/example/projektarendehantering/identity/domain/PatientProfileRepository.java`
around lines 7 - 8, The repository currently exposes raw PII via
PatientProfileRepository.findByPersonalIdentityNumber and the
PatientProfileEntity.personalIdentityNumber field; change the model to store
only an encryptedPersonalIdentity (ciphertext) plus a deterministic
blind-index/hmac column (e.g., personalIdentityHmac) and update the repository
to provide a lookup method like findByPersonalIdentityHmac(String
personalIdentityHmac) instead of findByPersonalIdentityNumber. Ensure callers
compute the HMAC/blind-index using a server-side secret before querying (use a
secure HMAC algorithm and key management) and persist the HMAC when
creating/updating PatientProfileEntity; remove or stop using plain
personalIdentityNumber in queries and storage.

Comment on lines +30 to +69
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);

String login = oAuth2User.getAttribute("login");
String fullName = oAuth2User.getAttribute("name");

Set<GrantedAuthority> authorities = new HashSet<>(oAuth2User.getAuthorities());

if (login != null) {
UUID userId = UUID.nameUUIDFromBytes(login.getBytes(StandardCharsets.UTF_8));

// Check if account already exists
var accountOpt = accountRepository.findById(userId);

if (accountOpt.isPresent()) {
AccountEntity account = accountOpt.get();
// Update display name if it changed on GitHub
if (fullName != null && !fullName.equals(account.getDisplayName())) {
account.setDisplayName(fullName);
accountRepository.save(account);
}

// Add the role from DB to authorities
if (account.getRole() != null) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + account.getRole().name()));
}
} else {
// FIRST LOGIN: Create central account with PENDING role
AccountEntity newAccount = new AccountEntity(
userId,
login,
fullName != null ? fullName : login,
Role.PENDING,
Instant.now()
);
accountRepository.save(newAccount);
authorities.add(new SimpleGrantedAuthority("ROLE_PENDING"));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add @Transactional to ensure atomic account creation/update.

The loadUser method performs multiple database operations (lookup, potential update, potential create) without a transaction boundary. If an error occurs mid-flow, this could leave the database in an inconsistent state.

🔒 Proposed fix
+import org.springframework.transaction.annotation.Transactional;
+
 `@Service`
 public class CustomOAuth2UserService extends DefaultOAuth2UserService {
     // ...
 
     `@Override`
+    `@Transactional`
     public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/identity/infrastructure/CustomOAuth2UserService.java`
around lines 30 - 69, The loadUser method in CustomOAuth2UserService performs
multiple DB operations (find, update, create) without a transaction; annotate
the method (or the CustomOAuth2UserService class) with `@Transactional` so the
account lookup/update/create in loadUser is executed atomically; ensure the
Spring transaction annotation is imported
(org.springframework.transaction.annotation.Transactional) and apply it to the
loadUser(OAuth2UserRequest) method (or the class) so that
accountRepository.findById(...), accountRepository.save(...) calls are wrapped
in a transaction.

Comment on lines +39 to +68
if (login != null) {
UUID userId = UUID.nameUUIDFromBytes(login.getBytes(StandardCharsets.UTF_8));

// Check if account already exists
var accountOpt = accountRepository.findById(userId);

if (accountOpt.isPresent()) {
AccountEntity account = accountOpt.get();
// Update display name if it changed on GitHub
if (fullName != null && !fullName.equals(account.getDisplayName())) {
account.setDisplayName(fullName);
accountRepository.save(account);
}

// Add the role from DB to authorities
if (account.getRole() != null) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + account.getRole().name()));
}
} else {
// FIRST LOGIN: Create central account with PENDING role
AccountEntity newAccount = new AccountEntity(
userId,
login,
fullName != null ? fullName : login,
Role.PENDING,
Instant.now()
);
accountRepository.save(newAccount);
authorities.add(new SimpleGrantedAuthority("ROLE_PENDING"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle the case where login is null more explicitly.

If login is null, the method silently returns a user without any role-based authorities from the database. Consider logging a warning or throwing an exception for this unexpected scenario.

🛡️ Suggested handling
         if (login != null) {
             // existing logic...
+        } else {
+            // Log warning - OAuth provider didn't return expected 'login' attribute
+            // This could indicate misconfiguration or unsupported provider
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/identity/infrastructure/CustomOAuth2UserService.java`
around lines 39 - 68, The code silently proceeds when login is null; update
CustomOAuth2UserService to handle this explicitly by checking if (login == null)
at the start of the method and then either (a) log a warning via the class
logger (e.g., logger.warn("OAuth2 login is null for provider ...")) and throw a
runtime/authentication exception (e.g., OAuth2AuthenticationException or
IllegalStateException) to abort authentication, or (b) if you prefer rejecting
gracefully, log the warning and add a safe fallback authority like new
SimpleGrantedAuthority("ROLE_ANONYMOUS") before returning; ensure this check
occurs before UUID.nameUUIDFromBytes(login.getBytes(...)) to avoid NPE and
mention accountRepository, AccountEntity and authorities when updating behavior.

Comment on lines +2 to 10
INSERT INTO accounts (id, github_username, display_name, role, created_at) VALUES
('550e8400-e29b-41d4-a716-446655440000', NULL, 'John Doe', 'PATIENT', CURRENT_TIMESTAMP),
('550e8400-e29b-41d4-a716-446655440001', NULL, 'Jane Smith', 'PATIENT', CURRENT_TIMESTAMP),
('550e8400-e29b-41d4-a716-446655440002', NULL, 'Anders Andersson', 'PATIENT', CURRENT_TIMESTAMP),
('770e8400-e29b-41d4-a716-446655440000', 'admin', 'Admin User', 'MANAGER', CURRENT_TIMESTAMP),
('770e8400-e29b-41d4-a716-446655440001', 'alice', 'Dr. Alice Roberts', 'DOCTOR', CURRENT_TIMESTAMP),
('770e8400-e29b-41d4-a716-446655440002', 'bob', 'Nurse Bob Jones', 'NURSE', CURRENT_TIMESTAMP),
('880e8400-e29b-41d4-a716-446655440000', 'newuser', 'New GitHub User', 'PENDING', CURRENT_TIMESTAMP)
ON CONFLICT (id) DO NOTHING;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
printf "== SQL init configuration ==\n"
fd -0 -a 'application.*' . | xargs -0 -r rg -n -C2 'spring\.sql\.init|spring\.profiles|defer-datasource-initialization'

printf "\n== Seed SQL files ==\n"
fd -a 'data*.sql' .

printf "\n== Privileged seeded GitHub usernames ==\n"
rg -n -C1 "github_username|'MANAGER'|'DOCTOR'|'NURSE'" src/main/resources/data.sql

Repository: ithsjava25/project-backend-code-trauma-team

Length of output: 2186


Move privileged OAuth seed accounts out of main data.sql.

github_username is now an authentication key. In src/main/resources/application.properties, spring.sql.init.mode=always causes data.sql to execute on every startup. This means real GitHub users named alice, bob, or admin will be automatically mapped to these pre-seeded DOCTOR/NURSE/MANAGER accounts in any environment using default configuration.

Keep these rows in a test-only seed file (data-test.sql) referenced only via test profiles, or use non-matching placeholder values that cannot map to external identities.

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

In `@src/main/resources/data.sql` around lines 2 - 10, The seed in data.sql
inserts privileged OAuth-linked accounts into the accounts table using real
github_username values ('admin','alice','bob') which will be reapplied on every
startup due to spring.sql.init.mode=always; move the privileged rows (those with
role 'MANAGER','DOCTOR','NURSE' and the github_username values
'admin','alice','bob') out of src/main/resources/data.sql into a test-only seed
file (e.g., src/main/resources/data-test.sql) that is only loaded by test
profiles, or replace their github_username values with non-matching placeholders
so they cannot collide with real GitHub identities; ensure the ON CONFLICT
behavior remains and that only non-privileged or placeholder rows remain in
data.sql.

@mattknatt mattknatt changed the title Refactor/restructuring Refactor/restructuring(DONT MERGE) Apr 9, 2026
@mattknatt mattknatt closed this Apr 9, 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.

1 participant