Skip to content

feat: add audit logging for all user actions#108

Merged
SandraNelj merged 3 commits into
mainfrom
feature/audit-log
Apr 24, 2026
Merged

feat: add audit logging for all user actions#108
SandraNelj merged 3 commits into
mainfrom
feature/audit-log

Conversation

@SandraNelj

@SandraNelj SandraNelj commented Apr 24, 2026

Copy link
Copy Markdown
Contributor
  • Add AuditLog entity, repository, service, and REST controller
  • Add Flyway migration V11__create_audit_log_table.sql
  • Add audit logging to IncidentService (create, view, delete, assign, status changes)
  • Add audit logging to DocumentService (upload, download, delete)
  • Add audit logging to UserService (role changes, status changes, approve, delete)
  • Update AdminController to pass currentAdmin to UserService methods
  • Add admin audit log viewer page (Vanilla JS frontend)
  • Update all tests with AuditLogService mock and new method signatures

Summary by CodeRabbit

  • New Features

    • Audit logging system added for admin and user-facing actions (approvals, role/status changes, deletes, document and incident events)
    • New admin endpoints to view and search paginated audit logs, including per-user filters
  • Improvements

    • All document and incident operations now record user context and timestamps
    • Admin actions require and log the authenticated admin identity for clearer audits and stronger checks

- Add AuditLog entity, repository, service, and REST controller
- Add Flyway migration V11__create_audit_log_table.sql
- Add audit logging to IncidentService (create, view, delete, assign, status changes)
- Add audit logging to DocumentService (upload, download, delete)
- Add audit logging to UserService (role changes, status changes, approve, delete)
- Update AdminController to pass currentAdmin to UserService methods
- Add admin audit log viewer page (Vanilla JS frontend)
- Update all tests with AuditLogService mock and new method signatures
@SandraNelj SandraNelj self-assigned this Apr 24, 2026
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

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

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80ae8b5b-dc07-4117-977a-71d712ad50f4

📥 Commits

Reviewing files that changed from the base of the PR and between b5eb9a6 and 1db2390.

📒 Files selected for processing (2)
  • src/test/java/org/example/team6backend/document/service/DocumentServiceTest.java
  • src/test/java/org/example/team6backend/user/service/UserServiceTest.java
📝 Walkthrough

Walkthrough

A new audit logging subsystem is added (entity, repo, service, controller). Controllers and services are updated to accept authenticated AppUser context and emit audit events for admin actions, document operations, and incident lifecycle changes. Database migration creates the audit_log table.

Changes

Cohort / File(s) Summary
Audit core
src/main/java/org/example/team6backend/auditlog/entity/AuditLog.java, src/main/java/org/example/team6backend/auditlog/repository/AuditLogRepository.java, src/main/java/org/example/team6backend/auditlog/service/AuditLogService.java, src/main/java/org/example/team6backend/auditlog/controller/AuditLogController.java, src/main/resources/db/migration/V11__create_audit_log_table.sql
Adds AuditLog JPA entity, repository with search and user queries, service for recording logs (with safe persistence), admin-only REST endpoints for querying logs, and DB migration to create audit_log table and indexes.
Admin & user management
src/main/java/org/example/team6backend/admin/AdminController.java, src/main/java/org/example/team6backend/user/service/UserService.java
Admin controller now resolves authenticated principal centrally; UserService methods for approve/update/delete now accept an AppUser currentAdmin and log corresponding audit events.
Document handling
src/main/java/org/example/team6backend/document/controller/DocumentController.java, src/main/java/org/example/team6backend/document/service/DocumentService.java
DocumentController forwards authenticated user to service; DocumentService now depends on AuditLogService and requires AppUser for upload/download/delete, logging UPLOAD/DOWNLOAD/DELETE events and including user metadata.
Incident operations
src/main/java/org/example/team6backend/incident/service/IncidentService.java
IncidentService injected with AuditLogService; incident create/view/delete and state transitions now emit audit logs; deleteIncident signature now accepts AppUser currentUser.
Tests
src/test/java/org/example/team6backend/admin/AdminControllerTest.java, src/test/java/org/example/team6backend/document/controller/DocumentControllerTest.java, src/test/java/org/example/team6backend/document/service/DocumentServiceTest.java, src/test/java/org/example/team6backend/incident/controller/IncidentControllerTest.java, src/test/java/org/example/team6backend/incident/service/IncidentServiceTest.java, src/test/java/org/example/team6backend/user/service/UserServiceTest.java
Adds mocked AuditLogService to test contexts; updates authentication setup and mocks/verify calls to match new AppUser parameters and changed method signatures.

