feat: enhance audit logs with geo flags, relative time, and UX improvements (#2760)

This commit is contained in:
ACX
2026-02-15 08:11:39 -07:00
committed by GitHub
parent c9551ef4ab
commit 0318d1b3bb
11 changed files with 224 additions and 11 deletions

View File

@@ -21,5 +21,6 @@ public class AuditLogDto {
private Long entityId;
private String description;
private String ipAddress;
private String countryCode;
private LocalDateTime createdAt;
}

View File

@@ -41,6 +41,9 @@ public class AuditLogEntity {
@Column(name = "ip_address", length = 45)
private String ipAddress;
@Column(name = "country_code", length = 2)
private String countryCode;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;

View File

@@ -23,6 +23,7 @@ import java.util.List;
public class AuditService {
private final AuditLogRepository auditLogRepository;
private final GeoIpService geoIpService;
public void log(AuditAction action, String description) {
log(action, null, null, description);
@@ -46,6 +47,8 @@ public class AuditService {
// Non-HTTP context (scheduled tasks, etc.)
}
String countryCode = geoIpService.resolveCountryCode(ipAddress);
AuditLogEntity entity = AuditLogEntity.builder()
.userId(userId)
.username(username)
@@ -54,6 +57,7 @@ public class AuditService {
.entityId(entityId)
.description(description)
.ipAddress(ipAddress)
.countryCode(countryCode)
.build();
auditLogRepository.save(entity);
@@ -86,6 +90,7 @@ public class AuditService {
.entityId(entity.getEntityId())
.description(entity.getDescription())
.ipAddress(entity.getIpAddress())
.countryCode(entity.getCountryCode())
.createdAt(entity.getCreatedAt())
.build();
}

View File

@@ -0,0 +1,74 @@
package org.booklore.service.audit;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.net.InetAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
@RequiredArgsConstructor
public class GeoIpService {
private static final String GEO_API_URL = "http://ip-api.com/json/%s?fields=countryCode";
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(2);
private final HttpClient httpClient;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, String> cache = new ConcurrentHashMap<>();
public String resolveCountryCode(String ip) {
if (ip == null || ip.isBlank() || isPrivateOrLocal(ip)) {
return null;
}
String result = cache.get(ip);
if (result == null) {
result = fetchCountryCode(ip);
cache.put(ip, result);
}
return result.isEmpty() ? null : result;
}
private String fetchCountryCode(String ip) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(String.format(GEO_API_URL, ip)))
.timeout(REQUEST_TIMEOUT)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
JsonNode node = objectMapper.readTree(response.body());
if (node.has("countryCode") && !node.get("countryCode").asText().isBlank()) {
return node.get("countryCode").asText();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.debug("Interrupted while resolving country code for IP: {}", ip);
} catch (Exception e) {
log.debug("Failed to resolve country code for IP: {}", ip);
}
return "";
}
private boolean isPrivateOrLocal(String ip) {
try {
InetAddress addr = InetAddress.getByName(ip);
return addr.isLoopbackAddress() || addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
} catch (Exception e) {
return true;
}
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS country_code CHAR(2) NULL;