EMailVerificationServiceImpl.java
/*
* Copyright 2017 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.server.service.impl;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.blocks.security.UserException;
import org.genesys.blocks.security.service.PasswordPolicy.PasswordPolicyException;
import org.genesys.server.component.security.AsAdminInvoker;
import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.model.UserRole;
import org.genesys.server.model.impl.User;
import org.genesys.server.model.impl.VerificationToken;
import org.genesys.server.service.ArticleService;
import org.genesys.server.service.ArticleTranslationService;
import org.genesys.server.service.ContentService;
import org.genesys.server.service.EMailService;
import org.genesys.server.service.EMailVerificationService;
import org.genesys.server.service.TokenVerificationService;
import org.genesys.server.service.TokenVerificationService.NoSuchVerificationTokenException;
import org.genesys.server.service.TokenVerificationService.TokenExpiredException;
import org.genesys.server.service.UserService;
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.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
public class EMailVerificationServiceImpl implements EMailVerificationService {
private static final Logger LOG = LoggerFactory.getLogger(EMailVerificationServiceImpl.class);
@Autowired
private TokenVerificationService tokenVerificationService;
@Autowired
private EMailService emailService;
@Autowired
private UserService userService;
@Autowired
private ContentService contentService;
@Autowired
private ArticleService articleService;
@Autowired
private AsAdminInvoker asAdminInvoker;
@Value("${frontend.url}")
private String frontendUrl;
@Value("${mail.user.from}")
private String defaultEmailFrom;
@Override
@Transactional
public void sendVerificationEmail(User user) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("email-verification", user.getUuid());
ArticleTranslationService.TranslatedArticle article = articleService.getGlobalArticle(ContentService.SMTP_EMAIL_VERIFICATION, Locale.ENGLISH);
if (article != null) {
final String mailSubject = article.getTranslation() != null ? article.getTranslation().getTitle() : article.getEntity().getTitle();
var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
final String mailBody = MessageFormat.format(body, frontendUrl, verificationToken.getUuid(), user.getEmail(), verificationToken.getKey());
emailService.sendMail(mailSubject, mailBody, user.getEmail());
} else {
LOG.warn("smtp.email-verification article not found. Not sending verification email");
}
}
@Override
@Transactional
public void sendPasswordResetEmail(User user) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("email-password", user.getUuid());
ArticleTranslationService.TranslatedArticle article = articleService.getGlobalArticle(ContentService.SMTP_EMAIL_PASSWORD, Locale.ENGLISH);
final String mailSubject = article.getTranslation() != null ? article.getTranslation().getTitle() : article.getEntity().getTitle();
var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
final String mailBody = MessageFormat.format(body, frontendUrl, verificationToken.getUuid(), user.getEmail(), verificationToken.getKey());
emailService.sendMail(mailSubject, mailBody, user.getEmail());
}
@Override
@Transactional
public void cancelPasswordReset(String tokenUuid) throws InvalidApiUsageException {
try {
tokenVerificationService.cancel(tokenUuid);
} catch (NoSuchVerificationTokenException e) {
LOG.warn("No such token. Error message {}", e.getMessage());
throw new InvalidApiUsageException("No such verification token");
}
}
/**
* User registration has been canceled. Remove user data if user not yet validated.
*/
@Override
@Transactional
public void cancelValidation(String tokenUuid) throws Exception {
try {
VerificationToken verificationToken = tokenVerificationService.fetchToken("email-verification", tokenUuid);
final User user = userService.getUser(UUID.fromString(verificationToken.getData()));
if (user.hasRole(UserRole.VALIDATEDUSER.getName())) {
throw new InvalidApiUsageException("User already validated");
}
asAdminInvoker.invoke(() -> {
userService.deleteUser(user);
return true;
});
tokenVerificationService.cancel(tokenUuid);
} catch (final NoSuchVerificationTokenException e) {
LOG.warn("No such token. Error message {}", e.getMessage());
throw new InvalidApiUsageException("No such verification token");
}
}
@Override
@Transactional
public void validateEMail(String tokenUuid, String key) throws NoSuchVerificationTokenException, TokenExpiredException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("email-verification", tokenUuid, key);
userService.userEmailValidated(UUID.fromString(consumedToken.getData()));
}
/**
* The implementation will switch the security context to the user, an set the
* new password
*/
@Override
@Transactional(rollbackFor = Throwable.class)
public void changePassword(final String tokenUuid, final String key, final String password) throws NoSuchVerificationTokenException, PasswordPolicyException,
TokenExpiredException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("email-password", tokenUuid, key);
final User user = userService.getUser(UUID.fromString(consumedToken.getData()));
user.setRuntimeAuthorities(List.of(UserRole.USER)); // getUser does not set runtime authorities, resulting in an exception
Authentication prevAuth = SecurityContextHolder.getContext().getAuthentication();
try {
LOG.warn("Setting temporary authorization for password reset for {}", user.getEmail());
final UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
userService.changePassword(user, password);
} finally {
LOG.warn("Restoring authorization away from {}", user.getEmail());
SecurityContextHolder.getContext().setAuthentication(prevAuth);
}
}
@Override
@Transactional
public void requestDeleteAccount(User user) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("delete-account", user.getUuid());
ArticleTranslationService.TranslatedArticle article = articleService.getGlobalArticle(ContentService.SMTP_DELETE_ACCOUNT, Locale.ENGLISH);
var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
final String mailBody = MessageFormat.format(body, frontendUrl, verificationToken.getUuid(), verificationToken.getKey(), user.getFullName(), user.getEmail());
var title = article.getTranslation() != null ? article.getTranslation().getTitle() : article.getEntity().getTitle();
emailService.sendMail(title, mailBody, user.getEmail());
}
@Override
@Transactional(rollbackFor = Throwable.class)
public void confirmDeleteAccount(String tokenUuid, String key) throws NoSuchVerificationTokenException, TokenExpiredException, UserException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("delete-account", tokenUuid, key);
String uuid = consumedToken.getData();
User currentUser = SecurityContextUtil.getCurrentUser();
if (currentUser.getUuid().equals(uuid)) {
userService.disableMyAccount();
ArticleTranslationService.TranslatedArticle article = articleService.getGlobalArticle(ContentService.SMTP_DELETE_ACCOUNT_INPROGRESS, Locale.ENGLISH);
var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
final String mailBody = MessageFormat.format(body, frontendUrl, currentUser.getFullName(), currentUser.getEmail());
var title = article.getTranslation() != null ? article.getTranslation().getTitle() : article.getEntity().getTitle();
emailService.sendMail(title, mailBody, defaultEmailFrom, currentUser.getEmail());
} else {
throw new NoSuchVerificationTokenException();
}
}
@Override
@Transactional
public void sendDeleteAccountEmail(User user) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("archive-account", user.getUuid());
ArticleTranslationService.TranslatedArticle article = articleService.getGlobalArticle(ContentService.USER_ARCHIVE_ACCOUNT, Locale.ENGLISH);
Map<String, Object> root = new HashMap<>();
root.put("frontendUrl", frontendUrl);
root.put("tokenUuid", verificationToken.getUuid());
root.put("tokenKey", verificationToken.getKey());
root.put("user", user);
var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
var title = article.getTranslation() != null ? article.getTranslation().getTitle() : article.getEntity().getTitle();
final String mailBody = contentService.processTemplate(body, root);
emailService.sendMail(title, mailBody, user.getEmail());
}
@Override
@Transactional
public void archiveUserByToken(String tokenUuid, String key) throws NoSuchVerificationTokenException, TokenExpiredException, UserException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("archive-account", tokenUuid, key);
String uuid = consumedToken.getData();
User currentUser = SecurityContextUtil.getCurrentUser();
if (currentUser.getUuid().equals(uuid)) {
userService.archiveUser(currentUser);
} else {
throw new NoSuchVerificationTokenException();
}
}
}