Sequence Diagram(s)

sequenceDiagram
    actor Admin as Admin User
    participant AC as AdminController
    participant US as UserService
    participant ALS as AuditLogService
    participant DB as Database

    Admin->>AC: PATCH /api/admin/users/{id}/role
    AC->>AC: Resolve authenticated AppUser
    AC->>US: updateUserRole(userId, newRole, admin)
    US->>DB: persist role change
    DB-->>US: OK
    US->>ALS: log("UPDATE_USER_ROLE", details, admin)
    ALS->>DB: insert audit_log
    DB-->>ALS: OK
    US-->>AC: updated user
    AC-->>Admin: 200 OK
Loading
sequenceDiagram
    actor User as Application User
    participant DC as DocumentController
    participant DS as DocumentService
    participant Minio as Minio Storage
    participant ALS as AuditLogService
    participant DB as Database

    User->>DC: POST /api/documents/upload
    DC->>DC: Resolve authenticated AppUser
    DC->>DS: uploadFile(file, incident, user)
    DS->>Minio: putObject(file)
    Minio-->>DS: OK
    DS->>DB: save Document entity
    DB-->>DS: saved
    DS->>ALS: log("UPLOAD_DOCUMENT", details, user)
    ALS->>DB: insert audit_log
    DB-->>ALS: OK
    DS-->>DC: return document
    DC-->>User: 201 Created
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

Poem

🐰 I hopped through code with nimble feet,
Each action logged, each trace complete,
Who did what, when, and why — all clear,
A tidy trail that I hold dear,
Thump-thump, audit trails bring cheer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add audit logging for all user actions' accurately describes the main objective of this changeset, which adds comprehensive audit logging across multiple services and controllers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/audit-log

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

Caution

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

⚠️ Outside diff range comments (4)
src/test/java/org/example/team6backend/admin/AdminControllerTest.java (1)

62-111: ⚠️ Potential issue | 🟡 Minor

Clear SecurityContextHolder between tests.

setUp() writes into the global SecurityContextHolder but nothing ever clears it. Since SecurityContextHolder uses a thread-local strategy, this state leaks into subsequent tests that run on the same thread (e.g., IncidentControllerTest already bothers to clear it in @AfterEach). Add the symmetric cleanup:

