Tools/src/main/java/de/ph87/tools/user/UserService.java

201 lines
7.5 KiB
Java

package de.ph87.tools.user;
import de.ph87.tools.common.uuid.AbstractUuid;
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.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;
@NonNull
public UserPrivateDto login(@NonNull final UserLoginRequest loginRequest, @NonNull final HttpServletResponse response) {
final User user = userRepository.findByName(loginRequest.name).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
if (passwordEncoder.matches(loginRequest.password, user.getPassword())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
user.touch();
writeUserUuidCookie(response, user);
return new UserPrivateDto(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;
}
@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 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);
});
}
@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);
});
}
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 = access(privateUuid);
modifier.accept(user);
return new UserPrivateDto(user);
}
private void deleteUnchecked(final HttpServletResponse response, final User user) {
userRepository.delete(user);
log.info("User DELETED: {}", user);
writeUserUuidCookie(response, null);
}
/* ACCESS --------------------------------------------------------------------------------------- */
@Nullable
public User accessOrNull(@Nullable final UserPrivateUuid userPrivateUuid) {
if (userPrivateUuid == null) {
return null;
}
return access(userPrivateUuid);
}
/* GETTERS & FINDERS ---------------------------------------------------------------------------- */
@NonNull
public UserPublicDto getDtoByPublicUuid(@NonNull final UserPublicUuid publicUuid) {
return new UserPublicDto(getByPublicUuid(publicUuid));
}
@NonNull
public User access(@NonNull final UserPrivateUuid privateUuid) {
return userRepository.findByPrivateUuid(privateUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
@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);
}
}