package org.molgenis.security.account; import org.molgenis.auth.User; import org.molgenis.auth.UserFactory; import org.molgenis.data.MolgenisDataAccessException; import org.molgenis.data.MolgenisDataException; import org.molgenis.data.settings.AppSettings; import org.molgenis.security.captcha.CaptchaException; import org.molgenis.security.captcha.CaptchaRequest; import org.molgenis.security.captcha.CaptchaService; import org.molgenis.security.user.MolgenisUserException; import org.molgenis.util.CountryCodes; import org.molgenis.util.ErrorMessageResponse; import org.molgenis.util.ErrorMessageResponse.ErrorMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.RedirectStrategy; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.naming.NoPermissionException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.io.IOException; import java.util.Collections; import java.util.Map; import static java.util.Objects.requireNonNull; import static org.molgenis.security.account.AccountController.URI; import static org.molgenis.security.user.UserAccountService.MIN_PASSWORD_LENGTH; @Controller @RequestMapping(URI) public class AccountController { private static final Logger LOG = LoggerFactory.getLogger(AccountController.class); public static final String URI = "/account"; private static final String CHANGE_PASSWORD_RELATIVE_URI = "/password/change"; public static final String CHANGE_PASSWORD_URI = URI + CHANGE_PASSWORD_RELATIVE_URI; static final String REGISTRATION_SUCCESS_MESSAGE_USER = "You have successfully registered, an activation e-mail has been sent to your email."; static final String REGISTRATION_SUCCESS_MESSAGE_ADMIN = "You have successfully registered, your request has been forwarded to the administrator."; private final AccountService accountService; private final CaptchaService captchaService; private final RedirectStrategy redirectStrategy; private final AppSettings appSettings; private final UserFactory userFactory; @Autowired public AccountController(AccountService accountService, CaptchaService captchaService, RedirectStrategy redirectStrategy, AppSettings appSettings, UserFactory userFactory) { this.accountService = requireNonNull(accountService); this.captchaService = requireNonNull(captchaService); this.redirectStrategy = requireNonNull(redirectStrategy); this.appSettings = requireNonNull(appSettings); this.userFactory = requireNonNull(userFactory); } @RequestMapping(value = "/login", method = RequestMethod.GET) public String getLoginForm() { return "login-modal"; } @RequestMapping(value = "/register", method = RequestMethod.GET) public ModelAndView getRegisterForm() { ModelAndView model = new ModelAndView("register-modal"); model.addObject("countries", CountryCodes.get()); model.addObject("min_password_length", MIN_PASSWORD_LENGTH); return model; } @RequestMapping(value = "/password/reset", method = RequestMethod.GET) public String getPasswordResetForm() { return "resetpassword-modal"; } @RequestMapping(value = CHANGE_PASSWORD_RELATIVE_URI, method = RequestMethod.GET) public ModelAndView getChangePasswordForm() { ModelAndView model = new ModelAndView("view-change-password"); model.addObject("min_password_length", MIN_PASSWORD_LENGTH); return model; } @RequestMapping(value = CHANGE_PASSWORD_RELATIVE_URI, method = RequestMethod.POST) public void changePassword(@Valid ChangePasswordForm form, HttpServletRequest request, HttpServletResponse response) throws IOException { try { // Change password of current user Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { accountService.changePassword(authentication.getName(), form.getPassword1()); } // Redirect to homepage redirectStrategy.sendRedirect(request, response, "/"); } catch (Exception e) { e.printStackTrace(); } } // Spring's FormHttpMessageConverter cannot bind target classes (as ModelAttribute can) @RequestMapping(value = "/register", method = RequestMethod.POST, headers = "Content-Type=application/x-www-form-urlencoded") @ResponseBody public Map<String, String> registerUser(@Valid @ModelAttribute RegisterRequest registerRequest, @Valid @ModelAttribute CaptchaRequest captchaRequest, HttpServletRequest request) throws Exception { if (appSettings.getSignUp()) { if (!registerRequest.getPassword().equals(registerRequest.getConfirmPassword())) { throw new BindException(RegisterRequest.class, "password does not match confirm password"); } if (!captchaService.validateCaptcha(captchaRequest.getCaptcha())) { throw new CaptchaException("invalid captcha answer"); } User user = toUser(registerRequest); String activationUri = null; if (StringUtils.isEmpty(request.getHeader("X-Forwarded-Host"))) { activationUri = ServletUriComponentsBuilder.fromCurrentRequest().replacePath(URI + "/activate").build() .toUriString(); } else { String scheme = request.getHeader("X-Forwarded-Proto"); if (scheme == null) scheme = request.getScheme(); activationUri = scheme + "://" + request.getHeader("X-Forwarded-Host") + URI + "/activate"; } accountService.createUser(user, activationUri); String successMessage = appSettings .getSignUpModeration() ? REGISTRATION_SUCCESS_MESSAGE_ADMIN : REGISTRATION_SUCCESS_MESSAGE_USER; captchaService.removeCaptcha(); return Collections.singletonMap("message", successMessage); } else { throw new NoPermissionException("Self registration is disabled"); } } @RequestMapping(value = "/activate/{activationCode}", method = RequestMethod.GET) public String activateUser(@Valid @NotNull @PathVariable String activationCode, Model model) { try { accountService.activateUser(activationCode); model.addAttribute("successMessage", "Your account has been activated, you can now sign in."); } catch (RuntimeException e) { model.addAttribute("errorMessage", e.getMessage()); } return "forward:/"; } // Spring's FormHttpMessageConverter cannot bind target classes (as ModelAttribute can) @RequestMapping(value = "/password/reset", method = RequestMethod.POST, headers = "Content-Type=application/x-www-form-urlencoded") @ResponseStatus(HttpStatus.NO_CONTENT) public void resetPassword(@Valid @ModelAttribute PasswordResetRequest passwordResetRequest) { accountService.resetPassword(passwordResetRequest.getEmail()); } @ExceptionHandler(MolgenisDataAccessException.class) @ResponseStatus(value = HttpStatus.UNAUTHORIZED) private void handleMolgenisDataAccessException(MolgenisDataAccessException e) { } @ExceptionHandler(CaptchaException.class) @ResponseStatus(value = HttpStatus.BAD_REQUEST) private void handleCaptchaException(CaptchaException e) { } @ExceptionHandler(MolgenisUserException.class) @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ResponseBody public ErrorMessageResponse handleMolgenisUserException(MolgenisUserException e) { LOG.debug("", e); return new ErrorMessageResponse(Collections.singletonList(new ErrorMessage(e.getMessage()))); } @ExceptionHandler(UsernameAlreadyExistsException.class) @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ResponseBody public ErrorMessageResponse handleUsernameAlreadyExistsException(UsernameAlreadyExistsException e) { LOG.debug("", e); return new ErrorMessageResponse(Collections.singletonList(new ErrorMessage(e.getMessage()))); } @ExceptionHandler(EmailAlreadyExistsException.class) @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ResponseBody public ErrorMessageResponse handleEmailAlreadyExistsException(EmailAlreadyExistsException e) { LOG.debug("", e); return new ErrorMessageResponse(Collections.singletonList(new ErrorMessage(e.getMessage()))); } @ExceptionHandler(MolgenisDataException.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorMessageResponse handleMolgenisDataException(MolgenisDataException e) { LOG.error("", e); return new ErrorMessageResponse(Collections.singletonList(new ErrorMessage(e.getMessage()))); } @ExceptionHandler(RuntimeException.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorMessageResponse handleRuntimeException(RuntimeException e) { LOG.error("", e); return new ErrorMessageResponse(Collections.singletonList(new ErrorMessage(e.getMessage()))); } private User toUser(RegisterRequest request) { User user = userFactory.create(); user.setUsername(request.getUsername()); user.setPassword(request.getPassword()); user.setEmail(request.getEmail()); user.setPhone(request.getPhone()); user.setFax(request.getFax()); user.setTollFreePhone(request.getTollFreePhone()); user.setAddress(request.getAddress()); user.setTitle(request.getTitle()); user.setLastName(request.getLastname()); user.setFirstName(request.getFirstname()); user.setDepartment(request.getDepartment()); user.setCity(request.getCity()); user.setCountry(CountryCodes.get(request.getCountry())); user.setChangePassword(false); user.setSuperuser(false); return user; } }