+    `@org.junit.jupiter.api.AfterEach`
+    void tearDown() {
+        SecurityContextHolder.clearContext();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/team6backend/admin/AdminControllerTest.java` around
lines 62 - 111, Tests set the Authentication in SecurityContextHolder within
setUp() but never clear it, causing state leakage across tests; add a symmetric
cleanup method annotated with `@AfterEach` (e.g., tearDown()) that calls
SecurityContextHolder.clearContext() to remove the Authentication set by
setUp(), referencing the existing setUp() and SecurityContextHolder to locate
where to add the cleanup.
src/main/java/org/example/team6backend/document/controller/DocumentController.java (1)

38-53: ⚠️ Potential issue | 🟡 Minor

Guard against a null userDetails.

userDetails.getUser() will NPE if the endpoint is ever reached without an authenticated CustomUserDetails principal (anonymous or OAuth2 token without a resolved CustomUserDetails). The same pattern applies to uploadFile (Line 69) and deleteFile (Line 96). Since these are now on the audit path, failing with a 500 here also means no audit event is emitted.

Consider a small guard that returns 401:

 AppUser user = userDetails.getUser();
+if (userDetails == null) {
+    throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
+}
+AppUser user = userDetails.getUser();

(or rely on method‑security annotations to prevent anonymous entry.)

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

In
`@src/main/java/org/example/team6backend/document/controller/DocumentController.java`
around lines 38 - 53, Guard against a null AuthenticationPrincipal in the
document controller methods: add an explicit null check for the method parameter
userDetails at the start of getFile (and replicate in uploadFile and deleteFile)
and throw a ResponseStatusException with HttpStatus.UNAUTHORIZED if userDetails
is null rather than calling userDetails.getUser(); reference the
CustomUserDetails type and use the existing logging pattern (log.info) to record
the unauthorized access before throwing so the request fails with 401 and still
allows auditing/handling.
src/main/java/org/example/team6backend/document/service/DocumentService.java (1)

67-94: ⚠️ Potential issue | 🟡 Minor

DOWNLOAD_DOCUMENT is audited before the stream is actually consumed.

auditLogService.log(...) fires as soon as the InputStream handle is obtained from Minio. If the controller fails while streaming bytes to the client (connection reset, serialization error, etc.), the audit row is already committed, giving false positives for completed downloads. For an audit trail this is usually tolerable, but worth calling out — if strict fidelity is needed, log on stream close/after successful transfer.

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

In `@src/main/java/org/example/team6backend/document/service/DocumentService.java`
around lines 67 - 94, The audit is currently recorded immediately in
DocumentService.downloadFile right after obtaining the InputStream from
minioService.downloadFile, producing false positives if the stream fails during
transfer; instead wrap the returned InputStream in a proxy (e.g., a
FilterInputStream/Wrapper) that overrides close() to call
auditLogService.log("DOWNLOAD_DOCUMENT", ...) only after the stream is
successfully closed (and optionally on successful EOF), and return that wrapped
stream from downloadFile; keep existing Minio Service exception handling intact
and ensure the wrapper delegates all read/close methods to the underlying stream
so behavior is unchanged except for logging on close.
src/main/java/org/example/team6backend/incident/service/IncidentService.java (1)

134-154: ⚠️ Potential issue | 🟠 Major

Auditing every VIEW_INCIDENT will dominate the audit table.

getById runs on every detail view and is also invoked from document flows (DocumentController calls incidentService.getById(...) during download/access checks per the test updates). One audit row per read will:

  • explode audit_log size quickly (far beyond mutations),
  • add a synchronous DB write to the hot read path (every view pays an extra insert),
  • drown out the security-relevant mutation events in the admin viewer.

Consider: (a) only auditing "sensitive" views (admin reading others' incidents, handlers outside their assignments), (b) rate-limiting per user/incident/day, or (c) moving view events to a separate (or async) sink.

If views must be audited at full resolution, make the insert asynchronous (@Async or event publisher) so reads don't block on audit writes, and plan table partitioning/retention for audit_log.

🧹 Nitpick comments (7)
src/main/java/org/example/team6backend/auditlog/entity/AuditLog.java (2)

19-24: Tighten non-null fields and use @Column(columnDefinition = "TEXT") for details.

action and createdAt are always set by AuditLogService.log(...) and should be @Column(nullable = false) to match a NOT NULL migration (see the migration comment). details can legitimately be long — annotate it so JPA/DDL tooling maps it to TEXT rather than VARCHAR(255) if the schema is ever regenerated.

-	private String action;
+	`@Column`(nullable = false)
+	private String action;
 	private String targetType;
 	private String targetId;
-	private String details;
-	private Instant createdAt;
+	`@Column`(columnDefinition = "TEXT")
+	private String details;
+	`@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/team6backend/auditlog/entity/AuditLog.java` around
lines 19 - 24, Add JPA column annotations on the AuditLog entity: mark the
fields action and createdAt with `@Column`(nullable = false) to reflect the NOT
NULL migration and ensure AuditLogService.log(...) invariants are enforced at
the schema level, and annotate details with `@Column`(columnDefinition = "TEXT")
so JPA/DDL tooling maps it to a TEXT column instead of a default VARCHAR(255);
update the AuditLog class fields action, createdAt, and details accordingly.

26-28: Make performedBy lazy and tighten the column mapping.

@ManyToOne defaults to FetchType.EAGER, so every audit-log read (including paginated admin list views) triggers an additional join/select for AppUser. Given that the admin audit viewer is designed to render potentially large lists, prefer LAZY and fetch join explicitly where needed. Also mirror the DB-level FK intent at the JPA layer.

-	`@ManyToOne`
-	`@JoinColumn`(name = "user_id")
+	`@ManyToOne`(fetch = FetchType.LAZY)
+	`@JoinColumn`(name = "user_id", foreignKey = `@ForeignKey`(name = "fk_audit_log_user"))
 	private AppUser performedBy;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/team6backend/auditlog/entity/AuditLog.java` around
lines 26 - 28, The performedBy association on AuditLog is currently eager;
change the `@ManyToOne` on the performedBy field in AuditLog to use FetchType.LAZY
and tighten the join column by specifying explicit column constraints (e.g. set
nullable appropriately and add a ForeignKey name) in the `@JoinColumn` so the JPA
mapping mirrors the DB FK intent; update any repository queries or service code
that need the user to use fetch joins (or EntityGraph) when loading AuditLog
with its AppUser.
src/test/java/org/example/team6backend/incident/service/IncidentServiceTest.java (1)

57-58: LGTM.

Mock and signature updates correctly track the new DocumentService.uploadFile(..., user) and IncidentService.deleteIncident(id, user) APIs. Consider adding a verify(auditLogService).log(...) in at least one happy-path test (e.g., deleteIncident) to lock the audit contract — otherwise regressions that silently stop emitting audit events will go unnoticed.

Also applies to: 109-114, 208-208

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

In
`@src/test/java/org/example/team6backend/incident/service/IncidentServiceTest.java`
around lines 57 - 58, Add a verification that an audit event is emitted in the
happy-path delete test: after calling IncidentService.deleteIncident(id, user)
in IncidentServiceTest, add a Mockito verify call to AuditLogService
(auditLogService) such as verify(auditLogService).log(...) with an
ArgumentMatcher or ArgumentCaptor to assert the expected audit entry (user,
action/type, target id). Locate the deleteIncident test method in
IncidentServiceTest and add this verify; you can mirror the same pattern in
other happy-path tests (e.g., the test exercising
DocumentService.uploadFile(..., user)) to lock the audit contract.
src/test/java/org/example/team6backend/user/service/UserServiceTest.java (1)

41-43: LGTM.

Signature updates are consistent and the explicit verify(auditLogService).log(anyString(), anyString(), eq(adminUser)) in updateUserRole_ShouldUpdateRole is a nice guardrail. Consider replicating this verification pattern in at least one of the deleteUser / approvePendingUser / updateUserActiveStatus happy-path tests so all four audited operations have at least one test that asserts the audit event is emitted.

Also applies to: 362-366

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

In `@src/test/java/org/example/team6backend/user/service/UserServiceTest.java`
around lines 41 - 43, One of the audited operations is missing an assertion that
AuditLogService.log was invoked; mirror the pattern used in
updateUserRole_ShouldUpdateRole by adding a
verify(auditLogService).log(anyString(), anyString(), eq(adminUser)) assertion
to at least one happy-path test (e.g., deleteUser_ShouldDeleteUser,
approvePendingUser_ShouldApproveUser, or
updateUserActiveStatus_ShouldUpdateStatus) so the test verifies the audit event
is emitted; locate the corresponding test method
(deleteUser/approvePendingUser/updateUserActiveStatus) and add the verify call
after exercising the service, making sure necessary Mockito matchers
(anyString(), eq(adminUser)) are available.
src/test/java/org/example/team6backend/admin/AdminControllerTest.java (1)

113-221: @WithMockUser is redundant alongside .with(authentication(auth)).

Each test now supplies an explicit UsernamePasswordAuthenticationToken via .with(authentication(auth)), which takes precedence over the @WithMockUser-provisioned context. With addFilters = false on the class, neither is actually driving authorization — the class-level annotation just confuses the intent. Drop the @WithMockUser(roles = "ADMIN") on these methods (or remove the post-processor) so the test intent is unambiguous.

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

In `@src/test/java/org/example/team6backend/admin/AdminControllerTest.java` around
lines 113 - 221, Remove the redundant `@WithMockUser`(roles = "ADMIN") annotations
from each test method that already uses .with(authentication(auth)) (e.g.,
shouldGetAllUsers, shouldGetUsersWithSearch, shouldGetPendingUsers,
shouldGetUserById, shouldApprovePendingUser, shouldUpdateUserRole,
shouldUpdateUserStatus, shouldDeleteUser) so the tests rely only on the explicit
UsernamePasswordAuthenticationToken provided via authentication(auth); delete
the `@WithMockUser` lines and any unused imports related to it to keep intent
unambiguous.
src/main/java/org/example/team6backend/auditlog/controller/AuditLogController.java (1)

12-37: Optional: route DB access through a service, not the repository.

Controllers elsewhere in this codebase delegate to services (UserService, DocumentService, IncidentService). Injecting AuditLogRepository directly into a controller bypasses that layer — a small AuditLogService.findLogs(...) / findByUser(...) would keep things consistent and give you a natural place to enforce pagination, search sanitization, and (later) cross-cutting rules like retention filters.

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

In
`@src/main/java/org/example/team6backend/auditlog/controller/AuditLogController.java`
around lines 12 - 37, Replace direct repository access in AuditLogController
with a service layer: create an AuditLogService (e.g., class AuditLogService
with methods List<AuditLog> findLogs(String search, Sort sort) and
List<AuditLog> findByUser(Long userId)) that encapsulates the calls to
AuditLogRepository.searchLogs(...) and findAll(...) / findByPerformedBy(...),
then inject AuditLogService into AuditLogController (replace the
AuditLogRepository field) and delegate getLogs(...) to
auditLogService.findLogs(search, Sort.by(...)) and getLogsByUser(...) to
auditLogService.findByUser(userId); keep repository usage only inside
AuditLogService so future pagination, sanitization and retention logic can be
added there.
src/main/java/org/example/team6backend/user/service/UserService.java (1)

119-166: Minor: no-op role/status changes still write audit rows.

Neither updateUserRole nor updateUserActiveStatus compare old vs new before saving and auditing. Posting the same role/status twice produces a misleading "changed X from RESIDENT to RESIDENT" audit entry and an unnecessary DB write. A simple early-return when oldRole == newRole / oldStatus == active avoids both.

♻️ Suggested tweak
 		AppUser user = getUserById(userId);
 		UserRole oldRole = user.getRole();
+		if (oldRole == newRole) {
+			return user;
+		}

(And analogously for updateUserActiveStatus.)

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

In `@src/main/java/org/example/team6backend/user/service/UserService.java` around
lines 119 - 166, Both updateUserRole and updateUserActiveStatus perform
unnecessary saves and audit logs when there is no actual change; modify
updateUserRole (compare oldRole to newRole) and updateUserActiveStatus (compare
oldStatus to active) after retrieving the AppUser via getUserById and, if equal,
log a debug message and return the user immediately without calling
userRepository.save or auditLogService.log; keep the existing checks for
last-admin deactivation in updateUserActiveStatus before the equality check or
adjust to only run when attempting to deactivate (active == false) and the
status would actually change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/example/team6backend/admin/AdminController.java`:
- Around line 37-46: The getCurrentUser(CustomUserDetails currentUser) method
currently throws IllegalStateException when no authenticated principal is found;
change it to throw an appropriate Spring security exception instead (e.g., throw
new AuthenticationCredentialsNotFoundException("No authenticated user found")
for unauthenticated cases or new AccessDeniedException("No principal available")
if you prefer 403 semantics) so the framework returns 401/403 instead of 500;
update the branches where Authentication auth =
SecurityContextHolder.getContext().getAuthentication() and the instanceof
CustomUserDetails check fail to throw the chosen security exception and ensure
imports reference
org.springframework.security.core.AuthenticationCredentialsNotFoundException or
org.springframework.security.access.AccessDeniedException.

In
`@src/main/java/org/example/team6backend/auditlog/controller/AuditLogController.java`:
- Around line 31-35: The endpoint getLogsByUser in AuditLogController currently
accepts a Long userId which mismatches AppUser.id (String); change the method
signature to use `@PathVariable` String userId and update the call to
auditLogRepository.findByPerformedBy(userId) to pass the String userId (matching
the corrected AuditLogRepository.findByPerformedBy signature). Ensure method
parameter type and repository invocation both use String to stay consistent with
AppUser.id.
- Around line 20-29: The getLogs endpoint in AuditLogController currently
returns unbounded lists and must be converted to paginated responses: change
AuditLogController.getLogs to accept a Pageable (or use `@PageableDefault`) and
return ResponseEntity<Page<AuditLog>>; replace calls to
auditLogRepository.findAll(Sort...) and auditLogRepository.searchLogs(search,
Sort...) with their pageable equivalents (e.g.,
auditLogRepository.findAll(pageable) and auditLogRepository.searchLogs(search,
pageable)). Update the repository method searchLogs signature to accept a
Pageable and return Page<AuditLog>, and ensure callers and imports are adjusted
accordingly; also keep sorting via Pageable to preserve createdAt DESC.

In
`@src/main/java/org/example/team6backend/auditlog/repository/AuditLogRepository.java`:
- Around line 12-15: The search is filtering on AuditLog.userName but
AuditLogService.log(...) never sets that field, so either populate userName when
creating the audit record or change the repository query to search the
relationship field; specifically, either update AuditLogService.log to call
log.setUserName(user.getName()) (or the appropriate getter) when building the
AuditLog entity (ensure AuditLog.setUserName and the user variable are used), or
modify AuditLogRepository.searchLogs' JPQL to use LOWER(a.performedBy.name) LIKE
LOWER(CONCAT('%', :s, '%')) (keeping the rest of the query and method signature
intact).
- Line 17: AuditLogRepository.findByPerformedBy currently takes a Long but
AuditLog.performedBy maps to AppUser whose id is a String; update the repository
to query by the performedBy's id (e.g. declare List<AuditLog>
findByPerformedBy_Id(String userId) or accept AppUser) and change
AuditLogController.getLogsByUser to accept a String path variable (and pass that
String into the updated repository method) so the parameter types match
AppUser.id; reference AuditLog.performedBy and AppUser.id when making the
change.

In
`@src/main/java/org/example/team6backend/auditlog/service/AuditLogService.java`:
- Around line 16-24: The log(...) method in AuditLogService is not populating
the AuditLog.entity fields userName, targetType, and targetId (only action,
details, createdAt, performedBy are set), so searches by userName and structured
queries by targetType/targetId will fail; fix by adding an overloaded method
AuditLogService.log(String action, String details, AppUser user, String
userName, String targetType, String targetId) (or optional params) and set
log.setUserName(userName), log.setTargetType(targetType), and
log.setTargetId(targetId) before calling auditLogRepository.save(log), then
update callers to pass the known target info (or call the new overload where
available) so those DB columns are populated for AuditLogRepository queries.
- Around line 11-24: AuditLogService.log currently calls auditLogRepository.save
directly (joining the caller transaction) so audit failures can roll back
business work; change to a post-commit approach so auditing observes successful
transactions only: publish a simple AuditEvent from AuditLogService.log (or
register a TransactionSynchronization via
TransactionSynchronizationManager.registerSynchronization) and move the actual
persist logic into an event listener annotated with
`@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT) (or a
TransactionSynchronization.afterCommit callback) that writes via
auditLogRepository.save; if you keep REQUIRES_NEW for AuditLogService.log, wrap
the save in try/catch and log errors to avoid bubbling exceptions and document
the trade-offs.

