EMailVerificationServiceImpl.java
/*
* Copyright 2020 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.gringlobal.service.impl;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.genesys.blocks.security.UserException;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.model.AppResource;
import org.gringlobal.model.SysUser;
import org.gringlobal.model.VerificationToken;
import org.gringlobal.model.WebUser;
import org.gringlobal.service.AppResourceService;
import org.gringlobal.service.EMailService;
import org.gringlobal.service.EMailVerificationService;
import org.gringlobal.service.TemplatingService;
import org.gringlobal.service.TokenVerificationService;
import org.gringlobal.service.TokenVerificationService.*;
import org.gringlobal.service.UserService;
import org.gringlobal.service.WebUserService;
import org.gringlobal.spring.TransactionHelper;
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.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@Slf4j
public class EMailVerificationServiceImpl implements EMailVerificationService {
@Autowired
private TokenVerificationService tokenVerificationService;
@Autowired
private EMailService emailService;
@Autowired
private WebUserService webUserService;
@Autowired
private UserService userService;
@Autowired
private TemplatingService templatingService;
@Autowired
private AppResourceService appResourceService;
@Value("${frontend.url}")
private String frontendUrl;
@Override
@Transactional
public void sendVerificationEmail(WebUser webUser) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("email-verification", webUser.getUsername());
final Map<String, Object> scopes = new HashMap<>();
scopes.put("url", frontendUrl);
scopes.put("tokenUUID", verificationToken.getUuid());
scopes.put("email", webUser.getUsername());
scopes.put("tokenKey", verificationToken.getKey());
final AppResource resource = appResourceService.getResource(AppResourceService.APP_NAME_GGCE, "email/webuser-verify-email.mustache", Locale.ENGLISH);
String messageTitle;
String messageTemplate;
if (resource != null) {
messageTitle = resource.getValueMember();
messageTemplate = resource.getDisplayMember();
} else {
log.info("AppResource 'email/webuser-verify-email.mustache' not found, using bundled template");
try {
messageTitle = "Verify your email address";
messageTemplate = Files.readString(Path.of(getClass().getResource("/email/webuser-verify-email.mustache").getPath()));
} catch (IOException e) {
log.warn("Failed to load message template, not sending email.", e);
return;
}
}
final String mailBody = templatingService.fillTemplate(messageTemplate, scopes);
emailService.sendMail(messageTitle, mailBody, webUser.getUsername());
}
@Override
@Transactional
public void validateEMail(String tokenUuid, String key) throws Exception {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("email-verification", tokenUuid, key);
WebUser webUser = (WebUser) webUserService.loadUserByUsername(consumedToken.getData());
asAdmin(() -> {
webUserService.setAccountActive(webUser.getId(), true);
return true;
});
}
/**
* 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 WebUser webUser = (WebUser) webUserService.loadUserByUsername(verificationToken.getData());
if (webUser.isEnabled()) {
throw new InvalidApiUsageException("User already validated");
}
asAdmin(() -> {
webUserService.remove(webUser);
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", e);
}
}
@Override
public void sendPasswordResetEmail(String email, String username, String origin) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("password-reset", username);
final Map<String, Object> scopes = new HashMap<>();
scopes.put("url", origin);
scopes.put("tokenUUID", verificationToken.getUuid());
scopes.put("tokenKey", verificationToken.getKey());
scopes.put("username", username);
final AppResource resource = appResourceService.getResource(AppResourceService.APP_NAME_GGCE, "email/reset-password.mustache", Locale.ENGLISH);
String messageTemplate;
String messageTitle;
if (resource != null) {
messageTitle = resource.getValueMember();
messageTemplate = resource.getDisplayMember();
} else {
log.info("AppResource 'email/reset-password.mustache' not found, using bundled template");
try {
messageTitle = "Reset password";
messageTemplate = Files.readString(Path.of(getClass().getResource("/email/reset-password.mustache").getPath()));
} catch (IOException e) {
log.warn("Failed to load message template, not sending email.", e);
return;
}
}
final String mailBody = templatingService.fillTemplate(messageTemplate, scopes);
emailService.sendMail(messageTitle, mailBody, email);
}
@Override
@Transactional(rollbackFor = Throwable.class)
public void changeWebUserPassword(final String tokenUuid, final String key, final String password, final String origin) throws NoSuchVerificationTokenException, TokenExpiredException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("password-reset", tokenUuid, key);
final WebUser user = (WebUser) webUserService.loadUserByUsername(consumedToken.getData());
Authentication prevAuth = SecurityContextHolder.getContext().getAuthentication();
try {
log.warn("Setting temporary authorization for password reset for {}", user.getUsername());
final UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
webUserService.setPassword(user.getId(), password);
sendResetConfirmationEmail(user.getUsername(), user.getWebCooperator().getEmail(), origin);
} finally {
log.warn("Restoring authorization away from {}", user.getUsername());
SecurityContextHolder.getContext().setAuthentication(prevAuth);
}
}
@Override
@Transactional(rollbackFor = Throwable.class)
public void changeSysUserPassword(final String tokenUuid, final String key, final String password, final String origin) throws NoSuchVerificationTokenException, TokenExpiredException, UserException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("password-reset", tokenUuid, key);
final SysUser user = userService.loadUserByUsername(consumedToken.getData());
Authentication prevAuth = SecurityContextHolder.getContext().getAuthentication();
try {
log.warn("Setting temporary authorization for password reset for {}", user.getUsername());
final UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
userService.setPassword(user, password);
sendResetConfirmationEmail(user.getUsername(), user.getCooperator().getEmail(), origin);
} finally {
log.warn("Restoring authorization away from {}", user.getUsername());
SecurityContextHolder.getContext().setAuthentication(prevAuth);
}
}
@Override
@Transactional
public void cancelPasswordReset(String tokenUuid) {
try {
tokenVerificationService.cancel(tokenUuid);
} catch (final NoSuchVerificationTokenException e) {
log.warn("No such token. Error message {}", e.getMessage());
throw new InvalidApiUsageException("No such verification token", e);
}
}
private void sendResetConfirmationEmail(String username, String email, String url) {
Date nowDate = new Date();
String resetDate = new SimpleDateFormat("MMM dd, yyyy 'at' HH:mm z").format(nowDate);
final Map<String, Object> scopes = new HashMap<>();
scopes.put("username", username);
scopes.put("resetDate", resetDate);
scopes.put("url", url);
final AppResource resource = appResourceService.getResource(AppResourceService.APP_NAME_GGCE, "email/reset-password-confirm.mustache", Locale.ENGLISH);
String messageTemplate;
String messageTitle;
if (resource != null) {
messageTitle = resource.getValueMember();
messageTemplate = resource.getDisplayMember();
} else {
log.info("AppResource 'email/reset-password-confirm.mustache' not found, using bundled template");
try {
messageTitle = "Reset password confirmation";
messageTemplate = Files.readString(Path.of(getClass().getResource("/email/reset-password-confirm.mustache").getPath()));
} catch (IOException e) {
log.warn("Failed to load message template, not sending email.", e);
return;
}
}
final String mailBody = templatingService.fillTemplate(messageTemplate, scopes);
emailService.sendMail(messageTitle, mailBody, email);
}
private <T> T asAdmin(Callable<T> callable) throws Exception {
UserDetails administrator = userService.loadUserByUsername("administrator");
List<GrantedAuthority> authorities = Lists.newArrayList(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"));
authorities.addAll(administrator.getAuthorities());
Authentication authentication = new UsernamePasswordAuthenticationToken(administrator, null, authorities);
return TransactionHelper.asUser(authentication, callable);
}
}