/* * Copyright 2015 Evgeny Dolganov (evgenij.dolganov@gmail.com). * * 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 och.front.service; import static java.util.Collections.*; import static och.api.model.BaseBean.*; import static och.api.model.PropKey.*; import static och.api.model.user.SecurityContext.*; import static och.api.model.user.UserRole.*; import static och.api.model.user.UserStatus.*; import static och.api.model.web.ReqInfo.*; import static och.comp.db.main.table.MainTables.*; import static och.util.ExceptionUtil.*; import static och.util.Util.*; import static och.util.servlet.WebUtil.*; import static och.util.sql.SingleTx.*; import java.net.URLEncoder; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Set; import och.api.annotation.Secured; import och.api.exception.ValidationException; import och.api.exception.user.BannedUserException; import och.api.exception.user.DuplicateUserDataException; import och.api.exception.user.InvalidLoginDataForUpdateException; import och.api.exception.user.InvalidUserActivationCodeException; import och.api.exception.user.NotActivatedUserException; import och.api.exception.user.UnmodifiableAdminUserException; import och.api.exception.user.UserActivationExpiredException; import och.api.exception.user.UserNotFoundException; import och.api.model.user.UpdateUserReq; import och.api.model.user.User; import och.api.model.user.UserExt; import och.api.model.user.UserRole; import och.api.model.user.UserStatus; import och.comp.db.base.universal.BaseUpdateOp; import och.comp.db.base.universal.SelectRows; import och.comp.db.base.universal.field.RowField; import och.comp.db.main.table._f.ActivationCode; import och.comp.db.main.table._f.ActivationStateDate; import och.comp.db.main.table._f.CachedRoles; import och.comp.db.main.table._f.Email; import och.comp.db.main.table._f.Login; import och.comp.db.main.table._f.PswHash; import och.comp.db.main.table._f.PswSalt; import och.comp.db.main.table._f.StatusCode; import och.comp.db.main.table.user.CreateUser; import och.comp.db.main.table.user.SelectUserByEmail; import och.comp.db.main.table.user.SelectUserById; import och.comp.db.main.table.user.SelectUserByIds; import och.comp.db.main.table.user.SelectUserByLogin; import och.comp.db.main.table.user.SelectUserByLoginOrEmail; import och.comp.db.main.table.user.UpdateUserById; import och.comp.db.main.table.user_role.CreateUserRole; import och.comp.db.main.table.user_role.DeleteAllUserRoles; import och.comp.mail.SendReq; import och.front.service.event.user.UserBannedEvent; import och.front.service.event.user.UserUnbannedEvent; import och.front.service.event.user.UserUpdateTxEvent; import och.util.servlet.WebUtil; public class UserService extends BaseFrontService { public UserService(FrontAppContext c) { super(c); } public long createUser(User user, String psw) throws Exception { if( ! props.getBoolVal(users_autoActivation)) return createUserInner(user, psw, true); //auto activate pushToSecurityContext_SYSTEM_USER(); try { long userId = createUserInner(user, psw, false); activateUser(userId); return userId; } finally { popUserFromSecurityContext(); } } @Secured public long createUser(User user, String psw, boolean sendEmail) throws Exception { checkAccessFor_MODERATOR(); return createUserInner(user, psw, sendEmail); } private long createUserInner(User user, String psw, boolean sendEmail) throws Exception { validateState(user); validateForText(psw, "psw"); //create user String pswSalt = createSalt(); byte[] pswHash = getHash(psw, pswSalt); Date activationDate = new Date(); String activationCode = createActivationCode(); long id = universal.nextSeqFor(users); try { universal.update(new CreateUser(new UserExt(id, user.login, user.email, NEW, pswHash, pswSalt, activationDate, activationCode))); }catch (SQLException e) { if(containsAnyTextInMessage(e, "users_login_key", "users_email_key", "unique index")){ throw new DuplicateUserDataException(); } throw e; } //send async email if(sendEmail) sendActivationEmailAsync(user.email, activationCode); log.info("user created: id="+id +", login="+user.login +", req="+getReqInfoStr()); return id; } public void activateUser(String email, String code) throws UserActivationExpiredException, InvalidUserActivationCodeException, Exception{ checkArgumentForEmpty(email, "email"); checkArgumentForEmpty(code, "code"); //check status UserExt user = universal.selectOne(new SelectUserByEmail(email)); if(user == null || user.getStatus() != NEW) return; //check date user.checkActivateExpiredTime(getActivateExpiredTime()); //check code if( ! code.equals(user.activationCode)) throw new InvalidUserActivationCodeException(email, code); //activate activateUserInner(user.id); log.info("user activated: id="+user.id +", login="+user.login +", req="+getReqInfoStr()); } @Secured public void activateUser(long id) throws Exception{ checkAccessFor_MODERATOR(); activateUserInner(id); c.events.tryFireEvent(new UserUnbannedEvent(id)); log.info("user activated/unbanned: id="+id +", req="+getReqInfoStr()); } private void activateUserInner(long id) throws Exception{ universal.update(new UpdateUserById(id, new StatusCode(ACTIVATED), new ActivationStateDate(new Date()), new ActivationCode(null))); } @Secured public void sendActivationEmailAgain(String email) throws UserActivationExpiredException, Exception { checkAccessFor_MODERATOR(); checkArgumentForEmpty(email, "email"); //check status UserExt user = universal.selectOne(new SelectUserByEmail(email)); if(user == null || user.getStatus() != NEW) return; //check date user.checkActivateExpiredTime(getActivateExpiredTime()); //send async email sendActivationEmailAsync(email, user.activationCode); } /** * Try find user by login or email * and check status and psw */ public User checkEmailOrLoginAndPsw(String loginOrEmail, String psw) throws NotActivatedUserException, BannedUserException, Exception { UserExt user = universal.selectOne(new SelectUserByLoginOrEmail(loginOrEmail)); if(user == null) return null; checkStatus(user, loginOrEmail); return equalsPsw(user, psw)? user.getUser() : null; } /** * Try find user by id * and check status */ public User checkClientUser(long id, boolean exceptionIfInvalidStatus) throws NotActivatedUserException, BannedUserException, Exception { UserExt user = universal.selectOne(new SelectUserById(id)); if(user == null) return null; try { checkStatus(user, id); }catch (ValidationException e) { if(exceptionIfInvalidStatus) throw e; else return null; } return user.getUser(); } public User getUserByLogin(String login) throws Exception { UserExt userExt = universal.selectOne(new SelectUserByLogin(login)); return userExt == null? null : userExt.getUser(); } public User getUserByLoginOrEmail(String val) throws Exception { UserExt userExt = universal.selectOne(new SelectUserByLoginOrEmail(val)); return userExt == null? null : userExt.getUser(); } public User getUserById(long id) throws Exception { List<User> list = getUsersByIds(list(id)); return isEmpty(list)? null : list.get(0); } public User findUserById(long id) throws Exception{ User user = getUserById(id); if(user == null) throw new UserNotFoundException(); return user; } public List<User> getUsersByIds(Collection<Long> ids) throws Exception { if(isEmpty(ids)) return emptyList(); SelectRows<UserExt> select = ids.size() == 1? new SelectUserById(firstFrom(ids)) : new SelectUserByIds(ids); List<UserExt> list = universal.select(select); return convert(list, (item) -> item.getUser()); } public void generateNewPassword(String email) throws Exception { generateNewPasswordInner(email, true); } @Secured public void generateNewPassword(String email, boolean sendEmail) throws Exception { checkAccessFor_MODERATOR(); generateNewPasswordInner(email, sendEmail); } private void generateNewPasswordInner(String email, boolean sendEmail) throws Exception { UserExt user = universal.selectOne(new SelectUserByEmail(email)); if(user == null) return; String pswSalt = createSalt(); String psw = generateRandomPsw(6); byte[] pswHash = getHash(psw, pswSalt); universal.update(new UpdateUserById(user.id, new PswHash(pswHash), new PswSalt(pswSalt))); //send async email if(sendEmail) sendNewPswEmailAsync(user.email, user.login, psw); log.info("user took new password: id="+user.id +", login="+user.login +", req="+getReqInfoStr()); } @Secured public User updateUser(long id, String curPsw, UpdateUserReq req) throws Exception { checkAccessFor_ADMIN(); validateState(req); validateForText(curPsw, "psw"); UserExt userExt = universal.selectOne(new SelectUserById(id)); if(userExt == null) throw new UserNotFoundException(); if( ! equalsPsw(userExt, curPsw)) throw new InvalidLoginDataForUpdateException(); //convert psw to hash req.pswHash = null; if(req.psw != null){ String pswSalt = createSalt(); req.pswHash = getHash(req.psw, pswSalt); req.pswSalt = pswSalt; req.psw = null; } String oldLogin = null; boolean oldPsw = false; ArrayList<RowField<?>> fields = new ArrayList<>(); if( isUpdateNotEmptyVal(userExt.email, req.email)) fields.add(new Email(req.email)); if( isUpdateNotEmptyVal(userExt.login, req.login)) { fields.add(new Login(req.login)); oldLogin = userExt.login; } if( ! isEmpty(req.pswHash)) { fields.add(new PswHash(req.pswHash)); fields.add(new PswSalt(req.pswSalt)); oldPsw = true; } //update tx setSingleTxMode(); try { universal.update(new UpdateUserById(id, array(fields, RowField.class))); c.events.fireEvent(new UserUpdateTxEvent(userExt, req)); }catch (Exception e) { rollbackSingleTx(); if(e instanceof SQLException){ if(containsAnyTextInMessage(e, "email", "login", "unique index")) throw new DuplicateUserDataException(); } throw e; } finally { closeSingleTx(); } userExt.update(req); log.info("user updated values: id="+id+", login="+userExt.login + (oldLogin != null? ", oldLogin="+oldLogin : "") + (oldPsw ? ", psw changed" : "") +", req="+getReqInfoStr()); return userExt.getUser(); } @Secured public void setRoles(long userId, Set<UserRole> roles)throws Exception { checkAccessFor_MODERATOR(); if(roles == null) roles = emptySet(); ArrayList<BaseUpdateOp> ops = new ArrayList<>(); //update roles ops ops.add(new DeleteAllUserRoles(userId)); for (UserRole role : roles) ops.add(new CreateUserRole(userId, role)); //update cache op ops.add(new UpdateUserById(userId, new CachedRoles(roles))); //invoke universal.update(array(ops, BaseUpdateOp.class)); log.info("user updated roles: id="+userId +", roles="+roles +", req="+getReqInfoStr()); } @Secured public void banUser(long id) throws Exception { checkAccessFor_MODERATOR(); User user = getUserById(id); if(user == null) return; //can't ban ADMIN if(user.getRoles().contains(ADMIN)) throw new UnmodifiableAdminUserException(user); universal.update(new UpdateUserById(id, new StatusCode(BANNED), new ActivationStateDate(new Date()), new ActivationCode(null))); c.events.tryFireEvent(new UserBannedEvent(id)); log.info("user banned: id="+id +", login="+user.login +", req="+getReqInfoStr()); } private void sendActivationEmailAsync(String email, String activationCode) { try { String subject = c.templates.fromTemplate("user-activation-subject.ftl"); String html = c.templates.fromTemplate("user-activation-text.ftl", map( "activationUrl", c.props.getVal(users_activationUrl), "email", URLEncoder.encode(email, "UTF-8"), "code", URLEncoder.encode(activationCode, "UTF-8") ) ); c.mails.sendAsync(new SendReq(email, subject, html)); } catch (Exception e) { log.error("can't send email", e); } } private void sendNewPswEmailAsync(String email, String login, String newPsw) { try { String subject = c.templates.fromTemplate("user-change-psw-subject.ftl"); String html = c.templates.fromTemplate("user-change-psw-text.ftl", map( "login", login, "psw", newPsw ) ); c.mails.sendAsync(new SendReq(email, subject, html)); }catch (Exception e) { log.error("can't send email", e); } } public long getActivateExpiredTime(){ //by time prop Long byTimePropVal = props.getLongVal(users_expiredTime); if(byTimePropVal != null) return byTimePropVal; //by day prop return props.getLongVal(users_expiredDays) * 1000*60*60*24; } public static boolean equalsPsw(UserExt user, String psw) { return Arrays.equals(user.pswHash, WebUtil.getHash(psw, user.pswSalt)); } public static void checkStatus(UserExt user, Object errorData) throws ValidationException { UserStatus status = user.getStatus(); if(status == NEW) throw new NotActivatedUserException(errorData); if(status == BANNED) throw new BannedUserException(errorData); } }