View Javadoc
1   package fr.tiogars.domaintemplate.domains.auth.user.services;
2   
3   import java.time.Clock;
4   import java.time.Instant;
5   import java.time.temporal.ChronoUnit;
6   import java.util.Comparator;
7   import java.util.LinkedHashSet;
8   import java.util.List;
9   import java.util.Objects;
10  import java.util.Optional;
11  import java.util.Set;
12  
13  import org.springframework.beans.factory.annotation.Autowired;
14  import org.springframework.security.crypto.password.PasswordEncoder;
15  import org.springframework.stereotype.Service;
16  import org.springframework.transaction.annotation.Transactional;
17  
18  import fr.tiogars.domaintemplate.domains.auth.role.entities.RoleEntity;
19  import fr.tiogars.domaintemplate.domains.auth.role.services.RoleService;
20  import fr.tiogars.domaintemplate.domains.auth.user.entities.User;
21  import fr.tiogars.domaintemplate.domains.auth.user.entities.UserEntity;
22  import fr.tiogars.domaintemplate.domains.auth.user.models.CreateUserRequest;
23  import fr.tiogars.domaintemplate.domains.auth.user.models.UpdateUserRequest;
24  import fr.tiogars.domaintemplate.domains.auth.user.repositories.UserRepository;
25  
26  /** Coordinates user account persistence and password hashing. */
27  @Service
28  public class UserService {
29  
30      private final Clock clock;
31      private final PasswordEncoder passwordEncoder;
32      private final RoleService roleService;
33      private final UserRepository repository;
34  
35      @Autowired
36      public UserService(UserRepository repository, RoleService roleService, PasswordEncoder passwordEncoder) {
37          this(repository, roleService, passwordEncoder, Clock.systemUTC());
38      }
39  
40      UserService(UserRepository repository, RoleService roleService, PasswordEncoder passwordEncoder, Clock clock) {
41          this.repository = repository;
42          this.roleService = roleService;
43          this.passwordEncoder = passwordEncoder;
44          this.clock = clock;
45      }
46  
47      @Transactional
48      public Optional<User> create(CreateUserRequest request) {
49          if (repository.findByUsername(request.username()).isPresent()) {
50              return Optional.empty();
51          }
52          Instant timestamp = currentTimestamp();
53          UserEntity entity = new UserEntity(
54                  request.username(),
55                  passwordEncoder.encode(request.password()),
56                  request.enabled() == null || request.enabled(),
57                  resolveRoles(request.roles()),
58                  timestamp,
59                  timestamp);
60          return Optional.of(toRecord(repository.save(entity)));
61      }
62  
63      @Transactional
64      public Optional<User> update(Long id, UpdateUserRequest request) {
65          return repository.findById(id)
66                  .filter(existing -> repository.findByUsername(request.username())
67                          .map(found -> found == existing || Objects.equals(found.getId(), existing.getId()))
68                          .orElse(true))
69                  .map(existing -> {
70                      String passwordHash = request.password() == null || request.password().isBlank()
71                              ? null
72                              : passwordEncoder.encode(request.password());
73                      existing.update(request.username(), passwordHash, request.enabled(), resolveRoles(request.roles()),
74                              currentTimestamp());
75                      return toRecord(repository.save(existing));
76                  });
77      }
78  
79      @Transactional
80      public boolean delete(Long id) {
81          if (!repository.existsById(id)) {
82              return false;
83          }
84          repository.deleteById(id);
85          return true;
86      }
87  
88      public List<User> findAll() {
89          return repository.findAllByOrderByUsernameAsc().stream()
90                  .map(UserService::toRecord)
91                  .toList();
92      }
93  
94      public Optional<User> findById(Long id) {
95          return repository.findById(id).map(UserService::toRecord);
96      }
97  
98      public Optional<User> findByUsername(String username) {
99          return repository.findByUsername(username).map(UserService::toRecord);
100     }
101 
102     @Transactional
103     public User forceAdminUser(String username, String password) {
104         RoleEntity adminRole = roleService.findEntityByCode(RoleService.ADMIN).orElseThrow();
105         Instant timestamp = currentTimestamp();
106         UserEntity entity = repository.findByUsername(username)
107                 .map(existing -> {
108                     existing.update(username, passwordEncoder.encode(password), true, Set.of(adminRole), timestamp);
109                     return existing;
110                 })
111                 .orElseGet(() -> new UserEntity(
112                         username,
113                         passwordEncoder.encode(password),
114                         true,
115                         Set.of(adminRole),
116                         timestamp,
117                         timestamp));
118         return toRecord(repository.save(entity));
119     }
120 
121     private Set<RoleEntity> resolveRoles(Set<String> roleCodes) {
122         Set<RoleEntity> roles = new LinkedHashSet<>();
123         for (String roleCode : roleCodes) {
124             roles.add(roleService.findEntityByCode(roleCode).orElseThrow(
125                     () -> new IllegalArgumentException("Unknown role: " + roleCode)));
126         }
127         return roles;
128     }
129 
130     private Instant currentTimestamp() {
131         return Instant.now(clock).truncatedTo(ChronoUnit.MICROS);
132     }
133 
134     public static User toRecord(UserEntity entity) {
135         return new User(
136                 entity.getId(),
137                 entity.getUsername(),
138                 entity.isEnabled(),
139                 entity.getRoles().stream()
140                         .sorted(Comparator.comparing(role -> role.getCode()))
141                         .map(RoleService::toRecord)
142                         .toList(),
143                 entity.getCreatedDate(),
144                 entity.getLastModifiedDate());
145     }
146 }