From c69820241f4890d5080bcf262b0e95d4d3aa83a2 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sat, 11 Apr 2026 22:54:23 -0600 Subject: [PATCH] Add activity logging --- .../backend/config/ActivityLoggingFilter.java | 84 ++++++++++++ ...tivityLoggingFilterRegistrationConfig.java | 16 +++ .../controller/ActivityLogController.java | 30 +++++ .../backend/controller/AuthController.java | 16 ++- .../dto/activity/ActivityLogResponse.java | 126 ++++++++++++++++++ .../petshop/backend/entity/ActivityLog.java | 44 ++++++ .../repository/ActivityLogRepository.java | 7 + .../security/RestAccessDeniedHandler.java | 5 + .../RestAuthenticationEntryPoint.java | 5 + .../backend/security/SecurityConfig.java | 7 +- .../backend/service/ActivityLogService.java | 109 +++++++++++++++ .../petshop/backend/service/UserService.java | 11 +- 12 files changed, 456 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilter.java create mode 100644 backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilterRegistrationConfig.java create mode 100644 backend/src/main/java/com/petshop/backend/controller/ActivityLogController.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/activity/ActivityLogResponse.java create mode 100644 backend/src/main/java/com/petshop/backend/service/ActivityLogService.java diff --git a/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilter.java b/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilter.java new file mode 100644 index 00000000..4f0414db --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilter.java @@ -0,0 +1,84 @@ +package com.petshop.backend.config; + +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.AppPrincipal; +import com.petshop.backend.service.ActivityLogService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Order(Ordered.LOWEST_PRECEDENCE - 20) +public class ActivityLoggingFilter extends OncePerRequestFilter { + + private final UserRepository userRepository; + private final ActivityLogService activityLogService; + + public ActivityLoggingFilter(UserRepository userRepository, ActivityLogService activityLogService) { + this.userRepository = userRepository; + this.activityLogService = activityLogService; + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String uri = request.getRequestURI(); + if (uri == null || uri.isBlank()) { + return true; + } + if (!uri.startsWith("/api/")) { + return true; + } + + String lower = uri.toLowerCase(java.util.Locale.ROOT); + return lower.startsWith("/api/v1/health") + || lower.startsWith("/api/v1/activity-logs") + || lower.startsWith("/v3/api-docs") + || lower.startsWith("/swagger-ui") + || lower.startsWith("/ws/"); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + filterChain.doFilter(request, response); + recordActivity(request, response); + } + + private void recordActivity(HttpServletRequest request, HttpServletResponse response) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return; + } + + Long userId = null; + Object principal = authentication.getPrincipal(); + if (principal instanceof AppPrincipal appPrincipal) { + userId = appPrincipal.getUserId(); + } else if (authentication instanceof UsernamePasswordAuthenticationToken token + && token.getPrincipal() instanceof AppPrincipal appPrincipal) { + userId = appPrincipal.getUserId(); + } + + if (userId == null) { + return; + } + + User user = userRepository.findById(userId).orElse(null); + if (user == null) { + return; + } + + String activity = String.format("%s %s -> %d", request.getMethod(), request.getRequestURI(), response.getStatus()); + activityLogService.record(user, activity); + } +} diff --git a/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilterRegistrationConfig.java b/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilterRegistrationConfig.java new file mode 100644 index 00000000..17f6fe2d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilterRegistrationConfig.java @@ -0,0 +1,16 @@ +package com.petshop.backend.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ActivityLoggingFilterRegistrationConfig { + + @Bean + public FilterRegistrationBean activityLoggingFilterRegistration(ActivityLoggingFilter activityLoggingFilter) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(activityLoggingFilter); + registrationBean.setEnabled(false); + return registrationBean; + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/ActivityLogController.java b/backend/src/main/java/com/petshop/backend/controller/ActivityLogController.java new file mode 100644 index 00000000..650fe6f0 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ActivityLogController.java @@ -0,0 +1,30 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.activity.ActivityLogResponse; +import com.petshop.backend.service.ActivityLogService; +import java.util.List; +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> getActivityLogs( + @RequestParam(defaultValue = "2000") int limit) { + int safeLimit = Math.min(Math.max(1, limit), 10000); + return ResponseEntity.ok(activityLogService.getLogs(safeLimit)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index ec4f822b..eae818d9 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -11,6 +11,7 @@ import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.JwtUtil; +import com.petshop.backend.service.ActivityLogService; import com.petshop.backend.service.AvatarStorageService; import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.PhoneUtils; @@ -29,6 +30,8 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashMap; @@ -38,18 +41,22 @@ import java.util.Map; @RequestMapping("/api/v1/auth") public class AuthController { + private static final Logger log = LoggerFactory.getLogger(AuthController.class); + private final AuthenticationManager authenticationManager; private final UserRepository userRepository; private final JwtUtil jwtUtil; private final PasswordEncoder passwordEncoder; private final AvatarStorageService avatarStorageService; + private final ActivityLogService activityLogService; - public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService) { + public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService) { this.authenticationManager = authenticationManager; this.userRepository = userRepository; this.jwtUtil = jwtUtil; this.passwordEncoder = passwordEncoder; this.avatarStorageService = avatarStorageService; + this.activityLogService = activityLogService; } @PostMapping("/register") @@ -60,18 +67,21 @@ public class AuthController { String phone = normalizePhone(request.getPhone()); if (userRepository.findByUsername(username).isPresent()) { + log.warn("Registration rejected: username already exists ({})", username); Map error = new HashMap<>(); error.put("message", "Username already exists"); return ResponseEntity.status(HttpStatus.CONFLICT).body(error); } if (userRepository.findByEmail(email).isPresent()) { + log.warn("Registration rejected: email already exists"); Map error = new HashMap<>(); error.put("message", "Email already exists"); return ResponseEntity.status(HttpStatus.CONFLICT).body(error); } if (phone != null && userRepository.findByPhone(phone).isPresent()) { + log.warn("Registration rejected: phone already exists"); Map error = new HashMap<>(); error.put("message", "Phone already exists"); return ResponseEntity.status(HttpStatus.CONFLICT).body(error); @@ -91,6 +101,7 @@ public class AuthController { User savedUser = userRepository.save(user); String token = jwtUtil.generateToken(savedUser); + activityLogService.record(savedUser, "POST /api/v1/auth/register -> 201"); return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse( savedUser.getId(), @@ -113,6 +124,7 @@ public class AuthController { .orElseThrow(() -> new UsernameNotFoundException("User not found")); String token = jwtUtil.generateToken(user); + activityLogService.record(user, "POST /api/v1/auth/login -> 200"); return ResponseEntity.ok(new LoginResponse( token, @@ -121,11 +133,13 @@ public class AuthController { )); } catch (BadCredentialsException e) { + log.warn("Login failed for username {}", request.getUsername()); Map error = new HashMap<>(); error.put("message", "Invalid username or password"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); } catch (InternalAuthenticationServiceException e) { if (e.getCause() instanceof DisabledException disabledException) { + log.warn("Login denied for disabled user {}", request.getUsername()); Map error = new HashMap<>(); error.put("message", disabledException.getMessage()); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); diff --git a/backend/src/main/java/com/petshop/backend/dto/activity/ActivityLogResponse.java b/backend/src/main/java/com/petshop/backend/dto/activity/ActivityLogResponse.java new file mode 100644 index 00000000..3d5bc890 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/activity/ActivityLogResponse.java @@ -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; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java b/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java index 7445de57..04dc79ec 100644 --- a/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java +++ b/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java @@ -21,6 +21,18 @@ public class ActivityLog { @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; @@ -61,6 +73,38 @@ public class ActivityLog { 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; } diff --git a/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java b/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java index 5c5db4c5..fa79d06a 100644 --- a/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java @@ -1,9 +1,16 @@ package com.petshop.backend.repository; import com.petshop.backend.entity.ActivityLog; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; @Repository public interface ActivityLogRepository extends JpaRepository { + boolean existsByUser_Id(Long userId); + + @Query("select a from ActivityLog a order by a.logTimestamp desc, a.logId desc") + List findRecent(Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/security/RestAccessDeniedHandler.java b/backend/src/main/java/com/petshop/backend/security/RestAccessDeniedHandler.java index 2ef240e9..fcaa49bc 100644 --- a/backend/src/main/java/com/petshop/backend/security/RestAccessDeniedHandler.java +++ b/backend/src/main/java/com/petshop/backend/security/RestAccessDeniedHandler.java @@ -8,12 +8,16 @@ import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; @Component public class RestAccessDeniedHandler implements AccessDeniedHandler { + private static final Logger log = LoggerFactory.getLogger(RestAccessDeniedHandler.class); + private final ApiErrorResponder apiErrorResponder; public RestAccessDeniedHandler(ApiErrorResponder apiErrorResponder) { @@ -22,6 +26,7 @@ public class RestAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + log.warn("Access denied: {} {} - {}", request.getMethod(), request.getRequestURI(), accessDeniedException.getMessage()); apiErrorResponder.write( response, HttpStatus.FORBIDDEN, diff --git a/backend/src/main/java/com/petshop/backend/security/RestAuthenticationEntryPoint.java b/backend/src/main/java/com/petshop/backend/security/RestAuthenticationEntryPoint.java index 2ae541b4..fb28314b 100644 --- a/backend/src/main/java/com/petshop/backend/security/RestAuthenticationEntryPoint.java +++ b/backend/src/main/java/com/petshop/backend/security/RestAuthenticationEntryPoint.java @@ -8,12 +8,16 @@ import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; @Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + private static final Logger log = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); + private final ApiErrorResponder apiErrorResponder; public RestAuthenticationEntryPoint(ApiErrorResponder apiErrorResponder) { @@ -22,6 +26,7 @@ public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + log.warn("Unauthorized request: {} {} - {}", request.getMethod(), request.getRequestURI(), authException.getMessage()); apiErrorResponder.write( response, HttpStatus.UNAUTHORIZED, diff --git a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java index d26e3e84..c197242f 100644 --- a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -21,6 +21,8 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import com.petshop.backend.config.ActivityLoggingFilter; + import java.util.List; @Configuration @@ -44,7 +46,7 @@ public class SecurityConfig { } @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, ActivityLoggingFilter activityLoggingFilter) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth @@ -65,6 +67,7 @@ public class SecurityConfig { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authenticationProvider(daoAuthenticationProvider()) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + http.addFilterAfter(activityLoggingFilter, JwtAuthenticationFilter.class); return http.build(); } @@ -97,7 +100,7 @@ public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { - + return new BCryptPasswordEncoder(); } } diff --git a/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java b/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java new file mode 100644 index 00000000..a23c1747 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java @@ -0,0 +1,109 @@ +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.User; +import com.petshop.backend.repository.ActivityLogRepository; +import com.petshop.backend.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class ActivityLogService { + + private static final Logger log = LoggerFactory.getLogger(ActivityLogService.class); + + private final ActivityLogRepository activityLogRepository; + private final UserRepository userRepository; + + public ActivityLogService(ActivityLogRepository activityLogRepository, UserRepository userRepository) { + this.activityLogRepository = activityLogRepository; + this.userRepository = userRepository; + } + + @Transactional + public void record(User user, String activity) { + if (user == null || activity == null || activity.isBlank()) { + return; + } + + try { + User managedUser = userRepository.findById(user.getId()).orElse(user); + StoreLocation store = managedUser.getPrimaryStore(); + ActivityLog entry = new ActivityLog(); + entry.setUser(managedUser); + 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); + } catch (Exception ex) { + log.warn("Failed to persist activity log", ex); + } + } + + @Transactional(readOnly = true) + public List getLogs(int limit) { + return activityLogRepository.findRecent(PageRequest.of(0, limit)).stream().map(this::toResponse).toList(); + } + + 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; + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java index a6661cf1..77141b92 100644 --- a/backend/src/main/java/com/petshop/backend/service/UserService.java +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -3,6 +3,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.user.UserRequest; import com.petshop.backend.dto.user.UserResponse; +import com.petshop.backend.repository.ActivityLogRepository; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; @@ -25,11 +26,13 @@ import static org.springframework.http.HttpStatus.CONFLICT; public class UserService { private final UserRepository userRepository; + private final ActivityLogRepository activityLogRepository; private final PasswordEncoder passwordEncoder; private final StoreRepository storeRepository; - public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository) { + public UserService(UserRepository userRepository, ActivityLogRepository activityLogRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository) { this.userRepository = userRepository; + this.activityLogRepository = activityLogRepository; this.passwordEncoder = passwordEncoder; this.storeRepository = storeRepository; } @@ -121,11 +124,17 @@ public class UserService { if (!userRepository.existsById(id)) { throw new ResourceNotFoundException("User not found with id: " + id); } + if (activityLogRepository.existsByUser_Id(id)) { + throw new ResponseStatusException(CONFLICT, "User cannot be deleted because activity logs exist"); + } userRepository.deleteById(id); } @Transactional public void bulkDeleteUsers(BulkDeleteRequest request) { + if (request.getIds() != null && request.getIds().stream().anyMatch(activityLogRepository::existsByUser_Id)) { + throw new ResponseStatusException(CONFLICT, "One or more users cannot be deleted because activity logs exist"); + } userRepository.deleteAllById(request.getIds()); }