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
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 }