Fix backend user contract and add User-Employee-Customer linkage

- Add active field to User entity and users table
- Add userId linkage to Employee and Customer entities with unique constraints and FKs
- Add repository methods findByUserId and findAllByEmail
- Create UserBusinessLinkageService for shared employee/customer creation logic
- Create AuthenticationHelper utility for resolving authenticated users
- Update UserService to persist all user fields and create linked business entities
- Update AuthController register to set active and create linked customer
- Update DataInitializer to be idempotent and use shared linkage service
- Update Postman collection user endpoints with fullName, email, and active
This commit is contained in:
2026-03-08 21:56:04 -06:00
parent d86652b462
commit a0d14e493f
12 changed files with 363 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ package com.petshop.backend.config;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.UserBusinessLinkageService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@@ -11,57 +12,137 @@ public class DataInitializer implements CommandLineRunner {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserBusinessLinkageService userBusinessLinkageService;
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) {
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.userBusinessLinkageService = userBusinessLinkageService;
}
@Override
public void run(String... args) {
System.out.println("==== DataInitializer: Starting user creation ====");
if (userRepository.findByUsername("admin").isEmpty()) {
User admin = userRepository.findByUsername("admin").orElse(null);
if (admin == null) {
System.out.println("Creating admin user...");
User admin = new User();
admin = new User();
admin.setUsername("admin");
admin.setPassword(passwordEncoder.encode("admin123"));
admin.setEmail("admin@petshop.com");
admin.setFullName("Admin User");
admin.setRole(User.Role.ADMIN);
userRepository.save(admin);
admin.setActive(true);
admin = userRepository.save(admin);
System.out.println("Admin user created successfully");
} else {
System.out.println("Admin user already exists");
// Normalize missing fields if needed
boolean updated = false;
if (admin.getFullName() == null || admin.getFullName().isEmpty()) {
admin.setFullName("Admin User");
updated = true;
}
if (admin.getEmail() == null || admin.getEmail().isEmpty()) {
admin.setEmail("admin@petshop.com");
updated = true;
}
if (admin.getActive() == null) {
admin.setActive(true);
updated = true;
}
if (admin.getRole() == null) {
admin.setRole(User.Role.ADMIN);
updated = true;
}
if (updated) {
admin = userRepository.save(admin);
System.out.println("Admin user normalized");
}
}
// Ensure linked employee
userBusinessLinkageService.ensureLinkedEmployee(admin);
if (userRepository.findByUsername("staff").isEmpty()) {
User staff = userRepository.findByUsername("staff").orElse(null);
if (staff == null) {
System.out.println("Creating staff user...");
User staff = new User();
staff = new User();
staff.setUsername("staff");
staff.setPassword(passwordEncoder.encode("staff123"));
staff.setEmail("staff@petshop.com");
staff.setFullName("Staff User");
staff.setRole(User.Role.STAFF);
userRepository.save(staff);
staff.setActive(true);
staff = userRepository.save(staff);
System.out.println("Staff user created successfully");
} else {
System.out.println("Staff user already exists");
// Normalize missing fields if needed
boolean updated = false;
if (staff.getFullName() == null || staff.getFullName().isEmpty()) {
staff.setFullName("Staff User");
updated = true;
}
if (staff.getEmail() == null || staff.getEmail().isEmpty()) {
staff.setEmail("staff@petshop.com");
updated = true;
}
if (staff.getActive() == null) {
staff.setActive(true);
updated = true;
}
if (staff.getRole() == null) {
staff.setRole(User.Role.STAFF);
updated = true;
}
if (updated) {
staff = userRepository.save(staff);
System.out.println("Staff user normalized");
}
}
// Ensure linked employee
userBusinessLinkageService.ensureLinkedEmployee(staff);
if (userRepository.findByUsername("customer").isEmpty()) {
User customer = userRepository.findByUsername("customer").orElse(null);
if (customer == null) {
System.out.println("Creating customer user...");
User customer = new User();
customer = new User();
customer.setUsername("customer");
customer.setPassword(passwordEncoder.encode("customer123"));
customer.setEmail("customer@petshop.com");
customer.setFullName("Test Customer");
customer.setRole(User.Role.CUSTOMER);
userRepository.save(customer);
customer.setActive(true);
customer = userRepository.save(customer);
System.out.println("Customer user created successfully");
} else {
System.out.println("Customer user already exists");
// Normalize missing fields if needed
boolean updated = false;
if (customer.getFullName() == null || customer.getFullName().isEmpty()) {
customer.setFullName("Test Customer");
updated = true;
}
if (customer.getEmail() == null || customer.getEmail().isEmpty()) {
customer.setEmail("customer@petshop.com");
updated = true;
}
if (customer.getActive() == null) {
customer.setActive(true);
updated = true;
}
if (customer.getRole() == null) {
customer.setRole(User.Role.CUSTOMER);
updated = true;
}
if (updated) {
customer = userRepository.save(customer);
System.out.println("Customer user normalized");
}
}
// Ensure linked customer
userBusinessLinkageService.ensureLinkedCustomer(customer);
System.out.println("==== DataInitializer: Completed ====");
}

View File

@@ -10,6 +10,7 @@ import com.petshop.backend.dto.auth.UserInfoResponse;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.service.UserBusinessLinkageService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -42,12 +43,14 @@ public class AuthController {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;
private final UserBusinessLinkageService userBusinessLinkageService;
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder) {
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
this.passwordEncoder = passwordEncoder;
this.userBusinessLinkageService = userBusinessLinkageService;
}
@PostMapping("/register")
@@ -70,9 +73,13 @@ public class AuthController {
user.setEmail(request.getEmail());
user.setFullName(request.getFullName());
user.setRole(User.Role.CUSTOMER);
user.setActive(true);
User savedUser = userRepository.save(user);
// Create or link customer record
userBusinessLinkageService.ensureLinkedCustomer(savedUser);
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
savedUser.getUsername(),
savedUser.getPassword(),

View File

@@ -15,6 +15,9 @@ public class Customer {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long customerId;
@Column(name = "user_id")
private Long userId;
@Column(nullable = false, length = 50)
private String firstName;
@@ -38,8 +41,9 @@ public class Customer {
public Customer() {
}
public Customer(Long customerId, String firstName, String lastName, String email, String phone, LocalDateTime createdAt, LocalDateTime updatedAt) {
public Customer(Long customerId, Long userId, String firstName, String lastName, String email, String phone, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.customerId = customerId;
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
@@ -56,6 +60,14 @@ public class Customer {
this.customerId = customerId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getFirstName() {
return firstName;
}
@@ -121,6 +133,7 @@ public class Customer {
public String toString() {
return "Customer{" +
"customerId=" + customerId +
", userId=" + userId +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +

View File

@@ -15,6 +15,9 @@ public class Employee {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long employeeId;
@Column(name = "user_id")
private Long userId;
@Column(nullable = false, length = 50)
private String firstName;
@@ -44,8 +47,9 @@ public class Employee {
public Employee() {
}
public Employee(Long employeeId, String firstName, String lastName, String email, String phone, String role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) {
public Employee(Long employeeId, Long userId, String firstName, String lastName, String email, String phone, String role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.employeeId = employeeId;
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
@@ -64,6 +68,14 @@ public class Employee {
this.employeeId = employeeId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getFirstName() {
return firstName;
}
@@ -145,6 +157,7 @@ public class Employee {
public String toString() {
return "Employee{" +
"employeeId=" + employeeId +
", userId=" + userId +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +

View File

@@ -34,6 +34,9 @@ public class User {
@Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
private Role role;
@Column(nullable = false)
private Boolean active = true;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@@ -49,7 +52,7 @@ public class User {
public User() {
}
public User(Long id, String username, String password, String email, String fullName, String avatarUrl, Role role, LocalDateTime createdAt, LocalDateTime updatedAt) {
public User(Long id, String username, String password, String email, String fullName, String avatarUrl, Role role, Boolean active, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.username = username;
this.password = password;
@@ -57,6 +60,7 @@ public class User {
this.fullName = fullName;
this.avatarUrl = avatarUrl;
this.role = role;
this.active = active;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
@@ -117,6 +121,14 @@ public class User {
this.role = role;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
@@ -156,6 +168,7 @@ public class User {
", fullName='" + fullName + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", role=" + role +
", active=" + active +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';

View File

@@ -8,9 +8,15 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByUserId(Long userId);
List<Customer> findAllByEmail(String email);
@Query("SELECT c FROM Customer c WHERE " +
"LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(c.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +

View File

@@ -4,6 +4,11 @@ import com.petshop.backend.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Optional<Employee> findByUserId(Long userId);
List<Employee> findAllByEmail(String email);
}

View File

@@ -0,0 +1,138 @@
package com.petshop.backend.service;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserBusinessLinkageService {
private final EmployeeRepository employeeRepository;
private final CustomerRepository customerRepository;
@Autowired
public UserBusinessLinkageService(EmployeeRepository employeeRepository, CustomerRepository customerRepository) {
this.employeeRepository = employeeRepository;
this.customerRepository = customerRepository;
}
@Transactional
public Employee ensureLinkedEmployee(User user) {
// Check if already linked
if (user.getId() != null) {
var existing = employeeRepository.findByUserId(user.getId());
if (existing.isPresent()) {
return existing.get();
}
}
// Check for email matches
List<Employee> emailMatches = employeeRepository.findAllByEmail(user.getEmail());
// If exactly one match exists and has no userId, link it
if (emailMatches.size() == 1) {
Employee employee = emailMatches.get(0);
if (employee.getUserId() == null) {
employee.setUserId(user.getId());
return employeeRepository.save(employee);
}
}
// Otherwise create a new linked Employee
Employee newEmployee = new Employee();
newEmployee.setUserId(user.getId());
newEmployee.setEmail(user.getEmail());
// Split fullName into firstName and lastName
String[] nameParts = splitFullName(user.getFullName());
newEmployee.setFirstName(nameParts[0]);
newEmployee.setLastName(nameParts[1]);
// Set required fields with deterministic values
newEmployee.setPhone("000-000-0000");
newEmployee.setIsActive(true);
// Map role based on user role
if (user.getRole() == User.Role.ADMIN) {
newEmployee.setRole("Manager");
} else if (user.getRole() == User.Role.STAFF) {
newEmployee.setRole("Staff");
} else {
newEmployee.setRole("Staff"); // fallback
}
return employeeRepository.save(newEmployee);
}
@Transactional
public Customer ensureLinkedCustomer(User user) {
// Check if already linked
if (user.getId() != null) {
var existing = customerRepository.findByUserId(user.getId());
if (existing.isPresent()) {
return existing.get();
}
}
// Check for email matches
List<Customer> emailMatches = customerRepository.findAllByEmail(user.getEmail());
// If exactly one match exists and has no userId, link it
if (emailMatches.size() == 1) {
Customer customer = emailMatches.get(0);
if (customer.getUserId() == null) {
customer.setUserId(user.getId());
return customerRepository.save(customer);
}
}
// Otherwise create a new linked Customer
Customer newCustomer = new Customer();
newCustomer.setUserId(user.getId());
newCustomer.setEmail(user.getEmail());
// Split fullName into firstName and lastName
String[] nameParts = splitFullName(user.getFullName());
newCustomer.setFirstName(nameParts[0]);
newCustomer.setLastName(nameParts[1]);
// Set required fields with deterministic values
newCustomer.setPhone("000-000-0001");
return customerRepository.save(newCustomer);
}
private String[] splitFullName(String fullName) {
if (fullName == null || fullName.trim().isEmpty()) {
return new String[]{"System", "User"};
}
String trimmed = fullName.trim();
int spaceIndex = trimmed.indexOf(' ');
if (spaceIndex == -1) {
// Single token
return new String[]{trimmed, "User"};
}
// Multiple tokens
String firstName = trimmed.substring(0, spaceIndex).trim();
String lastName = trimmed.substring(spaceIndex + 1).trim();
if (firstName.isEmpty()) {
firstName = "System";
}
if (lastName.isEmpty()) {
lastName = "User";
}
return new String[]{firstName, lastName};
}
}

View File

@@ -17,10 +17,12 @@ public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserBusinessLinkageService userBusinessLinkageService;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.userBusinessLinkageService = userBusinessLinkageService;
}
public Page<UserResponse> getAllUsers(String query, Pageable pageable) {
@@ -44,9 +46,20 @@ public class UserService {
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
user.setRole(request.getRole());
user.setActive(request.getActive() != null ? request.getActive() : true);
user = userRepository.save(user);
// Create or link business entity based on role
if (user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) {
userBusinessLinkageService.ensureLinkedEmployee(user);
} else if (user.getRole() == User.Role.CUSTOMER) {
userBusinessLinkageService.ensureLinkedCustomer(user);
}
return mapToResponse(user);
}
@@ -59,7 +72,10 @@ public class UserService {
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
}
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
user.setRole(request.getRole());
user.setActive(request.getActive() != null ? request.getActive() : true);
user = userRepository.save(user);
return mapToResponse(user);
@@ -82,7 +98,12 @@ public class UserService {
UserResponse response = new UserResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
response.setFullName(user.getFullName());
response.setEmail(user.getEmail());
response.setRole(user.getRole().toString());
response.setActive(user.getActive());
response.setCreatedAt(user.getCreatedAt());
response.setUpdatedAt(user.getUpdatedAt());
return response;
}
}

View File

@@ -0,0 +1,38 @@
package com.petshop.backend.util;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.UserRepository;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
public class AuthenticationHelper {
public static User getAuthenticatedUser(UserRepository userRepository) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new RuntimeException("No authenticated user found");
}
String username = authentication.getName();
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found: " + username));
}
public static Employee getAuthenticatedEmployee(UserRepository userRepository, EmployeeRepository employeeRepository) {
User user = getAuthenticatedUser(userRepository);
return employeeRepository.findByUserId(user.getId())
.orElseThrow(() -> new RuntimeException("Employee record not found for user: " + user.getUsername()));
}
public static Customer getAuthenticatedCustomer(UserRepository userRepository, CustomerRepository customerRepository) {
User user = getAuthenticatedUser(userRepository);
return customerRepository.findByUserId(user.getId())
.orElseThrow(() -> new RuntimeException("Customer record not found for user: " + user.getUsername()));
}
}

View File

@@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS storeLocation (
CREATE TABLE IF NOT EXISTS employee (
employeeId BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NULL,
firstName VARCHAR(50) NOT NULL,
lastName VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
@@ -19,7 +20,8 @@ CREATE TABLE IF NOT EXISTS employee (
role VARCHAR(50) NOT NULL,
isActive BOOLEAN DEFAULT TRUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uk_employee_user_id UNIQUE (user_id)
);
CREATE TABLE IF NOT EXISTS employeeStore (
@@ -32,12 +34,14 @@ CREATE TABLE IF NOT EXISTS employeeStore (
CREATE TABLE IF NOT EXISTS customer (
customerId BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NULL,
firstName VARCHAR(50) NOT NULL,
lastName VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uk_customer_user_id UNIQUE (user_id)
);
CREATE TABLE IF NOT EXISTS pet (
@@ -201,6 +205,7 @@ CREATE TABLE IF NOT EXISTS users (
fullName VARCHAR(100),
avatarUrl VARCHAR(255),
role VARCHAR(20) NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
@@ -239,3 +244,7 @@ CREATE TABLE IF NOT EXISTS message (
FOREIGN KEY (conversationId) REFERENCES conversation(id),
FOREIGN KEY (senderId) REFERENCES users(id)
);
-- Add foreign keys for user_id linkage
ALTER TABLE employee ADD CONSTRAINT fk_employee_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE customer ADD CONSTRAINT fk_customer_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;