PasswordResetEmailService.java

/*
 * Copyright (C) 2025 B3Partners B.V.
 *
 * SPDX-License-Identifier: MIT
 */

package org.tailormap.api.service;

import java.util.Locale;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.lang.Nullable;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.tailormap.api.persistence.TemporaryToken;
import org.tailormap.api.persistence.User;
import org.tailormap.api.repository.TemporaryTokenRepository;
import org.tailormap.api.repository.UserRepository;

@Service
public class PasswordResetEmailService {

  private static final Logger logger = LoggerFactory.getLogger(PasswordResetEmailService.class);

  private final Optional<JavaMailSender> emailSender;
  private final UserRepository userRepository;
  private final TemporaryTokenRepository temporaryTokenRepository;
  private final MessageSource messageSource;

  @Value("${tailormap-api.mail.from}")
  private String mailFrom;

  @Autowired(required = false)
  public PasswordResetEmailService(
      @Nullable JavaMailSender emailSender,
      UserRepository userRepository,
      TemporaryTokenRepository temporaryTokenRepository,
      MessageSource messageSource) {
    this.emailSender = Optional.ofNullable(emailSender);
    this.userRepository = userRepository;
    this.temporaryTokenRepository = temporaryTokenRepository;
    this.messageSource = messageSource;
  }

  @Async("passwordResetTaskExecutor")
  @Transactional
  public void sendPasswordResetEmailAsync(
      String email, String absoluteLinkPrefix, Locale locale, int tokenExpiryMinutes) {
    try {
      if (emailSender.isEmpty()) {
        logger.warn("Cannot send password reset email: JavaMailSender is not configured");
        return;
      }

      User user = userRepository.findByEmail(email).orElse(null);
      if (user == null || !user.isEnabledAndValidUntil()) {
        return;
      }

      TemporaryToken token =
          new TemporaryToken(TemporaryToken.TokenType.PASSWORD_RESET, user.getUsername(), tokenExpiryMinutes);
      token = temporaryTokenRepository.save(token);

      String absoluteLink =
          absoluteLinkPrefix + "/user/password-reset/" + token.getCombinedTokenAndExpirationAsBase64();

      SimpleMailMessage message = new SimpleMailMessage();
      message.setFrom(mailFrom);
      message.setTo(user.getEmail());
      message.setSubject(messageSource.getMessage("reset-password-request.email-subject", null, locale));
      message.setText(
          messageSource.getMessage("reset-password-request.email-body", new Object[] {absoluteLink}, locale));

      logger.info("Sending password reset email for user: {}", user.getUsername());
      emailSender.ifPresent((sender) -> sender.send(message)); // blocking, but run in async thread
    } catch (Exception e) {
      logger.error("Failed to send password reset email", e);
    }
  }
}