In
`@src/main/java/org/example/team6backend/document/service/DocumentService.java`:
- Around line 96-110: The deleteFile method currently deletes the DB row
(documentRepository.delete) inside the transaction and then calls
minioService.deleteFile before the transaction commits, risking divergence if
the DB commit fails; change the flow so the irreversible Minio deletion happens
only after a successful commit by registering a post-commit action: remove or
move the minioService.deleteFile call out of the transactional block and either
(a) use TransactionSynchronizationManager.registerSynchronization to call
minioService.deleteFile(document.getFileKey()) in afterCommit(), or (b) emit an
event and handle it with a `@TransactionalEventListener`(phase = AFTER_COMMIT)
that performs minioService.deleteFile; keep auditLogService.log inside the
transaction (or log before the post-commit deletion) and ensure exceptions from
the post-commit Minio delete are logged/warned but do not affect the
already-committed DB state.

In
`@src/main/java/org/example/team6backend/incident/service/IncidentService.java`:
- Around line 286-289: The audit log call in IncidentService currently
concatenates the AppUser object previousHandler directly, causing a
toString()/PII leak; update the UNASSIGN_INCIDENT audit message (the
auditLogService.log(...) invocation in IncidentService) to use
previousHandler.getName() (or safely resolve a display name) instead of
previousHandler, so the log contains only the handler's name and not the full
AppUser object.

