-
Notifications
You must be signed in to change notification settings - Fork 4
HTM-1820: Fix blocking when sending a password-reset email #1534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /* | ||
| * Copyright (C) 2025 B3Partners B.V. | ||
| * | ||
| * SPDX-License-Identifier: MIT | ||
| */ | ||
|
|
||
| package org.tailormap.api.configuration; | ||
|
|
||
| import java.util.concurrent.Executor; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.scheduling.annotation.EnableAsync; | ||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||
|
|
||
| @Configuration | ||
| @EnableAsync | ||
| public class AsyncConfig { | ||
|
|
||
| @Bean(name = "passwordResetTaskExecutor") | ||
| public Executor passwordResetTaskExecutor() { | ||
| ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); | ||
| executor.setCorePoolSize(1); | ||
| executor.setMaxPoolSize(1); | ||
| executor.setQueueCapacity(10); | ||
| executor.setThreadNamePrefix("pwd-reset-"); | ||
| executor.initialize(); | ||
| return executor; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| /* | ||
| * Copyright (C) 2025 B3Partners B.V. | ||
| * | ||
| * SPDX-License-Identifier: MIT | ||
| */ | ||
|
|
||
| package org.tailormap.api.service; | ||
|
|
||
| import java.util.Locale; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.MessageSource; | ||
| import org.springframework.mail.SimpleMailMessage; | ||
| import org.springframework.mail.javamail.JavaMailSender; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Service; | ||
| 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 JavaMailSender emailSender; | ||
| private final UserRepository userRepository; | ||
| private final TemporaryTokenRepository temporaryTokenRepository; | ||
| private final MessageSource messageSource; | ||
|
|
||
| @Value("${tailormap-api.mail.from}") | ||
| private String mailFrom; | ||
|
|
||
| public PasswordResetEmailService( | ||
| JavaMailSender emailSender, | ||
| UserRepository userRepository, | ||
| TemporaryTokenRepository temporaryTokenRepository, | ||
| MessageSource messageSource) { | ||
| this.emailSender = emailSender; | ||
| this.userRepository = userRepository; | ||
| this.temporaryTokenRepository = temporaryTokenRepository; | ||
| this.messageSource = messageSource; | ||
| } | ||
|
|
||
| @Async("passwordResetTaskExecutor") | ||
| public void sendPasswordResetEmailAsync( | ||
| String email, String absoluteLinkPrefix, Locale locale, int tokenExpiryMinutes) { | ||
| try { | ||
| 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.send(message); // blocking, but run in async thread | ||
| } catch (Exception e) { | ||
| logger.error("Failed to send password reset email", e); | ||
| } | ||
|
Comment on lines
+48
to
+74
|
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AsyncConfig should configure setWaitForTasksToCompleteOnShutdown(true) and setAwaitTerminationSeconds to ensure graceful shutdown. Without this, pending password reset emails might not be sent when the application shuts down, potentially leaving users without the ability to reset their passwords. This is especially important given the queue capacity of 10.