package org.sigmah.server.handler;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.sigmah.client.page.Page;
import org.sigmah.client.page.RequestParameter;
import org.sigmah.server.dao.UserDAO;
import org.sigmah.server.dispatch.impl.UserDispatch.UserExecutionContext;
import org.sigmah.server.domain.User;
import org.sigmah.server.handler.base.AbstractCommandHandler;
import org.sigmah.server.mail.MailService;
import org.sigmah.server.security.Authenticator;
import org.sigmah.shared.Language;
import org.sigmah.shared.command.PasswordManagementCommand;
import org.sigmah.shared.command.result.StringResult;
import org.sigmah.shared.dispatch.CommandException;
import org.sigmah.shared.dto.referential.EmailKey;
import org.sigmah.shared.dto.referential.EmailKeyEnum;
import org.sigmah.shared.dto.referential.EmailType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
/**
* Handler for {@link PasswordManagementCommand}.
*
* @author Denis Colliot (dcolliot@ideia.fr)
*/
public class PasswordManagementCommandHandler extends AbstractCommandHandler<PasswordManagementCommand, StringResult> {
/**
* Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(PasswordManagementCommandHandler.class);
/**
* <p>
* Timeout for "<em>change password</em>" tokens (in seconds).
* After this amount of time, token becomes invalid.
* </p>
* <p>
* <em>Set on 24 hours.</em>
* </p>
*/
private static final long CHANGE_PASSWORD_TIMEOUT = 24 * 60 * 60;
/**
* Injected {@link UserDAO}.
*/
private final UserDAO userDAO;
/**
* Injected {@link Authenticator} service.
*/
private final Authenticator authenticator;
/**
* Injected {@link MailService}.
*/
private final MailService mailService;
@Inject
public PasswordManagementCommandHandler(final UserDAO userDAO, final Authenticator authenticator, final MailService mailService) {
this.userDAO = userDAO;
this.authenticator = authenticator;
this.mailService = mailService;
}
/**
* {@inheritDoc}
*/
@Override
public StringResult execute(final PasswordManagementCommand command, final UserExecutionContext context) throws CommandException {
if (command.getAction() == null) {
throw new CommandException("Invalid command 'action' property: '" + command.getAction() + "'.");
}
final String email = command.getEmail();
final Language language = command.getLanguage();
final String newPassword = command.getNewPassword();
final String changePasswordToken = command.getChangePasswordToken();
final String result;
switch (command.getAction()) {
case ForgotPassword:
forgotPassword(email, language, context);
result = null;
break;
case UpdatePassword:
updatePassword(email, newPassword, context);
result = null;
break;
case RetrieveEmailFromToken:
// Retrieves 'change password' token corresponding user email.
result = retrieveEmailFromToken(changePasswordToken, context);
break;
default:
throw new CommandException("Invalid command 'action' property: '" + command.getAction() + "'.");
}
return new StringResult(result);
}
/**
* Sends a "<em>lost password</em>" email to the given {@code userEmail} corresponding {@link User}.
*
* @param email
* The user email.
* @param language
* The user language.
* @param context
* The execution context.
* @throws CommandException
* If an error occurs or arguments are invalid.
*/
@Transactional(rollbackOn = { Exception.class
})
// Transactional methods cannot be private (see Guice documentation).
protected void forgotPassword(final String email, final Language language, final UserExecutionContext context) throws CommandException {
if (StringUtils.isBlank(email)) {
throw new CommandException("User email is invalid.");
}
// Retrieves user.
final User user = userDAO.findUserByEmail(email);
if (user == null) {
throw new CommandException("No user found for email '" + email + "'.");
}
// Unique key is stored and sent by email.
final String changePasswordKey = UUID.randomUUID().toString().replaceAll("-", "");
// Stores the "change password" key and date.
user.setChangePasswordKey(changePasswordKey);
user.setDateChangePasswordKeyIssued(new Date());
userDAO.persist(user, context.getUser());
// Builds a link to pass by email, so the user can reset the password following the link.
final Map<RequestParameter, String> linkParameters = new HashMap<>();
linkParameters.put(RequestParameter.ID, changePasswordKey);
final String resetPasswordLink = context.getApplicationUrl(Page.RESET_PASSWORD, linkParameters);
// Sends email to user.
final Map<EmailKey, String> emailParameters = new HashMap<>();
final EmailType emailType = EmailType.LOST_PASSWORD;
emailParameters.put(EmailKeyEnum.RESET_PASSWORD_LINK, resetPasswordLink);
if (!mailService.send(emailType, emailParameters, language, email)) {
throw new CommandException("Sending email '" + emailType + "' to '" + email + "' failed. Rollbacking.");
}
if (LOG.isInfoEnabled()) {
LOG.info("Password reset email has been successfully sent to '{}' with following reset link: '{}'.", email, resetPasswordLink);
}
}
/**
* Updates the given {@code email} corresponding {@link User}'s password with the given one.
*
* @param email
* The user email.
* @param newPassword
* The new password.
* @param context
* The execution context.
* @throws CommandException
* If an error occurs or arguments are invalid.
*/
private void updatePassword(final String email, final String newPassword, final UserExecutionContext context) throws CommandException {
if (StringUtils.isBlank(email)) {
throw new CommandException("User email is invalid.");
}
if (StringUtils.isBlank(newPassword)) {
throw new CommandException("User's new password is invalid.");
}
final User user = userDAO.findUserByEmail(email);
if (user == null) {
throw new CommandException("No user found for email '" + email + "'.");
}
user.setHashedPassword(authenticator.hashPassword(newPassword));
// Deactivates "change password" key.
user.clearChangePasswordKey();
userDAO.persist(user, context.getUser());
if (LOG.isInfoEnabled()) {
LOG.info("User '{}' ({}) password has been successfully updated using password reset link.", user, email);
}
}
/**
* Retrieves the given "<em>change password</em>" {@code token} corresponding user email.
*
* @param token
* The "<em>change password</em>" token.
* @param context
* The execution context.
* @return The user email, or {@code null} if no user found for {@code token} or invalid token.
* @throws CommandException
* If an error occurs or arguments are invalid.
*/
private String retrieveEmailFromToken(final String token, final UserExecutionContext context) throws CommandException {
if (StringUtils.isBlank(token)) {
throw new CommandException("Change passord token is invalid.");
}
// Finds user.
final User user = userDAO.findUserByChangePasswordKey(token);
if (user == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("No user found for \"change password\" token '{}'.", token);
}
return null;
}
if (user.getDateChangePasswordKeyIssued() == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("User '{}' has been found for \"change password\" token '{}', but its issued date is null (should not be possible).", user, token);
}
return null;
}
// Has token expired?
final long timeSpentInSecs = (new Date().getTime() - user.getDateChangePasswordKeyIssued().getTime()) / 1000;
if (timeSpentInSecs > CHANGE_PASSWORD_TIMEOUT) {
// Expired token.
if (LOG.isDebugEnabled()) {
LOG.debug("User '{}' has an expired \"change password\" token '{}'. Reseting user \"change password\" properties.", user, token);
}
user.clearChangePasswordKey();
userDAO.persist(user, context.getUser());
return null;
}
if (LOG.isDebugEnabled()) {
LOG.debug("User '{}' has beend found for \"change password\" token '{}'.", user, token);
}
return user.getEmail();
}
}