mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 08:29:36 +01:00
- 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
181 lines
6.7 KiB
Java
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";
|
|
}
|
|
}
|
|
}
|