Tools/src/main/java/de/ph87/tools/user/UserService.java
2024-11-07 14:32:10 +01:00

256 lines
9.4 KiB
Java

package de.ph87.tools.user;
import de.ph87.tools.common.uuid.AbstractUuid;
import de.ph87.tools.email.EmailService;
import de.ph87.tools.group.GroupMapper;
import de.ph87.tools.group.GroupRepository;
import de.ph87.tools.user.push.UserPushService;
import de.ph87.tools.user.requests.UserLoginRequest;
import de.ph87.tools.user.uuid.UserPrivateUuid;
import de.ph87.tools.user.uuid.UserPublicUuid;
import jakarta.annotation.Nullable;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
import java.util.Random;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import static de.ph87.tools.common.EmailHelper.isEmailValid;
import static de.ph87.tools.user.uuid.UserPrivateUuidArgumentResolver.USER_UUID_COOKIE_NAME;
@Slf4j
@Service
@Transactional
@EnableScheduling
@RequiredArgsConstructor
public class UserService {
private static final String USER_NAME_DEFAULT_BASE = "Neuer Benutzer";
private static final int NAME_MIN_LENGTH = 2;
private static final Pattern NAME_NEGATIVE_REGEX = Pattern.compile("\\s+|^[^a-zA-Z0-9]+$");
private static final int PASSWORD_MIN_LENGTH = 10;
private static final Pattern PASSWORD_NEGATIVE_REGEX = Pattern.compile("^[a-zA-Z]+$|^[0-9]+$");
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final GroupRepository groupRepository;
private final UserAccessService userAccessService;
private final GroupMapper groupMapper;
private final UserPushService userPushService;
private final EmailService emailService;
@Nullable
public UserPrivateDto whoAmI(@Nullable final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
if (privateUuid == null) {
return null;
}
final User user = userRepository.findByPrivateUuid(privateUuid.uuid).orElse(null);
writeUserUuidCookie(response, user);
return UserPrivateDto.orNull(user);
}
@NonNull
public User getUserByPrivateUuidOrElseCreate(@Nullable final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
final User user = Optional
.ofNullable(privateUuid)
.map(AbstractUuid::getUuid)
.map(userRepository::findByPrivateUuid)
.filter(Optional::isPresent)
.map(Optional::get)
.orElseGet(this::createUnchecked);
writeUserUuidCookie(response, user);
return user;
}
@NonNull
public UserPrivateDto login(@NonNull final UserLoginRequest loginRequest, @NonNull final HttpServletResponse response) {
final Optional<User> userOptional = userRepository.findByName(loginRequest.username);
if (userOptional.isEmpty()) {
log.warn("Login failed: Unknown user: username={}", loginRequest.username);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
final User user = userOptional.get();
if (user.getPassword().isEmpty()) {
log.warn("Login failed: No password set: user={}", user);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
if (!user.verifyPassword(passwordEncoder, loginRequest.password)) {
log.warn("Login failed: Wrong password: user={}", user);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
user.touch();
writeUserUuidCookie(response, user);
log.info("Login successful: user={}", user);
return new UserPrivateDto(user);
}
public void logout(@NonNull final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
modify(privateUuid, user -> {
writeUserUuidCookie(response, null);
log.info("Logout successful: user={}", user);
});
}
@NonNull
public UserPrivateDto changeName(@NonNull final UserPrivateUuid privateUuid, @NonNull final String name) {
return modify(privateUuid, user -> {
if (user.getName().equals(name)) {
return;
}
final User duplicate = userRepository.findByName(name).orElse(null);
if (duplicate != null) {
log.warn("Cannot change User name because of duplicate: name={}, user={}, duplicate={}", name, user, duplicate);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
if (name.length() < NAME_MIN_LENGTH) {
log.warn("Cannot change User name: too short: length={}/{}, user={}", name.length(), NAME_MIN_LENGTH, user);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
if (NAME_NEGATIVE_REGEX.matcher(name).find()) {
log.warn("Cannot change User name: Matches negative regex: name={}, user={}", name, user);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
user.setName(name);
log.info("User name changed: user={}", user);
groupRepository.findAllByUsersContains(user).forEach(groupMapper::push);
});
}
@NonNull
public UserPrivateDto changePassword(@NonNull final UserPrivateUuid privateUuid, @NonNull final String password) {
return modify(privateUuid, user -> {
if (password.length() < PASSWORD_MIN_LENGTH) {
log.warn("Cannot change User password: too short: length={}/{}, user={}", password.length(), PASSWORD_MIN_LENGTH, user);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
if (PASSWORD_NEGATIVE_REGEX.matcher(password).find()) {
log.warn("Cannot change User password: Matches negative regex: user={}", user);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
user.setPassword(passwordEncoder, password);
log.info("User password changed: user={}", user);
});
}
@NonNull
public UserPrivateDto changeEmail(@NonNull final UserPrivateUuid privateUuid, @NonNull final String email) {
return modify(privateUuid, user -> {
if (!isEmailValid(email)) {
log.warn("Cannot change User email: not valid, user={}", user);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
if (user.getEmail().equals(email)) {
return;
}
user.setEmail(email);
log.info("User email changed: user={}", user);
emailService.queueEmailConfirmationEmail(user);
});
}
public boolean confirmEmail(@NonNull final String emailConfirmation) {
final User user = userRepository.findByEmailConfirmation(emailConfirmation).orElse(null);
if (user == null) {
log.warn("Failed to confirm email: emailConfirmation={}", emailConfirmation);
return false;
}
if (!user.isEmailConfirmed()) {
user.setEmailConfirmed(true);
log.info("User email confirmed: user={}", user);
push(user);
}
return true;
}
public void delete(@NonNull final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
final User user = userRepository.findByPrivateUuid(privateUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
deleteUnchecked(response, user);
}
/* CREATE, MODIFY, DELETE ----------------------------------------------------------------------- */
@NonNull
private User createUnchecked() {
final String name = generateFreeUserName();
final User user = userRepository.save(new User(name));
log.info("User CREATED: {}", user);
return user;
}
@NonNull
private String generateFreeUserName() {
int index = new Random().nextInt(1234, 5723);
String name = "%s #%d".formatted(USER_NAME_DEFAULT_BASE, index);
while (userRepository.existsByName(name)) {
index++;
name = "%s #%d".formatted(USER_NAME_DEFAULT_BASE, index);
}
return name;
}
@NonNull
private UserPrivateDto modify(@NonNull final UserPrivateUuid privateUuid, @NonNull Consumer<User> modifier) {
final User user = userAccessService.access(privateUuid);
modifier.accept(user);
return push(user);
}
private @NonNull UserPrivateDto push(@NonNull final User user) {
final UserPrivateDto privateDto = new UserPrivateDto(user);
userPushService.push(user, privateDto);
return privateDto;
}
private void deleteUnchecked(@NonNull final HttpServletResponse response, @NonNull final User user) {
userRepository.delete(user);
log.info("User DELETED: {}", user);
writeUserUuidCookie(response, null);
}
/* GETTERS & FINDERS ---------------------------------------------------------------------------- */
@NonNull
public UserPublicDto getDtoByPublicUuid(@NonNull final UserPublicUuid publicUuid) {
return new UserPublicDto(getByPublicUuid(publicUuid));
}
@NonNull
public User getByPublicUuid(@NonNull final UserPublicUuid publicUuid) {
return userRepository.findByPublicUuid(publicUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
/* COOKIES -------------------------------------------------------------------------------------- */
private static void writeUserUuidCookie(@NonNull final HttpServletResponse response, @Nullable final User user) {
final Cookie cookie = new Cookie(USER_UUID_COOKIE_NAME, "");
if (user != null) {
cookie.setValue(user.getPrivateUuid().uuid);
}
cookie.setMaxAge(PASSWORD_MIN_LENGTH * 365 * 24 * 60 * 60);
cookie.setPath("/");
response.addCookie(cookie);
}
}