package crmdna.member; import com.googlecode.objectify.Key; import com.microtripit.mandrillapp.lutung.model.MandrillApiError; import crmdna.client.Client; import crmdna.common.EmailConfig; import crmdna.common.Utils; import crmdna.common.api.APIException; import crmdna.common.api.APIResponse.Status; import crmdna.common.api.RequestInfo; import crmdna.encryption.Encryption; import crmdna.group.Group; import crmdna.mail2.*; import crmdna.mail2.MailContent.ReservedMailContentName; import crmdna.member.Member.AccountType; import crmdna.user.User; import crmdna.user.User.ClientLevelPrivilege; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.List; import java.util.Random; import static crmdna.common.AssertUtils.*; import static crmdna.common.OfyService.ofy; public class Account { public static final int MAX_PASSWORD_LENGTH = 50; public static final int MIN_PASSWORD_LENGTH = 3; public static MemberProp createAccount(String client, long groupId, long memberId, String password) throws NoSuchAlgorithmException, InvalidKeySpecException, MandrillApiError, IOException { Client.ensureValid(client); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, User.SUPER_USER); Utils.ensureValidEmail(memberEntity.email); ensureValidPassword(password); String email = memberEntity.email.toLowerCase(); List<Key<MemberEntity>> memberKeys = ofy(client).load().type(MemberEntity.class).filter("email", memberEntity.email) .filter("hasAccount", true).keys().list(); if (!memberKeys.isEmpty()) throw new APIException("There is already an account for email [" + email + "]") .status(Status.ERROR_RESOURCE_ALREADY_EXISTS); // TODO: memcache lock ensureVerificationEmailIsSetUp(client, groupId); byte[] salt = Encryption.generateRandomSalt(); ensure(salt != null, "salt is null"); ensure(salt.length > 0, "salt has length 0"); memberEntity.salt = salt; byte[] encryptedPassword = Encryption.getEncryptedPassword(password, salt); ensure(encryptedPassword != null, "encryptedPassword is null"); ensure(encryptedPassword.length > 0, "encryptedPassword has length 0"); memberEntity.encryptedPwd = encryptedPassword; memberEntity.isEmailVerified = false; final int ONE_MILLION = 1000000; memberEntity.verificationCode = new Random().nextInt(ONE_MILLION); memberEntity.hasAccount = true; memberEntity.accountType = AccountType.FEDERATED; ofy(client).save().entity(memberEntity).now(); sendVerificationEmail(client, groupId, memberId, User.SUPER_USER); return memberEntity.toProp(); } public static void sendVerificationEmail(String client, long groupId, long memberId, String login) throws MandrillApiError, IOException { Client.ensureValid(client); User.ensureValidUser(client, login); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, User.SUPER_USER); MemberProp memberProp = memberEntity.toProp(); Utils.ensureValidEmail(memberProp.contact.email); if (memberEntity.isEmailVerified) throw new APIException("Email is already verified").status(Status.ERROR_PRECONDITION_FAILED); ensure(memberEntity.verificationCode != 0, "Verification code not set in memberEntity [" + memberId + "]"); String email = memberProp.contact.email; MailMap mailMap = new MailMap(); String firstName = "Member"; if (memberProp.contact.firstName != null) firstName = memberProp.contact.firstName; mailMap.add(email, firstName, "N.A"); mailMap .add(email, MailMap.MergeVarID.VERIFICATION_CODE, memberEntity.verificationCode + ""); long mailContentId = MailContent .getByName(client, ReservedMailContentName.RESERVED_EMAIL_VERIFICATION.toString(), 0) .toProp().mailContentId; EmailConfig emailConfig = Group.getEmailConfig(client, groupId, login); MailSendInput msi = new MailSendInput(); msi.createMember = false; msi.groupId = groupId; msi.isTransactionEmail = true; msi.mailContentId = mailContentId; msi.senderEmail = emailConfig.contactEmail; msi.suppressIfAlreadySent = false; Mail.send(client, msi, mailMap, login); } public static void sendPasswordChangeNotificationEmail(String client, long groupId, long memberId) throws MandrillApiError, IOException { Client.ensureValid(client); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, User.SUPER_USER); MemberProp memberProp = memberEntity.toProp(); Utils.ensureValidEmail(memberProp.contact.email); String email = memberProp.contact.email; MailMap mailMap = new MailMap(); String firstName = memberEntity.firstName != null ? memberEntity.firstName : "Member"; mailMap.add(email, firstName, "N.A"); MailContentEntity mailContentEntity = MailContent .getByName(client, ReservedMailContentName.RESERVED_PASSWORD_CHANGE.toString(), 0); if (mailContentEntity == null) { String errMessage = "There is no mail content for name [" + ReservedMailContentName.RESERVED_PASSWORD_CHANGE + "] for client [" + client + "] for group id [0]"; throw new APIException(errMessage).status(Status.ERROR_INVALID_SETUP); } ensureNotNull(mailContentEntity.toProp().body, "Body for verification email is null"); EmailConfig emailConfig = Group.getEmailConfig(client, groupId, User.SUPER_USER); MailSendInput msi = new MailSendInput(); msi.createMember = false; msi.groupId = groupId; msi.isTransactionEmail = true; msi.mailContentId = mailContentEntity.toProp().mailContentId; msi.senderEmail = emailConfig.contactEmail; msi.suppressIfAlreadySent = false; Mail.send(client, msi, mailMap, User.SUPER_USER); } public static void sendPasswordResetEmail(String client, long groupId, long memberId, String password) throws MandrillApiError, IOException { Client.ensureValid(client); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, User.SUPER_USER); MemberProp memberProp = memberEntity.toProp(); Utils.ensureValidEmail(memberProp.contact.email); String email = memberProp.contact.email; MailMap mailMap = new MailMap(); String firstName = memberEntity.firstName != null ? memberEntity.firstName : "Member"; mailMap.add(email, firstName, "N.A"); mailMap.add(email, MailMap.MergeVarID.PASSWORD, password); MailContentEntity mailContentEntity = MailContent .getByName(client, ReservedMailContentName.RESERVED_PASSWORD_RESET.toString(), 0); if (mailContentEntity == null) { String errMessage = "There is no mail content for name [" + ReservedMailContentName.RESERVED_PASSWORD_RESET + "] for client [" + client + "], group id [0]"; throw new APIException(errMessage).status(Status.ERROR_INVALID_SETUP); } EmailConfig emailConfig = Group.getEmailConfig(client, groupId, User.SUPER_USER); MailSendInput msi = new MailSendInput(); msi.createMember = false; msi.groupId = groupId; msi.isTransactionEmail = true; msi.mailContentId = mailContentEntity.toProp().mailContentId; msi.senderEmail = emailConfig.contactEmail; msi.suppressIfAlreadySent = false; Mail.send(client, msi, mailMap, User.SUPER_USER); } private static void ensureVerificationEmailIsSetUp(String client, long groupId) { MailContentEntity mailContentEntity = MailContent.getByName(client, ReservedMailContentName.RESERVED_EMAIL_VERIFICATION.toString(), 0); if (mailContentEntity == null) { String errMessage = "There is no mail content for name [" + ReservedMailContentName.RESERVED_EMAIL_VERIFICATION + "] for client [" + client + "]"; RuntimeException ex = new RuntimeException(errMessage); Utils.sendAlertEmailToDevTeam(ex, new RequestInfo().client(client)); throw new APIException(errMessage).status(Status.ERROR_RESOURCE_NOT_FOUND); } ensureNotNull(mailContentEntity.toProp().body, "Body for verification email is null"); EmailConfig emailConfig = Group.getEmailConfig(client, groupId, User.SUPER_USER); ensureNotNull(emailConfig.contactEmail, "contactEmail is null for group [" + groupId + "]"); ensureNotNull(emailConfig.contactName, "contactName is null for group [" + groupId + "]"); Utils.ensureValidEmail(emailConfig.contactEmail); } private static void ensureValidPassword(String password) { ensureNotNull(password, "Password is null"); ensure(!password.isEmpty(), "Password is empty"); ensure(password.length() > MIN_PASSWORD_LENGTH, "Password should be greater than [" + MIN_PASSWORD_LENGTH + "] characters"); ensure(password.length() < MAX_PASSWORD_LENGTH, "Password should be lesser than [" + MAX_PASSWORD_LENGTH + "] characters"); } public static MemberProp changePassword(String client, long groupId, long memberId, String existingPassword, String newPassword) throws NoSuchAlgorithmException, InvalidKeySpecException, MandrillApiError, IOException { Client.ensureValid(client); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, User.SUPER_USER); if (!memberEntity.hasAccount) throw new APIException("There is no account for member [" + memberId + "]") .status(Status.ERROR_OPERATION_NOT_ALLOWED); if (memberEntity.accountDisabled) throw new APIException("Account is disabled for member [" + memberId + "]") .status(Status.ERROR_OPERATION_NOT_ALLOWED); LoginResult loginResult = getLoginResult(client, memberEntity.email, existingPassword); if (loginResult != LoginResult.SUCCESS) throw new APIException("Unable to change password - " + loginResult) .status(Status.ERROR_AUTH_FAILURE); ensure(!existingPassword.equals(newPassword), "Password cannot be the same"); ensureValidPassword(newPassword); ensureNotNull(memberEntity.salt, "salt is null"); memberEntity.encryptedPwd = Encryption.getEncryptedPassword(newPassword, memberEntity.salt); ofy(client).save().entity(memberEntity).now(); sendPasswordChangeNotificationEmail(client, groupId, memberId); return memberEntity.toProp(); } public static String resetPassword(String client, long groupId, long memberId) throws NoSuchAlgorithmException, InvalidKeySpecException, MandrillApiError, IOException { Client.ensureValid(client); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, User.SUPER_USER); if (!memberEntity.hasAccount) throw new APIException("There is no account for member [" + memberId + "]") .status(Status.ERROR_OPERATION_NOT_ALLOWED); if (memberEntity.accountDisabled) throw new APIException("Account is disabled for member [" + memberId + "]") .status(Status.ERROR_OPERATION_NOT_ALLOWED); String password = Utils.getRandomAlphaNumericString(6); memberEntity.encryptedPwd = Encryption.getEncryptedPassword(password, memberEntity.salt); ofy(client).save().entity(memberEntity).now(); sendPasswordResetEmail(client, groupId, memberId, password); return password; } public static LoginResult getLoginResult(String client, String email, String password) throws NoSuchAlgorithmException, InvalidKeySpecException { Client.ensureValid(client); Utils.ensureValidEmail(email); email = email.toLowerCase(); MemberQueryCondition queryCondition = new MemberQueryCondition(client, 100); queryCondition.email = email; int count = MemberLoader.getCount(queryCondition, User.SUPER_USER); if (count == 0) return LoginResult.EMAIL_DOES_NOT_EXIST; queryCondition.hasAccount = true; List<MemberEntity> memberEntities = MemberLoader.queryEntities(queryCondition, User.SUPER_USER); if (memberEntities.isEmpty()) return LoginResult.EMAIL_NOT_A_VALID_ACCOUNT; if (memberEntities.size() > 1) { String errMessage = "Email [" + email + "] has [" + memberEntities.size() + "] accounts for client [" + client + "]"; // should never happen Utils.sendAlertEmailToDevTeam(new RuntimeException(errMessage), new RequestInfo().client(client)); throw new APIException(errMessage).status(Status.ERROR_INTERNAL); } MemberEntity memberEntity = memberEntities.get(0); if (memberEntity.accountDisabled) return LoginResult.ACCOUNT_DISABLED; if (!memberEntity.isEmailVerified) return LoginResult.EMAIL_NOT_VERIFIED; ensureNotNullNotEmpty(password, "Supplied password is null or empty"); ensureNotNull(memberEntity.encryptedPwd, "No password stored in Member Entity"); ensureNotNull(memberEntity.salt, "No salt stored in Member Entity"); boolean result = Encryption.authenticate(password, memberEntity.encryptedPwd, memberEntity.salt); LoginResult loginResult = LoginResult.WRONG_CREDENTIAL; if (result) loginResult = LoginResult.SUCCESS; return loginResult; } public static EmailVerificationResult verifyEmail(String client, long memberId, long verificationCode) { MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, User.SUPER_USER); if (!memberEntity.hasAccount) return EmailVerificationResult.EMAIL_NOT_A_VALID_ACCOUNT; if (memberEntity.isEmailVerified) return EmailVerificationResult.ALREADY_VERIFIED; if (memberEntity.verificationCode == verificationCode) { memberEntity.isEmailVerified = true; memberEntity.accountCreatedMS = System.currentTimeMillis(); ofy(client).save().entity(memberEntity).now(); return EmailVerificationResult.SUCCESS; } else return EmailVerificationResult.WRONG_VERIFICATION_CODE; } public static MemberProp setEmailAsVerified(String client, long memberId, String login) { Client.ensureValid(client); User.ensureClientLevelPrivilege(client, login, ClientLevelPrivilege.VERIFY_EMAIL); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, login); memberEntity.isEmailVerified = true; ofy(client).save().entity(memberEntity).now(); return memberEntity.toProp(); } public static MemberProp setEmailAsUnverified(String client, long memberId, String login) { Client.ensureValid(client); User.ensureClientLevelPrivilege(client, login, ClientLevelPrivilege.VERIFY_EMAIL); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, login); memberEntity.isEmailVerified = false; ofy(client).save().entity(memberEntity).now(); return memberEntity.toProp(); } public static MemberProp disableOrEnableAccount(String client, long memberId, boolean disable, String login) { Client.ensureValid(client); User.ensureClientLevelPrivilege(client, login, ClientLevelPrivilege.ENABLE_DISABLE_ACCOUNT); MemberEntity memberEntity = MemberLoader.safeGet(client, memberId, login); if (!memberEntity.hasAccount) throw new APIException("There is no account for member [" + memberId + "]") .status(Status.ERROR_OPERATION_NOT_ALLOWED); memberEntity.accountDisabled = disable; ofy(client).save().entity(memberEntity).now(); return memberEntity.toProp(); } public static List<MemberProp> getMembersWithAccounts(String client) { List<MemberEntity> entities = ofy(client).load().type(MemberEntity.class).filter("hasAccount", true) .filter("isEmailVerified", true).list(); List<MemberProp> props = new ArrayList<>(); for (MemberEntity entity : entities) { props.add(entity.toProp()); } return props; } public static MemberProp getMemberWithAccount(String client, String email) { Utils.ensureValidEmail(email); List<MemberEntity> entities = ofy(client).load().type(MemberEntity.class).filter("hasAccount", true) .filter("email", email).list(); ensure(entities.size() == 1, "There are [" + entities.size() + "] members with email [" + email + "]"); return entities.get(0).toProp(); } public enum LoginResult { SUCCESS, EMAIL_DOES_NOT_EXIST, EMAIL_NOT_A_VALID_ACCOUNT, EMAIL_NOT_VERIFIED, WRONG_CREDENTIAL, ACCOUNT_DISABLED } public enum EmailVerificationResult { SUCCESS, EMAIL_NOT_A_VALID_ACCOUNT, WRONG_VERIFICATION_CODE, ALREADY_VERIFIED } }