restore activity logs endpoint

This commit is contained in:
2026-04-15 23:19:22 -06:00
parent 4402d0398f
commit 4e5f221749
6 changed files with 468 additions and 13 deletions

View File

@@ -10,7 +10,7 @@ public class ActivityLoggingFilterRegistrationConfig {
@Bean @Bean
public FilterRegistrationBean<ActivityLoggingFilter> activityLoggingFilterRegistration(ActivityLoggingFilter activityLoggingFilter) { public FilterRegistrationBean<ActivityLoggingFilter> activityLoggingFilterRegistration(ActivityLoggingFilter activityLoggingFilter) {
FilterRegistrationBean<ActivityLoggingFilter> registrationBean = new FilterRegistrationBean<>(activityLoggingFilter); FilterRegistrationBean<ActivityLoggingFilter> registrationBean = new FilterRegistrationBean<>(activityLoggingFilter);
registrationBean.setEnabled(false); registrationBean.setEnabled(true);
return registrationBean; return registrationBean;
} }
} }

View File

@@ -0,0 +1,37 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.activity.ActivityLogResponse;
import com.petshop.backend.service.ActivityLogService;
import java.time.LocalDate;
import java.util.List;
import org.springframework.format.annotation.DateTimeFormat;
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;
@RestController
@RequestMapping("/api/v1/activity-logs")
@PreAuthorize("hasRole('ADMIN')")
public class ActivityLogController {
private final ActivityLogService activityLogService;
public ActivityLogController(ActivityLogService activityLogService) {
this.activityLogService = activityLogService;
}
@GetMapping
public ResponseEntity<List<ActivityLogResponse>> getActivityLogs(
@RequestParam(defaultValue = "2000") int limit,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String role,
@RequestParam(required = false) String search,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
int safeLimit = Math.min(Math.max(1, limit), 10000);
return ResponseEntity.ok(activityLogService.getLogs(safeLimit, storeId, role, search, startDate, endDate));
}
}

View File

