diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ForgotPasswordController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ForgotPasswordController.java index c0e958a77..ce058fd22 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ForgotPasswordController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ForgotPasswordController.java @@ -1,9 +1,16 @@ package it.chalmers.gamma.adapter.primary.web; +import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; + +import it.chalmers.gamma.app.common.Email.EmailValidator; +import it.chalmers.gamma.app.user.domain.Cid.CidValidator; import it.chalmers.gamma.app.user.passwordreset.UserResetPasswordFacade; +import it.chalmers.gamma.app.validation.FailedValidation; +import it.chalmers.gamma.app.validation.SuccessfulValidation; +import it.chalmers.gamma.app.validation.ValidationResult; +import it.chalmers.gamma.app.validation.Validator; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; -import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -18,9 +25,31 @@ public ForgotPasswordController(UserResetPasswordFacade userResetPasswordFacade) this.userResetPasswordFacade = userResetPasswordFacade; } + public static final class IdentifierValidator implements Validator { + + @Override + public ValidationResult validate(String value) { + if (new CidValidator().validate(value) instanceof SuccessfulValidation) { + return new SuccessfulValidation(); + } else if (new EmailValidator().validate(value) instanceof SuccessfulValidation) { + return new SuccessfulValidation(); + } + + return new FailedValidation("Neither a valid cid or email"); + } + } + + public record ForgotPassword(@ValidatedWith(IdentifierValidator.class) String cidOrEmail) {} + @GetMapping("/forgot-password") public ModelAndView getForgotPassword( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest) { + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, + ForgotPassword form, + BindingResult bindingResult) { + if (form == null) { + form = new ForgotPassword(""); + } + ModelAndView mv = new ModelAndView(); if (htmxRequest) { @@ -30,13 +59,15 @@ public ModelAndView getForgotPassword( mv.addObject("page", "pages/forgot-password"); } - mv.addObject("form", new ForgotPassword("")); + mv.addObject("form", form); + + if (bindingResult.hasErrors()) { + mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); + } return mv; } - public record ForgotPassword(String email) {} - @PostMapping("/forgot-password") public ModelAndView sendForgotPassword( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, @@ -44,23 +75,17 @@ public ModelAndView sendForgotPassword( final BindingResult bindingResult) { ModelAndView mv = new ModelAndView(); + validateObject(form, bindingResult); + + if (bindingResult.hasErrors()) { + return getForgotPassword(htmxRequest, form, bindingResult); + } + try { - this.userResetPasswordFacade.startResetPasswordProcess(form.email); + this.userResetPasswordFacade.startResetPasswordProcess(form.cidOrEmail); mv.setViewName("redirect:forgot-password/finalize"); } catch (UserResetPasswordFacade.PasswordResetProcessException e) { mv.setViewName("redirect:forgot-password/finalize"); - } catch (IllegalArgumentException e) { - if (htmxRequest) { - mv.setViewName("pages/forgot-password"); - } else { - mv.setViewName("index"); - mv.addObject("page", "pages/forgot-password"); - } - - bindingResult.addError(new FieldError("form", "email", e.getMessage())); - - mv.addObject("form", new ForgotPassword("")); - mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); } return mv; @@ -99,7 +124,7 @@ public ModelAndView finalizeForgotPassword( ModelAndView mv = new ModelAndView(); - mv.setViewName("redirect:login"); + mv.setViewName("redirect:login?password-reset"); return mv; } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/LoginController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/LoginController.java index 514ceba46..a41b55b0f 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/LoginController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/LoginController.java @@ -19,6 +19,7 @@ public ModelAndView getLogin( @RequestParam(value = "authorizing", required = false) String authorizing, @RequestParam(value = "deleted", required = false) String deleted, @RequestParam(value = "account-created", required = false) String accountCreated, + @RequestParam(value = "password-reset", required = false) String passwordReset, @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, @RequestParam(value = "throttle", required = false) String throttle, HttpServletResponse response) { @@ -40,6 +41,7 @@ public ModelAndView getLogin( boolean isThrottled = throttle != null; boolean isDeleted = deleted != null; boolean isAccountCreated = accountCreated != null; + boolean isPasswordReset = passwordReset != null; mv.addObject("error", error); mv.addObject("logout", logout); @@ -47,6 +49,7 @@ public ModelAndView getLogin( mv.addObject("deleted", isDeleted); mv.addObject("throttle", isThrottled); mv.addObject("accountCreated", isAccountCreated); + mv.addObject("passwordReset", isPasswordReset); response.addHeader("HX-Retarget", "body"); diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/RegisterAccountController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/RegisterAccountController.java index 38a1bd8bf..d82c01f7f 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/RegisterAccountController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/RegisterAccountController.java @@ -1,5 +1,7 @@ package it.chalmers.gamma.adapter.primary.web; +import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; + import it.chalmers.gamma.app.common.Email.EmailValidator; import it.chalmers.gamma.app.user.UserCreationFacade; import it.chalmers.gamma.app.user.activation.domain.UserActivationToken.UserActivationTokenValidator; @@ -9,6 +11,7 @@ import it.chalmers.gamma.app.user.domain.LastName.LastNameValidator; import it.chalmers.gamma.app.user.domain.Nick.NickValidator; import it.chalmers.gamma.app.user.domain.UnencryptedPassword.UnencryptedPasswordValidator; +import java.time.Year; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; @@ -19,10 +22,6 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.servlet.ModelAndView; -import java.time.Year; - -import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; - @Controller public class RegisterAccountController { @@ -37,9 +36,11 @@ public RegisterAccountController(UserCreationFacade userCreationFacade) { @GetMapping("/activate-cid") public ModelAndView getActivateCid( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, ActivateCidForm form, BindingResult bindingResult) { + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, + ActivateCidForm form, + BindingResult bindingResult) { - if(form == null) { + if (form == null) { form = new ActivateCidForm(""); } @@ -51,6 +52,7 @@ public ModelAndView getActivateCid( mv.addObject("page", "register-account/activate-cid"); } + mv.addObject("form", form); mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); return mv; @@ -123,32 +125,29 @@ public ModelAndView registerAccount( try { if (!bindingResult.hasErrors()) { this.userCreationFacade.createUserWithCode( - new UserCreationFacade.NewUser( - form.password, - form.nick, - form.firstName, - form.lastName, - form.email, - form.acceptanceYear, - form.cid, - form.language), - form.code, - form.confirmPassword, - form.acceptUserAgreement); + new UserCreationFacade.NewUser( + form.password, + form.nick, + form.firstName, + form.lastName, + form.email, + form.acceptanceYear, + form.cid, + form.language), + form.code, + form.confirmPassword, + form.acceptUserAgreement); } } catch (UserCreationFacade.SomePropertyNotUniqueRuntimeException e) { bindingResult.addError( - new ObjectError( - "global", - "Please double check what you have entered. Please send an email to ita@chalmers.it if your issues persist.")); + new ObjectError( + "global", + "Please double check what you have entered. Please send an email to ita@chalmers.it if your issues persist.")); LOGGER.info( - "Some property wasn't unique when a user tried to create an account. More info on debug level..."); + "Some property wasn't unique when a user tried to create an account. More info on debug level..."); LOGGER.debug(e.getMessage()); } catch (IllegalArgumentException e) { - bindingResult.addError( - new ObjectError("global", - e.getMessage()) - ); + bindingResult.addError(new ObjectError("global", e.getMessage())); } if (bindingResult.hasErrors()) { diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetEntity.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetEntity.java similarity index 94% rename from app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetEntity.java rename to app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetEntity.java index e989b2e0a..e14b45294 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetEntity.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetEntity.java @@ -1,4 +1,4 @@ -package it.chalmers.gamma.adapter.secondary.jpa.user.password; +package it.chalmers.gamma.adapter.secondary.jpa.user; import it.chalmers.gamma.adapter.secondary.jpa.util.ImmutableEntity; import it.chalmers.gamma.app.user.domain.UserId; diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetJpaRepository.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetJpaRepository.java similarity index 85% rename from app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetJpaRepository.java rename to app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetJpaRepository.java index 2484c21ac..f91a426b6 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetJpaRepository.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetJpaRepository.java @@ -1,4 +1,4 @@ -package it.chalmers.gamma.adapter.secondary.jpa.user.password; +package it.chalmers.gamma.adapter.secondary.jpa.user; import java.util.Optional; import java.util.UUID; diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetRepositoryAdapter.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetRepositoryAdapter.java similarity index 76% rename from app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetRepositoryAdapter.java rename to app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetRepositoryAdapter.java index daa6dcc4f..140c7793c 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/password/UserPasswordResetRepositoryAdapter.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetRepositoryAdapter.java @@ -1,8 +1,7 @@ -package it.chalmers.gamma.adapter.secondary.jpa.user.password; +package it.chalmers.gamma.adapter.secondary.jpa.user; -import it.chalmers.gamma.adapter.secondary.jpa.user.UserEntity; -import it.chalmers.gamma.adapter.secondary.jpa.user.UserJpaRepository; import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.user.domain.Cid; import it.chalmers.gamma.app.user.domain.UserId; import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetRepository; import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetToken; @@ -24,15 +23,7 @@ public UserPasswordResetRepositoryAdapter( this.userPasswordResetJpaRepository = userPasswordResetJpaRepository; } - @Override - public PasswordReset createNewToken(Email email) throws UserNotFoundException { - Optional maybeUserEntity = this.userJpaRepository.findByEmail(email.value()); - - if (maybeUserEntity.isEmpty()) { - throw new UserNotFoundException(); - } - - UserEntity userEntity = maybeUserEntity.get(); + private PasswordReset createNewToken(UserEntity userEntity) { PasswordResetToken token = PasswordResetToken.generate(); UserPasswordResetEntity userPasswordResetEntity = @@ -45,7 +36,29 @@ public PasswordReset createNewToken(Email email) throws UserNotFoundException { this.userPasswordResetJpaRepository.save(userPasswordResetEntity); - return new PasswordReset(token, new UserId(userEntity.getId())); + return new PasswordReset(token, new Email(userEntity.email)); + } + + @Override + public PasswordReset createNewToken(Email email) throws UserNotFoundException { + Optional maybeUserEntity = this.userJpaRepository.findByEmail(email.value()); + + if (maybeUserEntity.isEmpty()) { + throw new UserNotFoundException(); + } + + return this.createNewToken(maybeUserEntity.get()); + } + + @Override + public PasswordReset createNewToken(Cid cid) throws UserNotFoundException { + Optional maybeUserEntity = this.userJpaRepository.findByCid(cid.value()); + + if (maybeUserEntity.isEmpty()) { + throw new UserNotFoundException(); + } + + return this.createNewToken(maybeUserEntity.get()); } @Override diff --git a/app/src/main/java/it/chalmers/gamma/app/user/UserCreationFacade.java b/app/src/main/java/it/chalmers/gamma/app/user/UserCreationFacade.java index 1dac83cc8..14ab96af0 100644 --- a/app/src/main/java/it/chalmers/gamma/app/user/UserCreationFacade.java +++ b/app/src/main/java/it/chalmers/gamma/app/user/UserCreationFacade.java @@ -1,5 +1,8 @@ package it.chalmers.gamma.app.user; +import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; +import static it.chalmers.gamma.app.authentication.AccessGuard.isNotSignedIn; + import it.chalmers.gamma.app.Facade; import it.chalmers.gamma.app.authentication.AccessGuard; import it.chalmers.gamma.app.common.Email; @@ -10,15 +13,11 @@ import it.chalmers.gamma.app.user.allowlist.AllowListRepository; import it.chalmers.gamma.app.user.domain.*; import jakarta.transaction.Transactional; +import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import java.util.UUID; - -import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; -import static it.chalmers.gamma.app.authentication.AccessGuard.isNotSignedIn; - @Service public class UserCreationFacade extends Facade { @@ -31,17 +30,18 @@ public class UserCreationFacade extends Facade { private final AllowListRepository allowListRepository; public UserCreationFacade( - AccessGuard accessGuard, - MailService mailService, - UserActivationRepository userActivationRepository, - UserRepository userRepository, - ThrottlingService throttlingService, AllowListRepository allowListRepository) { + AccessGuard accessGuard, + MailService mailService, + UserActivationRepository userActivationRepository, + UserRepository userRepository, + ThrottlingService throttlingService, + AllowListRepository allowListRepository) { super(accessGuard); this.mailService = mailService; this.userActivationRepository = userActivationRepository; this.userRepository = userRepository; this.throttlingService = throttlingService; - this.allowListRepository = allowListRepository; + this.allowListRepository = allowListRepository; } public void tryToActivateUser(String cidRaw) { @@ -56,9 +56,9 @@ public void tryToActivateUser(String cidRaw) { } else { LOGGER.info("Throttling an activation and its email..."); } - LOGGER.info("Cid {} has been activated", cid); + LOGGER.info("Cid {} has been activated", cid); } catch (UserActivationRepository.CidNotAllowedException e) { - LOGGER.info("Someone tried to activate the cid: {}", cid); + LOGGER.info("Someone tried to activate the cid: {}", cid); } } diff --git a/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/UserResetPasswordFacade.java b/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/UserResetPasswordFacade.java index 14da2e4ce..3817f5c3d 100644 --- a/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/UserResetPasswordFacade.java +++ b/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/UserResetPasswordFacade.java @@ -7,11 +7,13 @@ import it.chalmers.gamma.app.common.Email; import it.chalmers.gamma.app.mail.domain.MailService; import it.chalmers.gamma.app.throttling.ThrottlingService; +import it.chalmers.gamma.app.user.domain.Cid; import it.chalmers.gamma.app.user.domain.GammaUser; import it.chalmers.gamma.app.user.domain.UnencryptedPassword; import it.chalmers.gamma.app.user.domain.UserRepository; import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetRepository; import it.chalmers.gamma.app.user.passwordreset.domain.PasswordResetToken; +import it.chalmers.gamma.app.validation.SuccessfulValidation; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,25 +41,30 @@ public UserResetPasswordFacade( this.throttlingService = throttlingService; } - public void startResetPasswordProcess(String emailString) throws PasswordResetProcessException { + public void startResetPasswordProcess(String cidOrEmailString) + throws PasswordResetProcessException { this.accessGuard.require(isNotSignedIn()); - Email email = new Email(emailString); - try { - PasswordResetRepository.PasswordReset passwordReset = - this.passwordResetRepository.createNewToken(email); + PasswordResetRepository.PasswordReset passwordReset; + if (new Email.EmailValidator().validate(cidOrEmailString) instanceof SuccessfulValidation) { + passwordReset = this.passwordResetRepository.createNewToken(new Email(cidOrEmailString)); + } else if (new Cid.CidValidator().validate(cidOrEmailString) + instanceof SuccessfulValidation) { + passwordReset = this.passwordResetRepository.createNewToken(new Cid(cidOrEmailString)); + } else { + throw new IllegalArgumentException("Neither an email nor a cid."); + } - if (throttlingService.canProceed(passwordReset.userId().value() + "-password-reset", 3)) { - sendPasswordResetTokenMail(email, passwordReset.token()); + if (throttlingService.canProceed(passwordReset.email().value() + "-password-reset", 3)) { + sendPasswordResetTokenMail(passwordReset.email(), passwordReset.token()); } else { LOGGER.info("Throttling password reset process triggered."); } } catch (PasswordResetRepository.UserNotFoundException e) { LOGGER.debug( - "Someone tried to reset the password for the email " - + emailString - + " that doesn't exist"); + "Someone tried to reset the password for the email {} that doesn't exist", + cidOrEmailString); throw new PasswordResetProcessException(); } } diff --git a/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetRepository.java b/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetRepository.java index c5f5e6bbe..33a06aeb3 100644 --- a/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetRepository.java +++ b/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetRepository.java @@ -1,15 +1,18 @@ package it.chalmers.gamma.app.user.passwordreset.domain; import it.chalmers.gamma.app.common.Email; +import it.chalmers.gamma.app.user.domain.Cid; import it.chalmers.gamma.app.user.domain.UserId; import java.util.Optional; public interface PasswordResetRepository { - record PasswordReset(PasswordResetToken token, UserId userId) {} + record PasswordReset(PasswordResetToken token, Email email) {} PasswordReset createNewToken(Email email) throws UserNotFoundException; + PasswordReset createNewToken(Cid cid) throws UserNotFoundException; + Optional getToken(UserId id); void removeToken(PasswordResetToken token); diff --git a/app/src/main/resources/templates/pages/forgot-password.html b/app/src/main/resources/templates/pages/forgot-password.html index 520017fb8..8e42b658b 100644 --- a/app/src/main/resources/templates/pages/forgot-password.html +++ b/app/src/main/resources/templates/pages/forgot-password.html @@ -6,9 +6,9 @@ Reset password

- Please enter your email to begin the reset process. + Please enter your cid or email to begin the reset process.

-
+
diff --git a/app/src/main/resources/templates/pages/login.html b/app/src/main/resources/templates/pages/login.html index 91d9d7986..6ffd82969 100644 --- a/app/src/main/resources/templates/pages/login.html +++ b/app/src/main/resources/templates/pages/login.html @@ -39,6 +39,10 @@

Your account has been created.

+

+ Your password was reset. +

+ Register