mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
feat: enhance audit logs with geo flags, relative time, and UX improvements (#2760)
This commit is contained in:
@@ -21,5 +21,6 @@ public class AuditLogDto {
|
||||
private Long entityId;
|
||||
private String description;
|
||||
private String ipAddress;
|
||||
private String countryCode;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS country_code CHAR(2) NULL;
|
||||
Reference in New Issue
Block a user