@@ -0,0 +1,126 @@
package com.petshop.backend.dto.activity;
import java.time.LocalDateTime;
public class ActivityLogResponse {
private Long logId;
private Long userId;
private String username;
private String fullName;
private String role;
private Long storeId;
private String storeName;
private String usernameSnapshot;
private String fullNameSnapshot;
private String roleSnapshot;
private String storeNameSnapshot;
private String activity;
private LocalDateTime logTimestamp;
public ActivityLogResponse() {
}
public Long getLogId() {
return logId;
}
public void setLogId(Long logId) {
this.logId = logId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public String getUsernameSnapshot() {
return usernameSnapshot;
}
public void setUsernameSnapshot(String usernameSnapshot) {
this.usernameSnapshot = usernameSnapshot;
}
public String getFullNameSnapshot() {
return fullNameSnapshot;
}
public void setFullNameSnapshot(String fullNameSnapshot) {
this.fullNameSnapshot = fullNameSnapshot;
}
public String getRoleSnapshot() {
return roleSnapshot;
}
public void setRoleSnapshot(String roleSnapshot) {
this.roleSnapshot = roleSnapshot;
}
public String getStoreNameSnapshot() {
return storeNameSnapshot;
}
public void setStoreNameSnapshot(String storeNameSnapshot) {
this.storeNameSnapshot = storeNameSnapshot;
}
public String getActivity() {
return activity;
}
public void setActivity(String activity) {
this.activity = activity;
}
public LocalDateTime getLogTimestamp() {
return logTimestamp;
}
public void setLogTimestamp(LocalDateTime logTimestamp) {
this.logTimestamp = logTimestamp;
}
}

View File

@@ -0,0 +1,148 @@
package com.petshop.backend.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.Immutable;
import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Immutable
@Table(name = "activityLog")
public class ActivityLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long logId;
@ManyToOne
@JoinColumn(name = "userId", nullable = false)
private User user;
@ManyToOne
@JoinColumn(name = "storeId")
private StoreLocation store;
@Column(length = 50)
private String usernameSnapshot;
@Column(length = 100)
private String fullNameSnapshot;
@Column(length = 20)
private String roleSnapshot;
@Column(length = 100)
private String storeNameSnapshot;
@Column(nullable = false, columnDefinition = "TEXT")
private String activity;
@Column(nullable = false)
private LocalDateTime logTimestamp = LocalDateTime.now();
public ActivityLog() {
}
public ActivityLog(Long logId, User user, String activity, LocalDateTime logTimestamp) {
this.logId = logId;
this.user = user;
this.activity = activity;
this.logTimestamp = logTimestamp;
}
public Long getLogId() {
return logId;
}
public void setLogId(Long logId) {
this.logId = logId;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public StoreLocation getStore() {
return store;
}
public void setStore(StoreLocation store) {
this.store = store;
}
public String getUsernameSnapshot() {
return usernameSnapshot;
}
public void setUsernameSnapshot(String usernameSnapshot) {
this.usernameSnapshot = usernameSnapshot;
}
public String getFullNameSnapshot() {
return fullNameSnapshot;
}
public void setFullNameSnapshot(String fullNameSnapshot) {
this.fullNameSnapshot = fullNameSnapshot;
}
public String getRoleSnapshot() {
return roleSnapshot;
}
public void setRoleSnapshot(String roleSnapshot) {
this.roleSnapshot = roleSnapshot;
}
public String getStoreNameSnapshot() {
return storeNameSnapshot;
}
public void setStoreNameSnapshot(String storeNameSnapshot) {
this.storeNameSnapshot = storeNameSnapshot;
}
public String getActivity() {
return activity;
}
public void setActivity(String activity) {
this.activity = activity;
}
public LocalDateTime getLogTimestamp() {
return logTimestamp;
}
public void setLogTimestamp(LocalDateTime logTimestamp) {
this.logTimestamp = logTimestamp;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ActivityLog that = (ActivityLog) o;
return Objects.equals(logId, that.logId);
}
@Override
public int hashCode() {
return Objects.hash(logId);
}
@Override
public String toString() {
return "ActivityLog{" +
"logId=" + logId +
", user=" + user +
", activity='" + activity + '\'' +
", logTimestamp=" + logTimestamp +
'}';
}
}

View File

@@ -0,0 +1,18 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.ActivityLog;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ActivityLogRepository extends JpaRepository<ActivityLog, Long>, JpaSpecificationExecutor<ActivityLog> {
boolean existsByUser_Id(Long userId);
@Query("select a from ActivityLog a order by a.logTimestamp desc, a.logId desc")
List<ActivityLog> findRecent(Pageable pageable);
}

View File

@@ -1,48 +1,174 @@
package com.petshop.backend.service; package com.petshop.backend.service;
import com.petshop.backend.dto.activity.ActivityLogResponse;
import com.petshop.backend.entity.ActivityLog;
import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.repository.ActivityLogRepository;
import com.petshop.backend.repository.UserRepository; import com.petshop.backend.repository.UserRepository;
import jakarta.persistence.criteria.Predicate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Service @Service
public class ActivityLogService { public class ActivityLogService {
private static final Logger log = LoggerFactory.getLogger("activity"); private static final Logger log = LoggerFactory.getLogger(ActivityLogService.class);
private final ActivityLogRepository activityLogRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
public ActivityLogService(UserRepository userRepository) { public ActivityLogService(ActivityLogRepository activityLogRepository, UserRepository userRepository) {
this.activityLogRepository = activityLogRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
} }
@Async @Transactional
public void record(Long userId, String activity) { public void record(Long userId, String activity) {
if (userId == null || activity == null || activity.isBlank()) { if (userId == null || activity == null || activity.isBlank()) {
return; return;
} }
try { try {
User user = userRepository.findById(userId).orElse(null); User managedUser = userRepository.findById(userId).orElse(null);
if (user == null) { if (managedUser == null) {
return; return;
} }
StoreLocation store = user.getPrimaryStore(); StoreLocation store = managedUser.getPrimaryStore();
String role = user.getRole() != null ? user.getRole().name() : "UNKNOWN"; ActivityLog entry = new ActivityLog();
String storeName = store != null ? store.getStoreName() : "no store"; entry.setUser(managedUser);
log.info("{} | {} | {} | {}", role, user.getUsername(), storeName, activity.trim()); entry.setStore(store);
entry.setUsernameSnapshot(managedUser.getUsername());
entry.setFullNameSnapshot(resolveFullName(managedUser));
entry.setRoleSnapshot(managedUser.getRole() != null ? managedUser.getRole().name() : null);
entry.setStoreNameSnapshot(store != null ? store.getStoreName() : null);
entry.setActivity(activity.trim());
activityLogRepository.save(entry);
log.info("[ACTIVITY] {} | {} | {} | {}",
entry.getRoleSnapshot(),
entry.getUsernameSnapshot(),
entry.getStoreNameSnapshot() != null ? entry.getStoreNameSnapshot() : "no store",
entry.getActivity());
} catch (Exception ex) { } catch (Exception ex) {
log.warn("Failed to record activity", ex); log.warn("Failed to persist activity log", ex);
} }
} }
@Async
public void record(User user, String activity) { public void record(User user, String activity) {
if (user == null) { if (user == null) {
return; return;
} }
record(user.getId(), activity); record(user.getId(), activity);
} }
@Transactional(readOnly = true)
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search, LocalDate startDate, LocalDate endDate) {
Specification<ActivityLog> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (storeId != null) {
predicates.add(cb.equal(root.get("store").get("storeId"), storeId));
}
if (role != null && !role.isBlank()) {
predicates.add(cb.equal(root.get("roleSnapshot"), role));
}
if (search != null && !search.isBlank()) {
String pattern = "%" + search.toLowerCase() + "%";
Predicate searchPredicate = cb.or(
cb.like(cb.lower(root.get("activity")), pattern),
cb.like(cb.lower(root.get("fullNameSnapshot")), pattern),
cb.like(cb.lower(root.get("usernameSnapshot")), pattern)
);
predicates.add(searchPredicate);
}
if (startDate != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("logTimestamp"), startDate.atStartOfDay()));
}
if (endDate != null) {
predicates.add(cb.lessThan(root.get("logTimestamp"), endDate.plusDays(1).atStartOfDay()));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
PageRequest pageRequest = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "logTimestamp", "logId"));
return activityLogRepository.findAll(spec, pageRequest).stream()
.map(this::toResponse)
.toList();
}
@Transactional(readOnly = true)
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search) {
return getLogs(limit, storeId, role, search, null, null);
}
@Transactional(readOnly = true)
public List<ActivityLogResponse> getLogs(int limit) {
return getLogs(limit, null, null, null, null, null);
}
private ActivityLogResponse toResponse(ActivityLog entry) {
ActivityLogResponse response = new ActivityLogResponse();
response.setLogId(entry.getLogId());
if (entry.getUser() != null) {
response.setUserId(entry.getUser().getId());
response.setUsername(firstNonBlank(entry.getUsernameSnapshot(), entry.getUser().getUsername()));
response.setFullName(firstNonBlank(entry.getFullNameSnapshot(), resolveFullName(entry.getUser())));
response.setRole(firstNonBlank(entry.getRoleSnapshot(), entry.getUser().getRole() != null ? entry.getUser().getRole().name() : null));
}
StoreLocation store = entry.getStore();
if (store != null) {
response.setStoreId(store.getStoreId());
response.setStoreName(firstNonBlank(entry.getStoreNameSnapshot(), store.getStoreName()));
}
response.setUsernameSnapshot(entry.getUsernameSnapshot());
response.setFullNameSnapshot(entry.getFullNameSnapshot());
response.setRoleSnapshot(entry.getRoleSnapshot());
response.setStoreNameSnapshot(entry.getStoreNameSnapshot());
response.setActivity(entry.getActivity());
response.setLogTimestamp(entry.getLogTimestamp());
return response;
}
private String resolveFullName(User user) {
if (user == null) {
return null;
}
if (user.getFullName() != null && !user.getFullName().isBlank()) {
return user.getFullName();
}
String first = user.getFirstName();
String last = user.getLastName();
if (first == null || first.isBlank()) {
return last;
}
if (last == null || last.isBlank()) {
return first;
}
return first.trim() + " " + last.trim();
}
private String firstNonBlank(String preferred, String fallback) {
if (preferred != null && !preferred.isBlank()) {
return preferred;
}
return fallback;
}
} }