In `@src/main/resources/db/migration/V11__create_audit_log_table.sql`:
- Around line 1-10: Update the audit_log table schema: add a foreign key
constraint on audit_log.user_id referencing app_user(id) with your chosen delete
behavior (e.g., ON DELETE SET NULL if you want to keep audit rows, or ON DELETE
CASCADE to remove them), make created_at and action NOT NULL, and create indexes
to support queries (at minimum an index on created_at for ordering plus indexes
on user_id, user_name, and action; consider composite indexes like (user_id,
created_at DESC), (user_name, created_at DESC), or (action, created_at DESC) to
optimize the controller’s typical filter+order queries).

---

Outside diff comments:
In
`@src/main/java/org/example/team6backend/document/controller/DocumentController.java`:
- Around line 38-53: Guard against a null AuthenticationPrincipal in the
document controller methods: add an explicit null check for the method parameter
userDetails at the start of getFile (and replicate in uploadFile and deleteFile)
and throw a ResponseStatusException with HttpStatus.UNAUTHORIZED if userDetails
is null rather than calling userDetails.getUser(); reference the
CustomUserDetails type and use the existing logging pattern (log.info) to record
the unauthorized access before throwing so the request fails with 401 and still
allows auditing/handling.

In
`@src/main/java/org/example/team6backend/document/service/DocumentService.java`:
- Around line 67-94: The audit is currently recorded immediately in
DocumentService.downloadFile right after obtaining the InputStream from
minioService.downloadFile, producing false positives if the stream fails during
transfer; instead wrap the returned InputStream in a proxy (e.g., a
FilterInputStream/Wrapper) that overrides close() to call
auditLogService.log("DOWNLOAD_DOCUMENT", ...) only after the stream is
successfully closed (and optionally on successful EOF), and return that wrapped
stream from downloadFile; keep existing Minio Service exception handling intact
and ensure the wrapper delegates all read/close methods to the underlying stream
so behavior is unchanged except for logging on close.

