feat: add audit log viewing for vendor and tenant personas
Vendor sees all audit events with tenant filter at /vendor/audit. Tenant admin sees only their own events at /tenant/audit. Both support pagination, action/result filters, and text search. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
38
src/main/java/net/siegeln/cameleer/saas/audit/AuditDto.java
Normal file
38
src/main/java/net/siegeln/cameleer/saas/audit/AuditDto.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package net.siegeln.cameleer.saas.audit;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AuditDto {
|
||||
|
||||
private AuditDto() {}
|
||||
|
||||
public record AuditLogEntry(
|
||||
UUID id,
|
||||
String actorEmail,
|
||||
UUID tenantId,
|
||||
String action,
|
||||
String resource,
|
||||
String environment,
|
||||
String result,
|
||||
String sourceIp,
|
||||
Instant createdAt
|
||||
) {
|
||||
public static AuditLogEntry from(AuditEntity e) {
|
||||
return new AuditLogEntry(
|
||||
e.getId(), e.getActorEmail(), e.getTenantId(),
|
||||
e.getAction(), e.getResource(), e.getEnvironment(),
|
||||
e.getResult(), e.getSourceIp(), e.getCreatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record AuditLogPage(
|
||||
List<AuditLogEntry> content,
|
||||
int page,
|
||||
int size,
|
||||
long totalElements,
|
||||
int totalPages
|
||||
) {}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package net.siegeln.cameleer.saas.audit;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -13,4 +17,26 @@ public interface AuditRepository extends JpaRepository<AuditEntity, UUID> {
|
||||
List<AuditEntity> findByTenantIdAndCreatedAtBetween(UUID tenantId, Instant from, Instant to);
|
||||
|
||||
List<AuditEntity> findByActorId(UUID actorId);
|
||||
|
||||
@Query("""
|
||||
SELECT a FROM AuditEntity a
|
||||
WHERE (:tenantId IS NULL OR a.tenantId = :tenantId)
|
||||
AND (:action IS NULL OR a.action = :action)
|
||||
AND (:result IS NULL OR a.result = :result)
|
||||
AND (:from IS NULL OR a.createdAt >= :from)
|
||||
AND (:to IS NULL OR a.createdAt <= :to)
|
||||
AND (:search IS NULL
|
||||
OR LOWER(a.actorEmail) LIKE LOWER(CONCAT('%', :search, '%'))
|
||||
OR LOWER(a.resource) LIKE LOWER(CONCAT('%', :search, '%')))
|
||||
ORDER BY a.createdAt DESC
|
||||
""")
|
||||
Page<AuditEntity> findFiltered(
|
||||
@Param("tenantId") UUID tenantId,
|
||||
@Param("action") String action,
|
||||
@Param("result") String result,
|
||||
@Param("from") Instant from,
|
||||
@Param("to") Instant to,
|
||||
@Param("search") String search,
|
||||
Pageable pageable
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package net.siegeln.cameleer.saas.audit;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -30,4 +33,10 @@ public class AuditService {
|
||||
entry.setMetadata(metadata);
|
||||
auditRepository.save(entry);
|
||||
}
|
||||
|
||||
public Page<AuditEntity> search(UUID tenantId, String action, String result,
|
||||
Instant from, Instant to, String search,
|
||||
Pageable pageable) {
|
||||
return auditRepository.findFiltered(tenantId, action, result, from, to, search, pageable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.siegeln.cameleer.saas.portal;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditDto.AuditLogEntry;
|
||||
import net.siegeln.cameleer.saas.audit.AuditDto.AuditLogPage;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import net.siegeln.cameleer.saas.config.TenantContext;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/tenant/audit")
|
||||
public class TenantAuditController {
|
||||
|
||||
private final AuditService auditService;
|
||||
|
||||
public TenantAuditController(AuditService auditService) {
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<AuditLogPage> list(
|
||||
@RequestParam(required = false) String action,
|
||||
@RequestParam(required = false) String result,
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(required = false) Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "25") int size) {
|
||||
|
||||
UUID tenantId = TenantContext.getTenantId();
|
||||
size = Math.min(size, 100);
|
||||
var pageResult = auditService.search(tenantId, action, result, from, to, search,
|
||||
PageRequest.of(page, size));
|
||||
|
||||
var entries = pageResult.getContent().stream()
|
||||
.map(AuditLogEntry::from)
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(new AuditLogPage(
|
||||
entries, pageResult.getNumber(), pageResult.getSize(),
|
||||
pageResult.getTotalElements(), pageResult.getTotalPages()));
|
||||
}
|
||||
}
|
||||
51
src/main/java/net/siegeln/cameleer/saas/vendor/VendorAuditController.java
vendored
Normal file
51
src/main/java/net/siegeln/cameleer/saas/vendor/VendorAuditController.java
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
package net.siegeln.cameleer.saas.vendor;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditDto.AuditLogEntry;
|
||||
import net.siegeln.cameleer.saas.audit.AuditDto.AuditLogPage;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/vendor/audit")
|
||||
@PreAuthorize("hasAuthority('SCOPE_platform:admin')")
|
||||
public class VendorAuditController {
|
||||
|
||||
private final AuditService auditService;
|
||||
|
||||
public VendorAuditController(AuditService auditService) {
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<AuditLogPage> list(
|
||||
@RequestParam(required = false) UUID tenantId,
|
||||
@RequestParam(required = false) String action,
|
||||
@RequestParam(required = false) String result,
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(required = false) Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "25") int size) {
|
||||
|
||||
size = Math.min(size, 100);
|
||||
var pageResult = auditService.search(tenantId, action, result, from, to, search,
|
||||
PageRequest.of(page, size));
|
||||
|
||||
var entries = pageResult.getContent().stream()
|
||||
.map(AuditLogEntry::from)
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(new AuditLogPage(
|
||||
entries, pageResult.getNumber(), pageResult.getSize(),
|
||||
pageResult.getTotalElements(), pageResult.getTotalPages()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user