feat: add audit domain model, repository interface, AuditService, and unit test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-17 15:36:21 +01:00
parent fa3bc592d1
commit a0944a1c72
7 changed files with 167 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
package com.cameleer3.server.core.admin;
public enum AuditCategory {
INFRA, AUTH, USER_MGMT, CONFIG
}

View File

@@ -0,0 +1,24 @@
package com.cameleer3.server.core.admin;
import java.time.Instant;
import java.util.Map;
public record AuditRecord(
long id,
Instant timestamp,
String username,
String action,
AuditCategory category,
String target,
Map<String, Object> detail,
AuditResult result,
String ipAddress,
String userAgent
) {
/** Factory for creating new records (id and timestamp assigned by DB) */
public static AuditRecord create(String username, String action, AuditCategory category,
String target, Map<String, Object> detail, AuditResult result,
String ipAddress, String userAgent) {
return new AuditRecord(0, null, username, action, category, target, detail, result, ipAddress, userAgent);
}
}

View File

@@ -0,0 +1,25 @@
package com.cameleer3.server.core.admin;
import java.time.Instant;
import java.util.List;
public interface AuditRepository {
void insert(AuditRecord record);
record AuditQuery(
String username,
AuditCategory category,
String search,
Instant from,
Instant to,
String sort,
String order,
int page,
int size
) {}
record AuditPage(List<AuditRecord> items, long totalCount) {}
AuditPage find(AuditQuery query);
}

View File

@@ -0,0 +1,5 @@
package com.cameleer3.server.core.admin;
public enum AuditResult {
SUCCESS, FAILURE
}

View File

@@ -0,0 +1,49 @@
package com.cameleer3.server.core.admin;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Map;
public class AuditService {
private static final Logger log = LoggerFactory.getLogger(AuditService.class);
private final AuditRepository repository;
public AuditService(AuditRepository repository) {
this.repository = repository;
}
/** Log an action using the current SecurityContext for username */
public void log(String action, AuditCategory category, String target,
Map<String, Object> detail, AuditResult result,
HttpServletRequest request) {
String username = extractUsername();
log(username, action, category, target, detail, result, request);
}
/** Log an action with explicit username (for pre-auth contexts like login) */
public void log(String username, String action, AuditCategory category, String target,
Map<String, Object> detail, AuditResult result,
HttpServletRequest request) {
String ip = request != null ? request.getRemoteAddr() : null;
String userAgent = request != null ? request.getHeader("User-Agent") : null;
AuditRecord record = AuditRecord.create(username, action, category, target, detail, result, ip, userAgent);
repository.insert(record);
log.info("AUDIT: user={} action={} category={} target={} result={}",
username, action, category, target, result);
}
private String extractUsername() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getName() != null) {
String name = auth.getName();
return name.startsWith("user:") ? name.substring(5) : name;
}
return "unknown";
}
}