Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1c93b63
Added audit logging functionality with `AuditAspect`, `AuditLog`, `Au…
JohanHiths Apr 23, 2026
7f6a559
Enhanced audit logging system: added `AuditAction` annotation for met…
JohanHiths Apr 24, 2026
e51fc7f
Improved user signup auditing: added `AuditAction` annotation to `Sig…
JohanHiths Apr 24, 2026
c258810
update
JohanHiths Apr 24, 2026
523b8d1
Expanded audit logging system: added HTTP method tracking in `AuditLo…
JohanHiths Apr 24, 2026
df24570
Added detailed auditing for file actions: integrated `AuditAction` an…
JohanHiths Apr 24, 2026
03db623
update
JohanHiths Apr 25, 2026
df9f9fc
Refactored audit service usage in tests: added `AuditService` and `Au…
JohanHiths Apr 25, 2026
acdaaaa
removed responsebody
JohanHiths Apr 25, 2026
e4e7929
Enhanced audit logging: added `details` field to `AuditLog`, updated …
JohanHiths Apr 27, 2026
b668503
Refactored audit logging and admin functionality: adjusted entity ID …
JohanHiths Apr 27, 2026
3963deb
Merge branch 'main' into feature/auditlogs
JohanHiths Apr 27, 2026
33314c8
test
JohanHiths Apr 27, 2026
8428460
Merge remote-tracking branch 'origin/feature/auditlogs' into feature/…
JohanHiths Apr 27, 2026
5eedb44
Merge branch 'main' into feature/auditlogs
JohanHiths Apr 27, 2026
a984519
update
JohanHiths Apr 27, 2026
c7cb136
Merge branch 'feature/auditlogs' of https://github.com/ithsjava25/pro…
JohanHiths Apr 27, 2026
80bcd77
update
JohanHiths Apr 27, 2026
e5fc895
Added case management to admin: introduced `CASE_OFFICER` role, imple…
JohanHiths Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/backendlab/team4you/Team4youApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.webauthn.api.Bytes;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/backendlab/team4you/audit/AspectConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package backendlab.team4you.audit;


import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AspectConfig {



}

13 changes: 13 additions & 0 deletions src/main/java/backendlab/team4you/audit/AuditAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package backendlab.team4you.audit;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditAction {
String action();
String entity();
}
90 changes: 90 additions & 0 deletions src/main/java/backendlab/team4you/audit/AuditAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package backendlab.team4you.audit;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Component
public class AuditAspect {

private static final Logger log = LoggerFactory.getLogger(AuditAspect.class);

private final AuditService auditService;

public AuditAspect(AuditService auditService) {
this.auditService = auditService;
}

@Pointcut("within(backendlab.team4you..*)")
public void controllerMethods() {}

@AfterReturning(pointcut = "@annotation(auditAction)", returning = "result")
public void logAuditSuccess(JoinPoint joinPoint, AuditAction auditAction, Object result) {
record(joinPoint, auditAction, "SUCCESS");
}

@AfterThrowing(pointcut = "@annotation(auditAction)", throwing = "ex")
public void logAuditFailure(JoinPoint joinPoint, AuditAction auditAction, Throwable ex) {
record(joinPoint, auditAction, "FAILURE");
}

private void record(JoinPoint joinPoint, AuditAction auditAction, String status) {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = (auth != null) ? auth.getName() : "anonymous";

ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ip = "unknown";
String endpoint = "unknown";
String httpMethod = "UNKNOWN";

if (attrs != null) {
ip = attrs.getRequest().getRemoteAddr();
endpoint = attrs.getRequest().getRequestURI();
httpMethod = attrs.getRequest().getMethod();
}


String methodName = joinPoint.getSignature().toShortString();
String details = "Executed method: " + methodName;
Long entityId = null;

Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof Long) {
entityId = (Long) arg;
} else if (arg instanceof String && !((String) arg).contains("/")) {
details = "File/Key: " + arg;
}
}