In `@src/test/java/org/example/team6backend/admin/AdminControllerTest.java`:
- Around line 62-111: Tests set the Authentication in SecurityContextHolder
within setUp() but never clear it, causing state leakage across tests; add a
symmetric cleanup method annotated with `@AfterEach` (e.g., tearDown()) that calls
SecurityContextHolder.clearContext() to remove the Authentication set by
setUp(), referencing the existing setUp() and SecurityContextHolder to locate
where to add the cleanup.

---

Nitpick comments:
In
`@src/main/java/org/example/team6backend/auditlog/controller/AuditLogController.java`:
- Around line 12-37: Replace direct repository access in AuditLogController with
a service layer: create an AuditLogService (e.g., class AuditLogService with
methods List<AuditLog> findLogs(String search, Sort sort) and List<AuditLog>
findByUser(Long userId)) that encapsulates the calls to
AuditLogRepository.searchLogs(...) and findAll(...) / findByPerformedBy(...),
then inject AuditLogService into AuditLogController (replace the
AuditLogRepository field) and delegate getLogs(...) to
auditLogService.findLogs(search, Sort.by(...)) and getLogsByUser(...) to
auditLogService.findByUser(userId); keep repository usage only inside
AuditLogService so future pagination, sanitization and retention logic can be
added there.

