/* Copyright (c) 2011 Danish Maritime Authority.
*
* 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 net.maritimecloud.identityregistry.command.user;
import net.maritimecloud.identityregistry.domain.UserRoles;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;
import net.maritimecloud.identityregistry.command.api.ChangeUserEmailAddress;
import net.maritimecloud.identityregistry.command.api.ChangeUserPassword;
import net.maritimecloud.identityregistry.command.api.RegisterUser;
import net.maritimecloud.identityregistry.command.api.ResetPasswordKeyGenerated;
import net.maritimecloud.identityregistry.command.api.UnconfirmedUserEmailAddressSupplied;
import net.maritimecloud.identityregistry.command.api.UserAccountActivated;
import net.maritimecloud.identityregistry.command.api.UserEmailAddressVerified;
import net.maritimecloud.identityregistry.command.api.UserPasswordChanged;
import net.maritimecloud.identityregistry.command.api.UserRegistered;
import net.maritimecloud.identityregistry.command.api.VerifyEmailAddress;
import net.maritimecloud.identityregistry.domain.EncryptionService;
import net.maritimecloud.identityregistry.domain.DomainRegistry;
import org.axonframework.commandhandling.annotation.CommandHandler;
import org.axonframework.common.Assert;
import org.axonframework.eventsourcing.annotation.AbstractAnnotatedAggregateRoot;
import org.axonframework.eventsourcing.annotation.AggregateIdentifier;
import org.axonframework.eventsourcing.annotation.EventSourcingHandler;
import org.springframework.stereotype.Component;
/**
* @author Christoffer Børrild
*/
@Component
public class User extends AbstractAnnotatedAggregateRoot<UserId> {
@AggregateIdentifier
private UserId userId;
private String username;
private String password;
// private String passwordEncryptionSalt;
//private Person person;
private String emailAddress;
private UserRoles userRoles;
private boolean activated;
private UnconfirmedEmailAddress unconfirmedEmailAddress;
private String resetPasswordKey;
private static EncryptionService encryptionService() {
return DomainRegistry.encryptionService();
}
protected User() {
activated = false;
}
@CommandHandler
public User(RegisterUser command) {
this();
assertValidEmailAddress(command.getEmailAddress());
assertValidUsername(command.getPrefferedUsername());
assertPasswordComply(command.getPrefferedUsername(), command.getPassword());
String emailVerificationCode = new UnconfirmedEmailAddress(command.getEmailAddress()).activationCode().toString();
// HACK: FIXME: TODO:
// supply hardcoded code in odrer to auto-create users for test and demo without reading mails
emailVerificationCode = a_HACK_TEST_useStaticVerificationCode(command.getEmailAddress(), emailVerificationCode);
apply(new UserRegistered(
command.getUserId(),
command.getPrefferedUsername(),
command.getEmailAddress(),
asEncryptedValue(command.getPassword()),
emailVerificationCode)
);
}
@CommandHandler
public void handle(ChangeUserEmailAddress command) {
assertValidEmailAddress(command.getEmailAddress());
if (!command.getEmailAddress().equalsIgnoreCase(emailAddress())) {
String emailVerificationCode = new UnconfirmedEmailAddress(command.getEmailAddress()).activationCode().toString();
// HACK: FIXME: TODO:
// supply hardcoded code in order to auto-create users for test and demo without reading mails
emailVerificationCode = a_HACK_TEST_useStaticVerificationCode(command.getEmailAddress(), emailVerificationCode);
apply(new UnconfirmedUserEmailAddressSupplied(command.getUserId(), this.username(), command.getEmailAddress(), emailVerificationCode));
}
}
private String a_HACK_TEST_useStaticVerificationCode(String emailAddress, String emailVerificationCode) {
// HACK: FIXME: TODO:
// supply hardcoded code in odrer to auto-create users for test and demo without reading mails
if (emailAddress.contains("@static.demo.dma.dk")) {
emailVerificationCode = emailAddress.replace("@static.demo.dma.dk", "");
}
return emailVerificationCode;
}
@CommandHandler
public void handle(VerifyEmailAddress command) {
assertValidEmailVerificationCode(command.getEmailAddressVerificationId());
if (emailAddressAlreadyConfirmed()) {
return;
}
apply(new UserEmailAddressVerified(command.getUserId(), this.username(), unconfirmedEmailAddress.emailAddress));
if (!isActivated()) {
apply(new UserAccountActivated(command.getUserId(), this.username()));
}
}
private boolean emailAddressAlreadyConfirmed() {
return unconfirmedEmailAddress != null && unconfirmedEmailAddress.emailAddress().equals(emailAddress());
}
@CommandHandler
public void handle(ChangeUserPassword command) {
assertCanChangePassword(command.getCurrentPassword(), command.getChangedPassword());
apply(new UserPasswordChanged(command.getUserId(), this.username(), asEncryptedValue(command.getChangedPassword())));
}
@EventSourcingHandler
public void on(UserRegistered event) {
this.userId = event.getUserId();
this.setUsername(event.getPrefferedUsername());
this.unconfirmedEmailAddress = new UnconfirmedEmailAddress(event.getEmailAddress(), event.getEmailVerificationCode());
this.setPassword(event.getObfuscatedPassword());
}
@EventSourcingHandler
public void on(UnconfirmedUserEmailAddressSupplied event) {
this.unconfirmedEmailAddress = new UnconfirmedEmailAddress(event.getUnconfirmedEmailAddress(), event.getEmailVerificationCode());
}
@EventSourcingHandler
public void on(UserEmailAddressVerified event) {
this.setEmailAddress(event.getEmailAddress());
}
@EventSourcingHandler
public void on(UserAccountActivated event) {
this.activated = true;
}
@EventSourcingHandler
public void on(ResetPasswordKeyGenerated event) {
this.resetPasswordKey = event.getResetPasswordKey();
}
@EventSourcingHandler
public void on(UserPasswordChanged event) {
setPassword(event.getObfuscatedChangedPassword());
}
// public User(String aUsername, String aPassword /*,Person aPerson*/, String emailAddress, Role... roles) {
//
// this();
//
// if (roles.length == 0) {
// userRoles = new UserRoles(Role.USER);
// } else {
// userRoles = new UserRoles(roles);
// }
//
// // setPerson(aPerson);
// setUsername(aUsername);
// protectPassword(aPassword);
// // aPerson.internalOnlySetUser(this);
// setEmailAddress(emailAddress);
// passwordEncryptionSalt = "salt";
//
// //DomainEventPublisher.instance().publish(new UserRegistered(aUsername, aPerson.name(), aPerson.contactInformation().emailAddress()));
// }
//
// /**
// * @return the application specific unique id of the user
// */
// @Override
// public long id() {
// //return super.id();
// return id;
// }
//
// //public Person person() {
// // return person;
// //}
// //public UserDescriptor userDescriptor() {
// // return new UserDescriptor(
// // username(),
// // person().emailAddress().address());
// //}
// //
public String emailAddress() {
return emailAddress;
}
// @Override
// public int hashCode() {
// int hashCodeValue = +(45217 * 269) + this.username().hashCode();
// return hashCodeValue;
// }
//
// @Override
// public String toString() {
// return "User [username=" + username
// //+ ", person=" + person
// + ", emailAddress=" + emailAddress
// + "]";
// }
private void assertCanChangePassword(String aCurrentPassword, String aChangedPassword) {
Assert.notEmpty(aCurrentPassword, "Current password must be provided.");
Assert.notEmpty(aChangedPassword, "New password must be provided.");
assertCurrentPasswordConfirmedOrIsResetPasswordKey(aCurrentPassword, "Current password not confirmed.");
assertPasswordComply(username(), aChangedPassword);
// protectPassword(aChangedPassword);
//DomainEventPublisher.instance().publish(new UserPasswordChanged(userId(), username()));
}
private void assertPasswordComply(String username, String aChangedPassword) {
assertPasswordsNotSame(password(), aChangedPassword);
assertPasswordNotWeak(aChangedPassword);
assertUsernamePasswordNotSame(username, aChangedPassword);
}
//// public void changePersonalContactInformation(ContactInformation aContactInformation) {
//// person().changeContactInformation(aContactInformation);
//// }
////
//// public void changePersonalName(FullName aPersonalName) {
//// person().changeName(aPersonalName);
//// }
private void assertCurrentPasswordConfirmedOrIsResetPasswordKey(String aCurrentPassword, String message) {
if (isNotResetPasswordKey(aCurrentPassword)) {
assertCurrentPasswordConfirmed(aCurrentPassword, message);
}
}
private boolean isNotResetPasswordKey(String aCurrentPassword) {
return !aCurrentPassword.equals(resetPasswordKey);
}
private void assertCurrentPasswordConfirmed(String aCurrentPassword, String message) {
boolean valuesMatch = encryptionService().valuesMatch(aCurrentPassword, internalAccessOnlyEncryptedPassword());
Assert.isTrue(valuesMatch, message);
}
// @Override
// public boolean equals(Object anObject) {
// boolean equalObjects = false;
//
// if (anObject != null && this.getClass() == anObject.getClass()) {
// User typedObject = (User) anObject;
// equalObjects = this.username().equals(typedObject.username());
// }
//
// return equalObjects;
// }
protected void assertPasswordsNotSame(String aCurrentPassword, String aChangedPassword) {
if (aCurrentPassword == null) {
return;
}
boolean valuesMatch = encryptionService().valuesMatch(aChangedPassword, aCurrentPassword);
Assert.isFalse(valuesMatch, "The password is unchanged.");
}
protected void assertPasswordNotWeak(String aPlainTextPassword) {
Assert.isFalse(DomainRegistry.passwordService().isWeak(aPlainTextPassword), "The password must be stronger.");
}
protected void assertUsernamePasswordNotSame(String username, String aPlainTextPassword) {
assertArgumentNotEquals(username, aPlainTextPassword, "The username and password must not be the same.");
}
private void assertArgumentNotEquals(Object anObject1, Object anObject2, String aMessage) {
if (anObject1.equals(anObject2)) {
throw new IllegalArgumentException(aMessage);
}
}
private String internalAccessOnlyEncryptedPassword() {
return password();
}
private String username() {
return username;
}
private void setUsername(String aUsername) {
this.username = aUsername;
}
private String password() {
return password;
}
protected void setPassword(String aPassword) {
this.password = aPassword;
}
private void setEmailAddress(String anEmailAddress) {
this.emailAddress = anEmailAddress;
}
// //protected void setPerson(Person aPerson) {
// // assertArgumentNotNull(aPerson, "The person is required.");
// //
// // this.person = aPerson;
// //}
// //
// private void protectPassword(String aChangedPassword) {
// setPassword(asEncryptedValue(aChangedPassword));
// }
private String asEncryptedValue(String aPlainTextPassword) {
String encryptedValue = encryptionService().encryptedValue(aPlainTextPassword);
return encryptedValue;
}
//// protected GroupMember toGroupMember() {
//// GroupMember groupMember =
//// new GroupMember(
//// username(),
//// GroupMemberType.User);
////
//// return groupMember;
//// }
private void assertValidUsername(String aUsername) {
Assert.notEmpty(aUsername, "The username is required.");
assertArgumentLength(aUsername, 3, 250, "The username must be 3 to 250 characters.");
// TODO: check a domain service to see if the username is already taken
// ...command.getUsername()
// ...or perhaps add a saga for that!?!
}
private void assertArgumentLength(String aString, int aMinimum, int aMaximum, String aMessage) {
int length = aString.trim().length();
if (length < aMinimum || length > aMaximum) {
throw new IllegalArgumentException(aMessage);
}
}
private void assertValidEmailAddress(String anEmailAddress) {
Assert.notEmpty(anEmailAddress, "The email address is required.");
assertArgumentLength(anEmailAddress, 1, 100, "Email address must be 100 characters or less.");
Assert.isTrue(
Pattern.matches("\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*", anEmailAddress),
"Email address format is invalid.");
}
// public String internalAccessOnlyEncryptionSalt() {
// return passwordEncryptionSalt;
// }
//
// public UserRoles userRoles() {
// return userRoles;
// }
private void assertValidEmailVerificationCode(String emailAddressVerificationCode) {
if (!unconfirmedEmailAddress.match(emailAddressVerificationCode)) {
throw new IllegalArgumentException("Unknown or deprecated emailAddress Verification Code");
}
}
// /**
// * Generates a random activationId. The id must be used in order to activate the user account
// * but may also be used for other purposes, like reset password confirmation (TODO: change that!)
// */
// public void generateActivationId() {
// activationId = UUID.randomUUID().toString();
// }
//
// /**
// * Method that will mark the User (Account) as active if the supplied id match the stored activationId
// * @param anActivationId
// */
// public void activate(String anActivationId) {
// if (activationId() != null && activationId().equals(anActivationId)) {
// activate();
// }
// }
//
// private void activate() {
// active = true;
// }
public boolean isActivated() {
return activated;
}
public void registerResetPasswordKey(String resetPasswordKey) {
apply(new ResetPasswordKeyGenerated(userId, username, emailAddress, resetPasswordKey));
}
private static class UnconfirmedEmailAddress {
private final String emailAddress;
private final UUID activationCode;
public UnconfirmedEmailAddress(String emailAddress, String activationCode) {
this.emailAddress = emailAddress;
this.activationCode = UUID.fromString(activationCode);
}
public UnconfirmedEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
this.activationCode = UUID.randomUUID();
}
public String emailAddress() {
return emailAddress;
}
public UUID activationCode() {
return activationCode;
}
public boolean match(String activationCode) {
return this.activationCode.toString().equals(activationCode);
}
@Override
public int hashCode() {
int hash = 7;
hash = 59 * hash + Objects.hashCode(this.emailAddress);
hash = 59 * hash + Objects.hashCode(this.activationCode);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final UnconfirmedEmailAddress other = (UnconfirmedEmailAddress) obj;
if (!Objects.equals(this.emailAddress, other.emailAddress)) {
return false;
}
if (!Objects.equals(this.activationCode, other.activationCode)) {
return false;
}
return true;
}
}
}