1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 08:29:36 +01:00
effigenix/backend/src/main/java/de/effigenix/infrastructure/audit/DatabaseAuditLogger.java
Sebastian Frick b46495e1aa refactor: OffsetDateTime-Migration, atomare Batch-Sequenznummern und Quantity.reconstitute-Overload
- LocalDateTime → OffsetDateTime (UTC) in allen Domain-Klassen, JPA Entities, DTOs und Tests
- Liquibase-Migration 017: TIMESTAMP → TIMESTAMP WITH TIME ZONE für bestehende Spalten
- Custom DateTimeProvider für Spring Data @CreatedDate-Kompatibilität mit OffsetDateTime
- Neue Sequenztabelle (016) mit JPA Entity + PESSIMISTIC_WRITE Lock statt COUNT-basierter
  Batch-Nummernvergabe (Race Condition Fix)
- Quantity.reconstitute(amount, uom) 2-Parameter-Overload für bessere Lesbarkeit
2026-02-20 00:41:11 +01:00

181 lines
6.7 KiB
Java

package de.effigenix.infrastructure.audit;
import de.effigenix.application.usermanagement.AuditEvent;
import de.effigenix.application.usermanagement.AuditLogger;
import de.effigenix.shared.security.ActorId;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.UUID;
/**
* Database-backed implementation of AuditLogger.
*
* HACCP/GoBD Compliance:
* - Logs are written to database for durability
* - Logs are immutable after creation
* - Includes IP address and user agent for forensics
* - Retention: 10 years (gesetzlich)
*
* Architecture:
* - Infrastructure Layer implementation
* - Adapts Application Layer's AuditLogger port
* - Uses REQUIRES_NEW transaction to ensure audit logs are committed even if business transaction fails
*/
@Service
@Profile("!no-db")
public class DatabaseAuditLogger implements AuditLogger {
private static final Logger log = LoggerFactory.getLogger(DatabaseAuditLogger.class);
private final AuditLogJpaRepository repository;
public DatabaseAuditLogger(AuditLogJpaRepository repository) {
this.repository = repository;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(AuditEvent event, String entityId, ActorId performedBy) {
try {
AuditLogEntity auditLog = new AuditLogEntity(
UUID.randomUUID().toString(),
event,
entityId,
performedBy.value(),
null, // no additional details
OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(),
getUserAgent()
);
repository.save(auditLog);
log.debug("Audit log created: event={}, entityId={}, actor={}", event, entityId, performedBy.value());
} catch (Exception e) {
// Never fail business logic due to audit logging errors
log.error("Failed to create audit log: event={}, entityId={}, actor={}", event, entityId, performedBy.value(), e);
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(AuditEvent event, String details) {
try {
AuditLogEntity auditLog = new AuditLogEntity(
UUID.randomUUID().toString(),
event,
null, // no entity ID
null, // no actor (e.g., system event)
details,
OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(),
getUserAgent()
);
repository.save(auditLog);
log.debug("Audit log created: event={}, details={}", event, details);
} catch (Exception e) {
log.error("Failed to create audit log: event={}, details={}", event, details, e);
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(AuditEvent event, String entityId, String details, ActorId performedBy) {
try {
AuditLogEntity auditLog = new AuditLogEntity(
UUID.randomUUID().toString(),
event,
entityId,
performedBy.value(),
details,
OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(),
getUserAgent()
);
repository.save(auditLog);
log.debug("Audit log created: event={}, entityId={}, details={}, actor={}", event, entityId, details, performedBy.value());
} catch (Exception e) {
log.error("Failed to create audit log: event={}, entityId={}, details={}, actor={}", event, entityId, details, performedBy.value(), e);
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(AuditEvent event, ActorId performedBy) {
try {
AuditLogEntity auditLog = new AuditLogEntity(
UUID.randomUUID().toString(),
event,
null, // no entity ID
performedBy.value(),
null, // no additional details
OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(),
getUserAgent()
);
repository.save(auditLog);
log.debug("Audit log created: event={}, actor={}", event, performedBy.value());
} catch (Exception e) {
log.error("Failed to create audit log: event={}, actor={}", event, performedBy.value(), e);
}
}
/**
* Extracts client IP address from HTTP request.
* Handles X-Forwarded-For header for proxy/load balancer scenarios.
*/
private String getClientIpAddress() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return "unknown";
}
HttpServletRequest request = attributes.getRequest();
// Check X-Forwarded-For header (proxy/load balancer)
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
// X-Forwarded-For: client, proxy1, proxy2
return xForwardedFor.split(",")[0].trim();
}
// Fallback to remote address
return request.getRemoteAddr();
} catch (Exception e) {
log.warn("Failed to extract client IP address", e);
return "unknown";
}
}
/**
* Extracts User-Agent header from HTTP request.
*/
private String getUserAgent() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return "unknown";
}
HttpServletRequest request = attributes.getRequest();
String userAgent = request.getHeader("User-Agent");
return userAgent != null ? userAgent : "unknown";
} catch (Exception e) {
log.warn("Failed to extract User-Agent", e);
return "unknown";
}
}
}