In `@src/main/java/org/example/team6backend/auditlog/entity/AuditLog.java`:
- Around line 19-24: Add JPA column annotations on the AuditLog entity: mark the
fields action and createdAt with `@Column`(nullable = false) to reflect the NOT
NULL migration and ensure AuditLogService.log(...) invariants are enforced at
the schema level, and annotate details with `@Column`(columnDefinition = "TEXT")
so JPA/DDL tooling maps it to a TEXT column instead of a default VARCHAR(255);
update the AuditLog class fields action, createdAt, and details accordingly.
- Around line 26-28: The performedBy association on AuditLog is currently eager;
change the `@ManyToOne` on the performedBy field in AuditLog to use FetchType.LAZY
and tighten the join column by specifying explicit column constraints (e.g. set
nullable appropriately and add a ForeignKey name) in the `@JoinColumn` so the JPA
mapping mirrors the DB FK intent; update any repository queries or service code
that need the user to use fetch joins (or EntityGraph) when loading AuditLog
with its AppUser.

In `@src/main/java/org/example/team6backend/user/service/UserService.java`:
- Around line 119-166: Both updateUserRole and updateUserActiveStatus perform
unnecessary saves and audit logs when there is no actual change; modify
updateUserRole (compare oldRole to newRole) and updateUserActiveStatus (compare
oldStatus to active) after retrieving the AppUser via getUserById and, if equal,
log a debug message and return the user immediately without calling
userRepository.save or auditLogService.log; keep the existing checks for
last-admin deactivation in updateUserActiveStatus before the equality check or
adjust to only run when attempting to deactivate (active == false) and the
status would actually change.

