new admin-user api

This commit is contained in:
2025-12-08 07:10:10 -05:00
parent 0845443767
commit 7a8ec969b5
18 changed files with 1196 additions and 1004 deletions

View File

@@ -13,4 +13,6 @@ public interface UserRepository extends JpaRepository<User, Integer> {
boolean existsByEmailIgnoreCaseAndDeletedAtIsNull(String email);
Optional<User> findByUuid(UUID uuid);
boolean existsByRole(String role);
}

View File

@@ -1,16 +1,16 @@
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Brand;
import java.util.List;
import java.util.Optional;
public interface BrandService {
List<Brand> findAll();
Optional<Brand> findById(Integer id);
Brand save(Brand item);
void deleteById(Integer id);
}
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Brand;
import java.util.List;
import java.util.Optional;
public interface BrandService {
List<Brand> findAll();
Optional<Brand> findById(Integer id);
Brand save(Brand item);
void deleteById(Integer id);
}

View File

@@ -1,95 +1,95 @@
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.model.MerchantCategoryMapping;
import group.goforward.ballistic.model.ProductConfiguration;
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
import jakarta.transaction.Transactional;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class MerchantCategoryMappingService {
private final MerchantCategoryMappingRepository mappingRepository;
public MerchantCategoryMappingService(MerchantCategoryMappingRepository mappingRepository) {
this.mappingRepository = mappingRepository;
}
public List<MerchantCategoryMapping> findByMerchant(Integer merchantId) {
return mappingRepository.findByMerchantIdOrderByRawCategoryAsc(merchantId);
}
/**
* Resolve (or create) a mapping row for this merchant + raw category.
* - If it exists, returns it (with whatever mappedPartRole / mappedConfiguration are set).
* - If it doesn't exist, creates a placeholder row with null mappings and returns it.
*
* The importer can then:
* - skip rows where mappedPartRole is still null
* - use mappedConfiguration if present
*/
@Transactional
public MerchantCategoryMapping resolveMapping(Merchant merchant, String rawCategory) {
if (rawCategory == null || rawCategory.isBlank()) {
return null;
}
String trimmed = rawCategory.trim();
return mappingRepository
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
.orElseGet(() -> {
MerchantCategoryMapping mapping = new MerchantCategoryMapping();
mapping.setMerchant(merchant);
mapping.setRawCategory(trimmed);
mapping.setMappedPartRole(null);
mapping.setMappedConfiguration(null);
return mappingRepository.save(mapping);
});
}
/**
* Upsert mapping (admin UI).
*/
@Transactional
public MerchantCategoryMapping upsertMapping(
Merchant merchant,
String rawCategory,
String mappedPartRole,
ProductConfiguration mappedConfiguration
) {
String trimmed = rawCategory.trim();
MerchantCategoryMapping mapping = mappingRepository
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
.orElseGet(() -> {
MerchantCategoryMapping m = new MerchantCategoryMapping();
m.setMerchant(merchant);
m.setRawCategory(trimmed);
return m;
});
mapping.setMappedPartRole(
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
);
mapping.setMappedConfiguration(mappedConfiguration);
return mappingRepository.save(mapping);
}
/**
* Backwards-compatible overload for existing callers (e.g. controller)
* that dont care about productConfiguration yet.
*/
@Transactional
public MerchantCategoryMapping upsertMapping(
Merchant merchant,
String rawCategory,
String mappedPartRole
) {
// Delegate to the new method with `null` configuration
return upsertMapping(merchant, rawCategory, mappedPartRole, null);
}
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.model.MerchantCategoryMapping;
import group.goforward.ballistic.model.ProductConfiguration;
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
import jakarta.transaction.Transactional;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class MerchantCategoryMappingService {
private final MerchantCategoryMappingRepository mappingRepository;
public MerchantCategoryMappingService(MerchantCategoryMappingRepository mappingRepository) {
this.mappingRepository = mappingRepository;
}
public List<MerchantCategoryMapping> findByMerchant(Integer merchantId) {
return mappingRepository.findByMerchantIdOrderByRawCategoryAsc(merchantId);
}
/**
* Resolve (or create) a mapping row for this merchant + raw category.
* - If it exists, returns it (with whatever mappedPartRole / mappedConfiguration are set).
* - If it doesn't exist, creates a placeholder row with null mappings and returns it.
*
* The importer can then:
* - skip rows where mappedPartRole is still null
* - use mappedConfiguration if present
*/
@Transactional
public MerchantCategoryMapping resolveMapping(Merchant merchant, String rawCategory) {
if (rawCategory == null || rawCategory.isBlank()) {
return null;
}
String trimmed = rawCategory.trim();
return mappingRepository
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
.orElseGet(() -> {
MerchantCategoryMapping mapping = new MerchantCategoryMapping();
mapping.setMerchant(merchant);
mapping.setRawCategory(trimmed);
mapping.setMappedPartRole(null);
mapping.setMappedConfiguration(null);
return mappingRepository.save(mapping);
});
}
/**
* Upsert mapping (admin UI).
*/
@Transactional
public MerchantCategoryMapping upsertMapping(
Merchant merchant,
String rawCategory,
String mappedPartRole,
ProductConfiguration mappedConfiguration
) {
String trimmed = rawCategory.trim();
MerchantCategoryMapping mapping = mappingRepository
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
.orElseGet(() -> {
MerchantCategoryMapping m = new MerchantCategoryMapping();
m.setMerchant(merchant);
m.setRawCategory(trimmed);
return m;
});
mapping.setMappedPartRole(
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
);
mapping.setMappedConfiguration(mappedConfiguration);
return mappingRepository.save(mapping);
}
/**
* Backwards-compatible overload for existing callers (e.g. controller)
* that dont care about productConfiguration yet.
*/
@Transactional
public MerchantCategoryMapping upsertMapping(
Merchant merchant,
String rawCategory,
String mappedPartRole
) {
// Delegate to the new method with `null` configuration
return upsertMapping(merchant, rawCategory, mappedPartRole, null);
}
}

View File

@@ -1,14 +1,14 @@
package group.goforward.ballistic.services;
public interface MerchantFeedImportService {
/**
* Full product + offer import for a given merchant.
*/
void importMerchantFeed(Integer merchantId);
/**
* Offers-only sync (price / stock) for a given merchant.
*/
void syncOffersOnly(Integer merchantId);
package group.goforward.ballistic.services;
public interface MerchantFeedImportService {
/**
* Full product + offer import for a given merchant.
*/
void importMerchantFeed(Integer merchantId);
/**
* Offers-only sync (price / stock) for a given merchant.
*/
void syncOffersOnly(Integer merchantId);
}

View File

@@ -1,17 +1,17 @@
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Psa;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface PsaService {
List<Psa> findAll();
Optional<Psa> findById(UUID id);
Psa save(Psa psa);
void deleteById(UUID id);
}
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Psa;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface PsaService {
List<Psa> findAll();
Optional<Psa> findById(UUID id);
Psa save(Psa psa);
void deleteById(UUID id);
}

View File

@@ -1,16 +1,16 @@
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.State;
import java.util.List;
import java.util.Optional;
public interface StatesService {
List<State> findAll();
Optional<State> findById(Integer id);
State save(State item);
void deleteById(Integer id);
}
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.State;
import java.util.List;
import java.util.Optional;
public interface StatesService {
List<State> findAll();
Optional<State> findById(Integer id);
State save(State item);
void deleteById(Integer id);
}

View File

@@ -1,16 +1,16 @@
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.User;
import java.util.List;
import java.util.Optional;
public interface UsersService {
List<User> findAll();
Optional<User> findById(Integer id);
User save(User item);
void deleteById(Integer id);
}
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.User;
import java.util.List;
import java.util.Optional;
public interface UsersService {
List<User> findAll();
Optional<User> findById(Integer id);
User save(User item);
void deleteById(Integer id);
}

View File

@@ -0,0 +1,55 @@
package group.goforward.ballistic.services.admin;
import group.goforward.ballistic.model.User;
import group.goforward.ballistic.repos.UserRepository;
import group.goforward.ballistic.web.dto.admin.AdminUserDto;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@Service
public class AdminUserService {
private static final Set<String> ALLOWED_ROLES = Set.of("USER", "ADMIN");
private final UserRepository userRepository;
public AdminUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<AdminUserDto> getAllUsersForAdmin() {
return userRepository.findAll()
.stream()
.map(AdminUserDto::fromUser)
.toList();
}
@Transactional
public AdminUserDto updateUserRole(UUID userUuid, String newRole, Authentication auth) {
if (newRole == null || !ALLOWED_ROLES.contains(newRole.toUpperCase())) {
throw new IllegalArgumentException("Invalid role: " + newRole);
}
User user = userRepository.findByUuid(userUuid)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
// Optional safety: do not allow demoting yourself (you can loosen this later)
String currentEmail = auth != null ? auth.getName() : null;
boolean isSelf = currentEmail != null
&& currentEmail.equalsIgnoreCase(user.getEmail());
if (isSelf && !"ADMIN".equalsIgnoreCase(newRole)) {
throw new IllegalStateException("You cannot change your own role to non-admin.");
}
user.setRole(newRole.toUpperCase());
// updatedAt will be handled by your entity / DB defaults
return AdminUserDto.fromUser(user);
}
}

View File

@@ -1,38 +1,38 @@
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.Brand;
import group.goforward.ballistic.repos.BrandRepository;
import group.goforward.ballistic.services.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class BrandServiceImpl implements BrandService {
@Autowired
private BrandRepository repo;
@Override
public List<Brand> findAll() {
return repo.findAll();
}
@Override
public Optional<Brand> findById(Integer id) {
return repo.findById(id);
}
@Override
public Brand save(Brand item) {
return null;
}
@Override
public void deleteById(Integer id) {
deleteById(id);
}
}
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.Brand;
import group.goforward.ballistic.repos.BrandRepository;
import group.goforward.ballistic.services.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class BrandServiceImpl implements BrandService {
@Autowired
private BrandRepository repo;
@Override
public List<Brand> findAll() {
return repo.findAll();
}
@Override
public Optional<Brand> findById(Integer id) {
return repo.findById(id);
}
@Override
public Brand save(Brand item) {
return null;
}
@Override
public void deleteById(Integer id) {
deleteById(id);
}
}

View File

@@ -1,41 +1,41 @@
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.Psa;
import group.goforward.ballistic.repos.PsaRepository;
import group.goforward.ballistic.services.PsaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Service
public class PsaServiceImpl implements PsaService {
private final PsaRepository psaRepository;
@Autowired
public PsaServiceImpl(PsaRepository psaRepository) {
this.psaRepository = psaRepository;
}
@Override
public List<Psa> findAll() {
return psaRepository.findAll();
}
@Override
public Optional<Psa> findById(UUID id) {
return psaRepository.findById(id);
}
@Override
public Psa save(Psa psa) {
return psaRepository.save(psa);
}
@Override
public void deleteById(UUID id) {
psaRepository.deleteById(id);
}
}
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.Psa;
import group.goforward.ballistic.repos.PsaRepository;
import group.goforward.ballistic.services.PsaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Service
public class PsaServiceImpl implements PsaService {
private final PsaRepository psaRepository;
@Autowired
public PsaServiceImpl(PsaRepository psaRepository) {
this.psaRepository = psaRepository;
}
@Override
public List<Psa> findAll() {
return psaRepository.findAll();
}
@Override
public Optional<Psa> findById(UUID id) {
return psaRepository.findById(id);
}
@Override
public Psa save(Psa psa) {
return psaRepository.save(psa);
}
@Override
public void deleteById(UUID id) {
psaRepository.deleteById(id);
}
}

View File

@@ -1,38 +1,38 @@
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.State;
import group.goforward.ballistic.repos.StateRepository;
import group.goforward.ballistic.services.StatesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StatesServiceImpl implements StatesService {
@Autowired
private StateRepository repo;
@Override
public List<State> findAll() {
return repo.findAll();
}
@Override
public Optional<State> findById(Integer id) {
return repo.findById(id);
}
@Override
public State save(State item) {
return null;
}
@Override
public void deleteById(Integer id) {
deleteById(id);
}
}
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.State;
import group.goforward.ballistic.repos.StateRepository;
import group.goforward.ballistic.services.StatesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StatesServiceImpl implements StatesService {
@Autowired
private StateRepository repo;
@Override
public List<State> findAll() {
return repo.findAll();
}
@Override
public Optional<State> findById(Integer id) {
return repo.findById(id);
}
@Override
public State save(State item) {
return null;
}
@Override
public void deleteById(Integer id) {
deleteById(id);
}
}

View File

@@ -1,37 +1,37 @@
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.User;
import group.goforward.ballistic.repos.UserRepository;
import group.goforward.ballistic.services.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UsersServiceImpl implements UsersService {
@Autowired
private UserRepository repo;
@Override
public List<User> findAll() {
return repo.findAll();
}
@Override
public Optional<User> findById(Integer id) {
return repo.findById(id);
}
@Override
public User save(User item) {
return null;
}
@Override
public void deleteById(Integer id) {
deleteById(id);
}
}
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.User;
import group.goforward.ballistic.repos.UserRepository;
import group.goforward.ballistic.services.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UsersServiceImpl implements UsersService {
@Autowired
private UserRepository repo;
@Override
public List<User> findAll() {
return repo.findAll();
}
@Override
public Optional<User> findById(Integer id) {
return repo.findById(id);
}
@Override
public User save(User item) {
return null;
}
@Override
public void deleteById(Integer id) {
deleteById(id);
}
}

View File

@@ -1,13 +1,13 @@
/**
* Provides the classes necessary for the Spring Services implementations for the ballistic -Builder application.
* This package includes Services implementations for Spring-Boot application
*
*
* <p>The main entry point for managing the inventory is the
* {@link group.goforward.ballistic.BattlBuilderApplication} class.</p>
*
* @since 1.0
* @author Don Strawsburg
* @version 1.1
*/
/**
* Provides the classes necessary for the Spring Services implementations for the ballistic -Builder application.
* This package includes Services implementations for Spring-Boot application
*
*
* <p>The main entry point for managing the inventory is the
* {@link group.goforward.ballistic.BattlBuilderApplication} class.</p>
*
* @since 1.0
* @author Don Strawsburg
* @version 1.1
*/
package group.goforward.ballistic.services.impl;

View File

@@ -0,0 +1,37 @@
package group.goforward.ballistic.web.admin;
import group.goforward.ballistic.services.admin.AdminUserService;
import group.goforward.ballistic.web.dto.admin.AdminUserDto;
import group.goforward.ballistic.web.dto.admin.UpdateUserRoleRequest;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public class AdminUserController {
private final AdminUserService adminUserService;
public AdminUserController(AdminUserService adminUserService) {
this.adminUserService = adminUserService;
}
@GetMapping
public List<AdminUserDto> listUsers() {
return adminUserService.getAllUsersForAdmin();
}
@PatchMapping("/{uuid}/role")
public AdminUserDto updateRole(
@PathVariable("uuid") UUID uuid,
@RequestBody UpdateUserRoleRequest request,
Authentication auth
) {
return adminUserService.updateUserRole(uuid, request.getRole(), auth);
}
}

View File

@@ -0,0 +1,76 @@
package group.goforward.ballistic.web.dto.admin;
import group.goforward.ballistic.model.User;
import java.time.OffsetDateTime;
import java.util.UUID;
public class AdminUserDto {
// We'll expose the UUID as the "id" used by the frontend
private UUID id;
private String email;
private String displayName;
private String role;
private OffsetDateTime createdAt;
private OffsetDateTime updatedAt;
private OffsetDateTime lastLoginAt;
public AdminUserDto(UUID id,
String email,
String displayName,
String role,
OffsetDateTime createdAt,
OffsetDateTime updatedAt,
OffsetDateTime lastLoginAt) {
this.id = id;
this.email = email;
this.displayName = displayName;
this.role = role;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.lastLoginAt = lastLoginAt;
}
public static AdminUserDto fromUser(User user) {
return new AdminUserDto(
user.getUuid(), // use UUID here (stable id)
user.getEmail(),
user.getDisplayName(),
user.getRole(), // String: "USER" / "ADMIN"
user.getCreatedAt(),
user.getUpdatedAt(),
user.getLastLoginAt()
);
}
// Getters (and setters if you want Jackson to use them)
public UUID getId() {
return id;
}
public String getEmail() {
return email;
}
public String getDisplayName() {
return displayName;
}
public String getRole() {
return role;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public OffsetDateTime getLastLoginAt() {
return lastLoginAt;
}
}

View File

@@ -0,0 +1,21 @@
package group.goforward.ballistic.web.dto.admin;
public class UpdateUserRoleRequest {
private String role;
public UpdateUserRoleRequest() {
}
public UpdateUserRoleRequest(String role) {
this.role = role;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}

View File

@@ -13,9 +13,10 @@ spring.datasource.driver-class-name=org.postgresql.Driver
security.jwt.secret=ballistic-test-secret-key-1234567890-ABCDEFGHIJKLNMOPQRST
security.jwt.access-token-minutes=2880
# Temp disabling logging to find what I fucked up
spring.jpa.show-sql=false
logging.level.org.hibernate.SQL=warn
# Logging
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=INFO
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=warn