256 lines
9.4 KiB
Java
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);
|
|
}
|
|
|
|
}
|