Hide adopted pets

This commit is contained in:
2026-04-02 09:54:32 -06:00
parent 86267ddddf
commit ca06f6c8b3
3 changed files with 242 additions and 6 deletions

View File

@@ -16,4 +16,16 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
@Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species))")
Page<Pet> searchPublicPets(@Param("q") String query, @Param("species") String species, Pageable pageable);
@Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " +
"(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId) AND " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
Page<Pet> searchCustomerVisiblePets(@Param("userId") Long userId, @Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
}

View File

@@ -7,12 +7,16 @@ import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.security.AppPrincipal;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.PetRepository;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -34,13 +38,38 @@ public class PetService {
}
public Page<PetResponse> getAllPets(String query, String species, String status, Pageable pageable) {
return petRepository.searchPets(normalizeFilter(query), normalizeFilter(species), normalizeFilter(status), pageable)
String normalizedQuery = normalizeFilter(query);
String normalizedSpecies = normalizeFilter(species);
String normalizedStatus = normalizeFilter(status);
CurrentViewer viewer = getCurrentViewer();
Page<Pet> pets;
if (viewer == null) {
if (!isAllowedPublicStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0);
}
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable);
} else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) {
pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, pageable);
} else if (viewer.role() == User.Role.CUSTOMER) {
if (!isAllowedCustomerStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0);
}
pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable);
} else {
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable);
}
return pets
.map(this::mapToResponse);
}
public PetResponse getPetById(Long id) {
Pet pet = petRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
if (!canViewPet(pet, getCurrentViewer())) {
throw new ResourceNotFoundException("Pet not found with id: " + id);
}
return mapToResponse(pet);
}
@@ -110,7 +139,7 @@ public class PetService {
if (pet.getImageUrl() == null || pet.getImageUrl().isBlank()) {
throw new ResourceNotFoundException("Pet image not found for id: " + id);
}
if (!canViewPetImage(pet, requesterUserId, requesterRole)) {
if (!canViewPet(pet, new CurrentViewer(requesterUserId, requesterRole))) {
throw new ForbiddenImageAccessException();
}
Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl());
@@ -122,14 +151,21 @@ public class PetService {
return "available".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()));
}
private boolean canViewPetImage(Pet pet, Long requesterUserId, User.Role requesterRole) {
private boolean canViewPet(Pet pet, CurrentViewer viewer) {
if (isPubliclyVisible(pet)) {
return true;
}
if (requesterRole == User.Role.STAFF || requesterRole == User.Role.ADMIN) {
if (viewer != null && (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN)) {
return true;
}
if (requesterUserId == null) {
if (viewer == null || viewer.userId() == null) {
return false;
}
return isAdoptedByUser(pet, viewer.userId());
}
private boolean isAdoptedByUser(Pet pet, Long userId) {
if (userId == null) {
return false;
}
if (!"adopted".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) {
@@ -137,10 +173,22 @@ public class PetService {
}
return adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(pet.getPetId(), "Completed")
.map(Adoption::getCustomer)
.map(customer -> requesterUserId.equals(customer.getUserId()))
.map(customer -> userId.equals(customer.getUserId()))
.orElse(false);
}
private CurrentViewer getCurrentViewer() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof AppPrincipal appPrincipal) {
return new CurrentViewer(appPrincipal.getUserId(), appPrincipal.getRole());
}
return null;
}
private Pet findPet(Long id) {
return petRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
@@ -177,6 +225,14 @@ public class PetService {
return status == null ? "" : status.trim();
}
private boolean isAllowedPublicStatus(String status) {
return status == null || "available".equalsIgnoreCase(status);
}
private boolean isAllowedCustomerStatus(String status) {
return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status);
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
@@ -203,6 +259,9 @@ public class PetService {
public record ImagePayload(Resource resource, MediaType mediaType) {
}
private record CurrentViewer(Long userId, User.Role role) {
}
public static class ForbiddenImageAccessException extends RuntimeException {
}
}