auditService.saveLog(
username,
null,
auditAction.action(),
endpoint,
httpMethod,
ip,
status,
details,
auditAction.entity(),
Math.toIntExact(entityId)

);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

} catch (Exception e) {
log.warn("Failed to persist audit log for {}: {}",
joinPoint.getSignature().toShortString(), e.getMessage(), e);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
117 changes: 117 additions & 0 deletions src/main/java/backendlab/team4you/audit/AuditLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package backendlab.team4you.audit;


import jakarta.persistence.*;

import java.time.ZonedDateTime;

@Entity
@Table(name = "audit")
public class AuditLog {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;


private String username;
private String email;

String details;

private String action;

private String httpMethod;

private String endpoint;

private String entityType;

private Long entityId;

@Column(name = "ip_address")
private String ipAddress;


private ZonedDateTime timestamp;

private String status;


public AuditLog() {
}

public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
public Long getEntityId() {
return entityId;
}
public void setEntityId(Long entityId) {
this.entityId = entityId;
}

public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public ZonedDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(ZonedDateTime timestamp) {
this.timestamp = timestamp;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public String getHttpMethod() {
return httpMethod;
}
public void setHttpMethod(String httpMethod) {
this.httpMethod = httpMethod;
}
}
13 changes: 13 additions & 0 deletions src/main/java/backendlab/team4you/audit/AuditLogRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package backendlab.team4you.audit;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {


List<AuditLog> findAllByOrderByTimestampDesc();
}
79 changes: 79 additions & 0 deletions src/main/java/backendlab/team4you/audit/AuditService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package backendlab.team4you.audit;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.time.ZonedDateTime;

@Service
public class AuditService {

private static final Logger log = LoggerFactory.getLogger(AuditService.class);

AuditLogRepository auditLogRepository;
public AuditService(AuditLogRepository auditRepository) {
this.auditLogRepository = auditRepository;
}

public void saveLog( String username,
String email,
String action,
String endpoint,
String httpMethod,
String ipAddress,
String status,
String details,
String entityType,
int entityId) {

AuditLog log = new AuditLog();

log.setUsername(username);
log.setEmail(email);
log.setAction(action);
log.setEndpoint(endpoint);
log.setIpAddress(ipAddress);
log.setTimestamp(ZonedDateTime.now());
log.setDetails(details);
log.setStatus(status);
log.setHttpMethod(httpMethod);
log.setEntityType(entityType);
log.setEntityId((long) entityId);

auditLogRepository.save(log);


}
Comment thread
coderabbitai[bot] marked this conversation as resolved.


public void log(String username,
String action,
String entityType,
Long entityId,
String details,
String status) {

{
try {
AuditLog auditLog = new AuditLog();
auditLog.setUsername(username);
auditLog.setAction(action);
auditLog.setEntityType(entityType);
auditLog.setEntityId((long) Math.toIntExact(entityId));
auditLog.setDetails(details);
auditLog.setStatus(status);
auditLog.setTimestamp(ZonedDateTime.now());

auditLogRepository.save(auditLog);

log.info("Audit log saved: action={}, entity={}:{}", action, entityType, entityId);

} catch (Exception e) {
System.out.println("Failed to save audit log " + e.getMessage());
}
}

}
Comment on lines +51 to +78

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace System.out.println with SLF4J and don't swallow failures silently.

A few concerns in log(...):

  1. Lines 65 and 68 use System.out.println. That bypasses the app's log configuration, formatting, and log levels. Use SLF4J at INFO/ERROR and pass the exception so the stack trace is captured.
  2. The catch (Exception e) block only prints a message — callers (e.g., UserService.updateRole) can't distinguish a persisted audit from a silently-lost one. For a compliance/audit subsystem this is risky; consider either letting the caller decide (propagate) or at least logging at ERROR with the exception.
  3. Line 58 Math.toIntExact(entityId) will throw ArithmeticException for any Long > Integer.MAX_VALUE because AuditLog.entityId is declared int. Fixing AuditLog.entityId to Long (flagged separately) removes this hazard at the root.
  4. Line 51 opens an extra { ... } block inside the method body that serves no purpose — drop it.
🛡️ Proposed fix
-import org.springframework.stereotype.Service;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
...
 public class AuditService {
+    private static final Logger log = LoggerFactory.getLogger(AuditService.class);
...
     public void log(String username,
                     String action,
                     String entityType,
                     Long entityId,
                     String details,
                     String status) {
-
-        {
-            try {
-                AuditLog auditLog = new AuditLog();
-
-                auditLog.setUsername(username);
-                auditLog.setAction(action);
-                auditLog.setEntityType(entityType);
-                auditLog.setEntityId(Math.toIntExact(entityId));
-                auditLog.setDetails(details);
-                auditLog.setStatus(status);
-                auditLog.setTimestamp(ZonedDateTime.now());
-
-                auditLogRepository.save(auditLog);
-
-                System.out.println(" Audit log saved " + action);
-
-            } catch (Exception e) {
-                System.out.println("Failed to save audit log " + e.getMessage());
-            }
-        }
-
+        try {
+            AuditLog auditLog = new AuditLog();
+            auditLog.setUsername(username);
+            auditLog.setAction(action);
+            auditLog.setEntityType(entityType);
+            auditLog.setEntityId(entityId); // change field type to Long, see AuditLog.java
+            auditLog.setDetails(details);
+            auditLog.setStatus(status);
+            auditLog.setTimestamp(ZonedDateTime.now());
+            auditLogRepository.save(auditLog);
+            log.info("Audit log saved: action={}, entity={}:{}", action, entityType, entityId);
+        } catch (Exception e) {
+            log.error("Failed to save audit log for action={}, entity={}:{}", action, entityType, entityId, e);
+        }
     }
📝 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 log(String username,
String action,
String entityType,
Long entityId,
String details,
String status) {
{
try {
AuditLog auditLog = new AuditLog();
auditLog.setUsername(username);
auditLog.setAction(action);
auditLog.setEntityType(entityType);
auditLog.setEntityId(Math.toIntExact(entityId));
auditLog.setDetails(details);
auditLog.setStatus(status);
auditLog.setTimestamp(ZonedDateTime.now());
auditLogRepository.save(auditLog);
System.out.println(" Audit log saved " + action);
} catch (Exception e) {
System.out.println("Failed to save audit log " + e.getMessage());
}
}
}
public void log(String username,
String action,
String entityType,
Long entityId,
String details,
String status) {
try {
AuditLog auditLog = new AuditLog();
auditLog.setUsername(username);
auditLog.setAction(action);
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId);
auditLog.setDetails(details);
auditLog.setStatus(status);
auditLog.setTimestamp(ZonedDateTime.now());
auditLogRepository.save(auditLog);
log.info("Audit log saved: action={}, entity={}:{}", action, entityType, entityId);
} catch (Exception e) {
log.error("Failed to save audit log for action={}, entity={}:{}", action, entityType, entityId, e);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/audit/AuditService.java` around lines 44 -
72, In the log(...) method remove the extraneous inner block, replace the
System.out.println calls with an SLF4J Logger (use logger.info(...) when saving
and logger.error(..., e) in the catch to include the stack trace), and stop
swallowing failures: either rethrow the exception or wrap it in a runtime
exception and throw so callers can react (e.g., throw new
RuntimeException("Failed to save audit log", e)); also avoid
Math.toIntExact(entityId) — set AuditLog.entityId using a Long-compatible setter
(AuditLog.setEntityId) or convert only after ensuring it fits; refer to the
method log, class AuditLog, and auditLogRepository to locate these changes.

}
8 changes: 8 additions & 0 deletions src/main/java/backendlab/team4you/audit/AuditStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backendlab.team4you.audit;

public enum AuditStatus {
SUCCESS,
FAILURE,
PENDING

}
Comment on lines +3 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm AuditStatus is unused anywhere
rg -nP '\bAuditStatus\b' --type=java

Repository: ithsjava25/project-backend-team4you

Length of output: 164


🏁 Script executed:

# First, let's see the full AuditStatus.java file to check the blank line issue
cat -n src/main/java/backendlab/team4you/audit/AuditStatus.java

Repository: ithsjava25/project-backend-team4you

Length of output: 238


🏁 Script executed:

# Find and inspect AuditLog class to see how status field is typed
find . -name "AuditLog.java" -type f

Repository: ithsjava25/project-backend-team4you

Length of output: 135


🏁 Script executed:

# Find and inspect AuditService class to see method signatures
find . -name "AuditService.java" -type f

Repository: ithsjava25/project-backend-team4you

Length of output: 139


🏁 Script executed:

# Read AuditLog.java
cat -n src/main/java/backendlab/team4you/audit/AuditLog.java

Repository: ithsjava25/project-backend-team4you

Length of output: 3078


🏁 Script executed:

# Read AuditService.java
cat -n src/main/java/backendlab/team4you/audit/AuditService.java

Repository: ithsjava25/project-backend-team4you

Length of output: 2688


Enum is defined but appears unused — type AuditLog.status as AuditStatus to get real benefit.

Based on AuditService.saveLog(...) and AuditService.log(...), status is still being handled as a raw String (line 22 and 49 respectively). That defeats the purpose of this enum; callers can pass arbitrary strings and typos won't be caught at compile time. Consider changing AuditLog.status to AuditStatus with @Enumerated(EnumType.STRING) (line 37) and updating the service signatures accordingly.

Remove the blank line at line 7 inside the enum body.

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

In `@src/main/java/backendlab/team4you/audit/AuditStatus.java` around lines 3 - 8,
AuditStatus enum is unused and has an extra blank line; change AuditLog.status
from String to the AuditStatus enum and annotate it with
`@Enumerated`(EnumType.STRING), then update AuditService.saveLog(...) and
AuditService.log(...) signatures and implementations to accept/handle
AuditStatus instead of raw String so callers are type-safe; also remove the
blank line inside the AuditStatus enum body.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package backendlab.team4you.casefile;

import backendlab.team4you.audit.AuditAction;
import backendlab.team4you.common.ConfidentialityLevel;
import backendlab.team4you.user.UserEntity;
import backendlab.team4you.user.UserService;
Expand Down Expand Up @@ -30,6 +31,7 @@ public CaseFileController(CaseFileService caseFileService, UserService userServi
}

@PostMapping
@AuditAction(action = "FILE_UPLOAD", entity = "CASE_FILE")
public ResponseEntity<CaseFileResponseDto> uploadFile(
@PathVariable Long caseRecordId,
@RequestParam("file") MultipartFile file,
Expand All @@ -42,6 +44,7 @@ public ResponseEntity<CaseFileResponseDto> uploadFile(
}

@GetMapping
@AuditAction(action = "FILE_DOWNLOAD", entity = "CASE_FILE")
public ResponseEntity<List<CaseFileListItemDto>> listFiles(
@PathVariable Long caseRecordId,
Principal principal
Expand All @@ -55,6 +58,7 @@ public ResponseEntity<List<CaseFileListItemDto>> listFiles(
}

@GetMapping("/{fileId}")
@AuditAction(action = "FILE_DOWNLOAD", entity = "CASE_FILE")
public ResponseEntity<StreamingResponseBody> downloadFile(
@PathVariable Long caseRecordId,
@PathVariable Long fileId,
Expand Down Expand Up @@ -87,6 +91,7 @@ public ResponseEntity<StreamingResponseBody> downloadFile(
}

@DeleteMapping("/{fileId}")
@AuditAction(action = "FILE_DELETE", entity = "CASE_FILE")
public ResponseEntity<Void> deleteFile(
@PathVariable Long caseRecordId,
@PathVariable Long fileId,
Expand Down
Loading
Loading