UserService.java
package fr.tiogars.domaintemplate.domains.auth.user.services;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import fr.tiogars.domaintemplate.domains.auth.role.entities.RoleEntity;
import fr.tiogars.domaintemplate.domains.auth.role.services.RoleService;
import fr.tiogars.domaintemplate.domains.auth.user.entities.User;
import fr.tiogars.domaintemplate.domains.auth.user.entities.UserEntity;
import fr.tiogars.domaintemplate.domains.auth.user.models.CreateUserRequest;
import fr.tiogars.domaintemplate.domains.auth.user.models.UpdateUserRequest;
import fr.tiogars.domaintemplate.domains.auth.user.repositories.UserRepository;
/** Coordinates user account persistence and password hashing. */
@Service
public class UserService {
private final Clock clock;
private final PasswordEncoder passwordEncoder;
private final RoleService roleService;
private final UserRepository repository;
@Autowired
public UserService(UserRepository repository, RoleService roleService, PasswordEncoder passwordEncoder) {
this(repository, roleService, passwordEncoder, Clock.systemUTC());
}
UserService(UserRepository repository, RoleService roleService, PasswordEncoder passwordEncoder, Clock clock) {
this.repository = repository;
this.roleService = roleService;
this.passwordEncoder = passwordEncoder;
this.clock = clock;
}
@Transactional
public Optional<User> create(CreateUserRequest request) {
if (repository.findByUsername(request.username()).isPresent()) {
return Optional.empty();
}
Instant timestamp = currentTimestamp();
UserEntity entity = new UserEntity(
request.username(),
passwordEncoder.encode(request.password()),
request.enabled() == null || request.enabled(),
resolveRoles(request.roles()),
timestamp,
timestamp);
return Optional.of(toRecord(repository.save(entity)));
}
@Transactional
public Optional<User> update(Long id, UpdateUserRequest request) {
return repository.findById(id)
.filter(existing -> repository.findByUsername(request.username())
.map(found -> found == existing || Objects.equals(found.getId(), existing.getId()))
.orElse(true))
.map(existing -> {
String passwordHash = request.password() == null || request.password().isBlank()
? null
: passwordEncoder.encode(request.password());
existing.update(request.username(), passwordHash, request.enabled(), resolveRoles(request.roles()),
currentTimestamp());
return toRecord(repository.save(existing));
});
}
@Transactional
public boolean delete(Long id) {
if (!repository.existsById(id)) {
return false;
}
repository.deleteById(id);
return true;
}
public List<User> findAll() {
return repository.findAllByOrderByUsernameAsc().stream()
.map(UserService::toRecord)
.toList();
}
public Optional<User> findById(Long id) {
return repository.findById(id).map(UserService::toRecord);
}
public Optional<User> findByUsername(String username) {
return repository.findByUsername(username).map(UserService::toRecord);
}
@Transactional
public User forceAdminUser(String username, String password) {
RoleEntity adminRole = roleService.findEntityByCode(RoleService.ADMIN).orElseThrow();
Instant timestamp = currentTimestamp();
UserEntity entity = repository.findByUsername(username)
.map(existing -> {
existing.update(username, passwordEncoder.encode(password), true, Set.of(adminRole), timestamp);
return existing;
})
.orElseGet(() -> new UserEntity(
username,
passwordEncoder.encode(password),
true,
Set.of(adminRole),
timestamp,
timestamp));
return toRecord(repository.save(entity));
}
private Set<RoleEntity> resolveRoles(Set<String> roleCodes) {
Set<RoleEntity> roles = new LinkedHashSet<>();
for (String roleCode : roleCodes) {
roles.add(roleService.findEntityByCode(roleCode).orElseThrow(
() -> new IllegalArgumentException("Unknown role: " + roleCode)));
}
return roles;
}
private Instant currentTimestamp() {
return Instant.now(clock).truncatedTo(ChronoUnit.MICROS);
}
public static User toRecord(UserEntity entity) {
return new User(
entity.getId(),
entity.getUsername(),
entity.isEnabled(),
entity.getRoles().stream()
.sorted(Comparator.comparing(role -> role.getCode()))
.map(RoleService::toRecord)
.toList(),
entity.getCreatedDate(),
entity.getLastModifiedDate());
}
}