In `@src/test/java/org/example/team6backend/admin/AdminControllerTest.java`:
- Around line 113-221: Remove the redundant `@WithMockUser`(roles = "ADMIN")
annotations from each test method that already uses .with(authentication(auth))
(e.g., shouldGetAllUsers, shouldGetUsersWithSearch, shouldGetPendingUsers,
shouldGetUserById, shouldApprovePendingUser, shouldUpdateUserRole,
shouldUpdateUserStatus, shouldDeleteUser) so the tests rely only on the explicit
UsernamePasswordAuthenticationToken provided via authentication(auth); delete
the `@WithMockUser` lines and any unused imports related to it to keep intent
unambiguous.

In
`@src/test/java/org/example/team6backend/incident/service/IncidentServiceTest.java`:
- Around line 57-58: Add a verification that an audit event is emitted in the
happy-path delete test: after calling IncidentService.deleteIncident(id, user)
in IncidentServiceTest, add a Mockito verify call to AuditLogService
(auditLogService) such as verify(auditLogService).log(...) with an
ArgumentMatcher or ArgumentCaptor to assert the expected audit entry (user,
action/type, target id). Locate the deleteIncident test method in
IncidentServiceTest and add this verify; you can mirror the same pattern in
other happy-path tests (e.g., the test exercising
DocumentService.uploadFile(..., user)) to lock the audit contract.

In `@src/test/java/org/example/team6backend/user/service/UserServiceTest.java`:
- Around line 41-43: One of the audited operations is missing an assertion that
AuditLogService.log was invoked; mirror the pattern used in
updateUserRole_ShouldUpdateRole by adding a
verify(auditLogService).log(anyString(), anyString(), eq(adminUser)) assertion
to at least one happy-path test (e.g., deleteUser_ShouldDeleteUser,
approvePendingUser_ShouldApproveUser, or
updateUserActiveStatus_ShouldUpdateStatus) so the test verifies the audit event
is emitted; locate the corresponding test method
(deleteUser/approvePendingUser/updateUserActiveStatus) and add the verify call
after exercising the service, making sure necessary Mockito matchers
(anyString(), eq(adminUser)) are available.
🪄 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: 60847024-49fc-40fb-8e54-6d5af4064ad4

📥 Commits

Reviewing files that changed from the base of the PR and between 03b2e74 and 525e025.

📒 Files selected for processing (16)
  • src/main/java/org/example/team6backend/admin/AdminController.java
  • src/main/java/org/example/team6backend/auditlog/controller/AuditLogController.java
  • src/main/java/org/example/team6backend/auditlog/entity/AuditLog.java
  • src/main/java/org/example/team6backend/auditlog/repository/AuditLogRepository.java
  • src/main/java/org/example/team6backend/auditlog/service/AuditLogService.java
  • src/main/java/org/example/team6backend/document/controller/DocumentController.java
  • src/main/java/org/example/team6backend/document/service/DocumentService.java
  • src/main/java/org/example/team6backend/incident/service/IncidentService.java
  • src/main/java/org/example/team6backend/user/service/UserService.java
  • src/main/resources/db/migration/V11__create_audit_log_table.sql
  • src/test/java/org/example/team6backend/admin/AdminControllerTest.java
  • src/test/java/org/example/team6backend/document/controller/DocumentControllerTest.java
  • src/test/java/org/example/team6backend/document/service/DocumentServiceTest.java
  • src/test/java/org/example/team6backend/incident/controller/IncidentControllerTest.java
  • src/test/java/org/example/team6backend/incident/service/IncidentServiceTest.java
  • src/test/java/org/example/team6backend/user/service/UserServiceTest.java

Comment thread src/main/java/org/example/team6backend/admin/AdminController.java
Comment thread src/main/java/org/example/team6backend/auditlog/service/AuditLogService.java Outdated
Comment thread src/main/resources/db/migration/V11__create_audit_log_table.sql
- Populate userName, targetType, and targetId in AuditLogService.log()
- Fix findByPerformedBy type mismatch (Long → String for AppUser.id)
- Add pagination to audit log endpoints (Pageable instead of Sort)
- Reorder DocumentService.deleteFile() to delete MinIO after commit
- Change IllegalStateException to AccessDeniedException in AdminController
- Fix Flyway migration file naming (double underscore)
- Add missing AuditLogService mocks to all tests
@SandraNelj
SandraNelj merged commit 2eb92fc into main Apr 24, 2026
2 checks passed
@SandraNelj
SandraNelj deleted the feature/audit-log branch April 24, 2026 13:41
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