package se.bilhalsning.service; import java.time.Instant; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import se.bilhalsning.entity.EmailChangeToken; import se.bilhalsning.entity.User; import se.bilhalsning.exception.EmailChangeTokenInvalidException; import se.bilhalsning.repository.EmailChangeTokenRepository; @Service @RequiredArgsConstructor public class EmailChangeService { private static final long TOKEN_TTL_HOURS = 24; private final UserService userService; private final EmailChangeTokenRepository tokenRepository; private final EmailService emailService; private final PasswordResetService passwordResetService; @Value("${app.public-base-url:http://localhost:3000}") private String publicBaseUrl; @Value("${app.email-change.expose-token:false}") private boolean exposeToken; @Transactional public Optional requestChange(String currentEmail, String password, String newEmail) { User user = userService.authenticate(currentEmail, password); userService.validateEmailAvailableForChange(user, newEmail); String normalizedEmail = newEmail.toLowerCase().trim(); tokenRepository.deleteUnusedByUserId(user.getId()); String rawToken = passwordResetService.generateRawToken(); EmailChangeToken entity = new EmailChangeToken(); entity.setUser(user); entity.setNewEmail(normalizedEmail); entity.setTokenHash(PasswordResetService.hashToken(rawToken)); entity.setExpiresAt(Instant.now().plusSeconds(TOKEN_TTL_HOURS * 3600)); tokenRepository.save(entity); String confirmUrl = publicBaseUrl.replaceAll("/$", "") + "/bekrafta-epost?token=" + rawToken; emailService.sendEmailChangeConfirmation(normalizedEmail, confirmUrl); return exposeToken ? Optional.of(rawToken) : Optional.empty(); } @Transactional public User confirmChange(String rawToken, String password) { EmailChangeToken token = tokenRepository .findByTokenHashAndUsedAtIsNull(PasswordResetService.hashToken(rawToken)) .filter(t -> t.getExpiresAt().isAfter(Instant.now())) .orElseThrow(EmailChangeTokenInvalidException::new); User user = token.getUser(); userService.authenticate(user.getEmail(), password); User updated = userService.applyEmailChange(user, token.getNewEmail()); token.setUsedAt(Instant.now()); tokenRepository.deleteUnusedByUserId(user.getId()); tokenRepository.save(token); return updated; } }