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();
		}
	}
}