From 16c97f8f0e4634f20d41fc73253ac7bc9b2bbfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Theodor=20Angerg=C3=A5rd?= Date: Sun, 16 Jun 2024 01:22:08 +0200 Subject: [PATCH] Reset password and create account through email and token query parameter. other things as well. --- .../adapter/primary/web/AdminController.java | 2 +- .../primary/web/AllowListController.java | 2 +- .../adapter/primary/web/ApiKeyController.java | 26 ++++--- .../primary/web/ClientsController.java | 28 ++++--- .../primary/web/ForgotPasswordController.java | 73 +++++++++++-------- .../adapter/primary/web/GdprController.java | 2 +- .../adapter/primary/web/GroupsController.java | 27 ++++--- .../web/RegisterAccountController.java | 69 ++++++++++-------- .../primary/web/SuperGroupsController.java | 34 +++++---- .../adapter/primary/web/ThymeleafAdvice.java | 10 +++ .../jpa/user/UserActivationEntity.java | 5 +- .../user/UserActivationRepositoryAdapter.java | 36 +++++---- .../jpa/user/UserPasswordResetEntity.java | 9 ++- .../user/UserPasswordResetJpaRepository.java | 2 +- .../UserPasswordResetRepositoryAdapter.java | 27 ++++--- .../app/{TokenUtils.java => Tokens.java} | 12 +-- .../gamma/app/apikey/domain/ApiKeyToken.java | 10 +-- .../gamma/app/client/domain/ClientId.java | 6 +- .../gamma/app/client/domain/ClientSecret.java | 10 +-- .../gamma/app/user/UserCreationFacade.java | 65 ++++++++++------- .../user/activation/ActivationCodeFacade.java | 12 +-- .../domain/UserActivationRepository.java | 16 +--- .../domain/UserActivationToken.java | 25 ++----- .../UserResetPasswordFacade.java | 51 ++++--------- .../domain/PasswordResetRepository.java | 7 +- .../domain/PasswordResetToken.java | 12 +-- .../bootstrap/EnsureAnAdminUserBootstrap.java | 10 +-- .../gamma/security/SecurityFiltersConfig.java | 4 + app/src/main/resources/db/migration/README.md | 23 ------ .../resources/db/migration/V2__TOKENS.sql | 2 + .../templates/pages/activation-codes.html | 2 - .../pages/finalize-forgot-password.html | 10 +-- .../templates/pages/forgot-password.html | 13 ++++ .../register-account/email-sent.html | 3 - .../register-account/register-account.html | 9 +-- 35 files changed, 322 insertions(+), 332 deletions(-) rename app/src/main/java/it/chalmers/gamma/app/{TokenUtils.java => Tokens.java} (78%) delete mode 100644 app/src/main/resources/db/migration/README.md create mode 100644 app/src/main/resources/db/migration/V2__TOKENS.sql diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/AdminController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/AdminController.java index 70df2e332..0c1a38626 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/AdminController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/AdminController.java @@ -80,6 +80,6 @@ public ModelAndView setAdmins( return this.getAdmins(htmxRequest); } - return new ModelAndView("redirect:admins"); + return new ModelAndView("redirect:/admins"); } } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/AllowListController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/AllowListController.java index 77e925d81..e2678ca8b 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/AllowListController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/AllowListController.java @@ -47,7 +47,7 @@ public ModelAndView allow( return getAllowList(htmxRequest); } - return new ModelAndView("redirect:allow-list"); + return new ModelAndView("redirect:/allow-list"); } public record AllowCidForm(String cid) {} diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ApiKeyController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ApiKeyController.java index 0de8ef713..5204911c0 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ApiKeyController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ApiKeyController.java @@ -1,19 +1,20 @@ package it.chalmers.gamma.adapter.primary.web; -import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; -import static it.chalmers.gamma.app.common.UUIDValidator.isValidUUID; - import it.chalmers.gamma.app.apikey.ApiKeyFacade; import it.chalmers.gamma.app.apikey.ApiKeySettingsFacade; import it.chalmers.gamma.app.common.PrettyName.PrettyNameValidator; import it.chalmers.gamma.app.supergroup.SuperGroupFacade; import jakarta.servlet.http.HttpServletResponse; -import java.util.*; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; +import java.util.*; + +import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; +import static it.chalmers.gamma.app.common.UUIDValidator.isValidUUID; + @Controller public class ApiKeyController { @@ -129,11 +130,8 @@ public record CreateApiKey( String enDescription, String keyType) {} - @GetMapping("/api-keys/create") - public ModelAndView getCreateApiKey( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, - CreateApiKey form, - BindingResult bindingResult) { + public ModelAndView createGetCreateApiKey( + boolean htmxRequest, CreateApiKey form, BindingResult bindingResult) { ModelAndView mv = new ModelAndView(); if (htmxRequest) { @@ -150,13 +148,19 @@ public ModelAndView getCreateApiKey( mv.addObject("form", form); mv.addObject("keyTypes", this.apiKeyFacade.getApiKeyTypes()); - if (bindingResult.hasErrors()) { + if (bindingResult != null && bindingResult.hasErrors()) { mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); } return mv; } + @GetMapping("/api-keys/create") + public ModelAndView getCreateApiKey( + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest) { + return createGetCreateApiKey(htmxRequest, null, null); + } + @PostMapping("/api-keys/create") public ModelAndView createApiKey( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, @@ -168,7 +172,7 @@ public ModelAndView createApiKey( validateObject(form, bindingResult); if (bindingResult.hasErrors()) { - return getCreateApiKey(htmxRequest, form, bindingResult); + return createGetCreateApiKey(htmxRequest, form, bindingResult); } ApiKeyFacade.CreatedApiKey createdApiKey = diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ClientsController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ClientsController.java index cbeec53d0..8ccf642d7 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ClientsController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ClientsController.java @@ -1,8 +1,5 @@ package it.chalmers.gamma.adapter.primary.web; -import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; -import static it.chalmers.gamma.app.common.UUIDValidator.isValidUUID; - import it.chalmers.gamma.app.client.ClientApprovalFacade; import it.chalmers.gamma.app.client.ClientAuthorityFacade; import it.chalmers.gamma.app.client.ClientFacade; @@ -15,13 +12,17 @@ import it.chalmers.gamma.security.authentication.AuthenticationExtractor; import it.chalmers.gamma.security.authentication.UserAuthentication; import jakarta.servlet.http.HttpServletResponse; -import java.util.*; -import java.util.stream.Collectors; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; +import java.util.*; +import java.util.stream.Collectors; + +import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; +import static it.chalmers.gamma.app.common.UUIDValidator.isValidUUID; + @Controller public class ClientsController { @@ -222,11 +223,8 @@ public void setRestrictions(List restrictions) { } } - @GetMapping("/clients/create") - public ModelAndView getCreateClient( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, - CreateClient form, - BindingResult bindingResult) { + public ModelAndView createGetCreateClient( + boolean htmxRequest, CreateClient form, BindingResult bindingResult) { ModelAndView mv = new ModelAndView(); if (form == null) { @@ -242,13 +240,19 @@ public ModelAndView getCreateClient( mv.addObject("form", form); - if (bindingResult.hasErrors()) { + if (bindingResult != null && bindingResult.hasErrors()) { mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); } return mv; } + @GetMapping("/clients/create") + public ModelAndView getCreateClient( + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest) { + return createGetCreateClient(htmxRequest, null, null); + } + @GetMapping("/clients/create/new-restriction") public ModelAndView newRestrictionRow( @RequestHeader(value = "HX-Request", required = true) boolean htmxRequest) { @@ -283,7 +287,7 @@ public ModelAndView getCreateClient( validateObject(form, bindingResult); if (bindingResult.hasErrors()) { - return getCreateClient(htmxRequest, form, bindingResult); + return createGetCreateClient(htmxRequest, form, bindingResult); } ModelAndView mv = new ModelAndView(); 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 ce058fd22..3621e0efb 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,7 +1,5 @@ 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; @@ -11,11 +9,15 @@ import it.chalmers.gamma.app.validation.Validator; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; +import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; + @Controller public class ForgotPasswordController { @@ -41,15 +43,8 @@ public ValidationResult validate(String value) { public record ForgotPassword(@ValidatedWith(IdentifierValidator.class) String cidOrEmail) {} - @GetMapping("/forgot-password") - public ModelAndView getForgotPassword( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, - ForgotPassword form, - BindingResult bindingResult) { - if (form == null) { - form = new ForgotPassword(""); - } - + public ModelAndView createGetForgotPassword( + boolean htmxRequest, ForgotPassword form, BindingResult bindingResult, boolean hasSent) { ModelAndView mv = new ModelAndView(); if (htmxRequest) { @@ -60,40 +55,43 @@ public ModelAndView getForgotPassword( } mv.addObject("form", form); + mv.addObject("hasSent", hasSent); - if (bindingResult.hasErrors()) { + if (bindingResult != null && bindingResult.hasErrors()) { mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); } return mv; } + @GetMapping("/forgot-password") + public ModelAndView getForgotPassword( + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest) { + return createGetForgotPassword(htmxRequest, new ForgotPassword(""), null, false); + } + @PostMapping("/forgot-password") public ModelAndView sendForgotPassword( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, ForgotPassword form, final BindingResult bindingResult) { - ModelAndView mv = new ModelAndView(); - validateObject(form, bindingResult); if (bindingResult.hasErrors()) { - return getForgotPassword(htmxRequest, form, bindingResult); + return createGetForgotPassword(htmxRequest, form, bindingResult, false); } try { this.userResetPasswordFacade.startResetPasswordProcess(form.cidOrEmail); - mv.setViewName("redirect:forgot-password/finalize"); } catch (UserResetPasswordFacade.PasswordResetProcessException e) { - mv.setViewName("redirect:forgot-password/finalize"); + // ignore } - return mv; + return createGetForgotPassword(htmxRequest, form, bindingResult, true); } - @GetMapping("/forgot-password/finalize") - public ModelAndView getFinalizeForgotPassword( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest) { + public ModelAndView createGetFinalizeForgotPassword( + boolean htmxRequest, FinalizeForgotPassword form, BindingResult bindingResult) { ModelAndView mv = new ModelAndView(); if (htmxRequest) { @@ -103,29 +101,42 @@ public ModelAndView getFinalizeForgotPassword( mv.addObject("page", "pages/finalize-forgot-password"); } - mv.addObject("form", new FinalizeForgotPassword("", "", "", "")); + mv.addObject("form", form); + + if (bindingResult != null && bindingResult.hasErrors()) { + mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); + } return mv; } - public record FinalizeForgotPassword( - String email, String token, String password, String confirmPassword) {} + @GetMapping("/forgot-password/finalize") + public ModelAndView getFinalizeForgotPassword( + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, + @RequestParam(value = "token", required = true) String token) { + FinalizeForgotPassword form = new FinalizeForgotPassword(token, "", ""); + + return createGetFinalizeForgotPassword(htmxRequest, form, null); + } + + public record FinalizeForgotPassword(String token, String password, String confirmPassword) {} @PostMapping("/forgot-password/finalize") public ModelAndView finalizeForgotPassword( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, - FinalizeForgotPassword form) { + FinalizeForgotPassword form, + BindingResult bindingResult) { try { this.userResetPasswordFacade.finishResetPasswordProcess( - form.email, form.token, form.password, form.confirmPassword); + form.token, form.password, form.confirmPassword); } catch (UserResetPasswordFacade.PasswordResetProcessException e) { throw new RuntimeException(e); + } catch (IllegalArgumentException e) { + bindingResult.addError(new ObjectError("global", e.getMessage())); + return createGetFinalizeForgotPassword( + htmxRequest, new FinalizeForgotPassword(form.token, "", ""), bindingResult); } - ModelAndView mv = new ModelAndView(); - - mv.setViewName("redirect:login?password-reset"); - - return mv; + return new ModelAndView("redirect:/"); } } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/GdprController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/GdprController.java index 3e06d324d..241fc5c6d 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/GdprController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/GdprController.java @@ -82,6 +82,6 @@ public ModelAndView setGdprTrained( this.userGdprTrainingFacade.updateGdprTrainedStatus(userId, false); } - return new ModelAndView("redirect:gdpr"); + return new ModelAndView("redirect:/gdpr"); } } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/GroupsController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/GroupsController.java index 5952120cd..fc7fce3ba 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/GroupsController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/GroupsController.java @@ -1,8 +1,5 @@ package it.chalmers.gamma.adapter.primary.web; -import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; -import static it.chalmers.gamma.app.common.UUIDValidator.isValidUUID; - import it.chalmers.gamma.app.common.PrettyName.PrettyNameValidator; import it.chalmers.gamma.app.group.GroupFacade; import it.chalmers.gamma.app.post.PostFacade; @@ -10,8 +7,6 @@ import it.chalmers.gamma.app.user.UserFacade; import it.chalmers.gamma.app.user.domain.Name.NameValidator; import jakarta.servlet.http.HttpServletResponse; -import java.util.*; -import java.util.stream.Collectors; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; @@ -20,6 +15,12 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; +import java.util.*; +import java.util.stream.Collectors; + +import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; +import static it.chalmers.gamma.app.common.UUIDValidator.isValidUUID; + @Controller public class GroupsController { @@ -435,11 +436,7 @@ public ModelAndView getNewMember( return mv; } - @GetMapping("/groups/create") - public ModelAndView getCreateGroup( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, - GroupForm form, - BindingResult bindingResult) { + public ModelAndView createGetCreateGroup(boolean htmxRequest, GroupForm form, BindingResult bindingResult) { ModelAndView mv = new ModelAndView(); if (htmxRequest) { @@ -456,13 +453,19 @@ public ModelAndView getCreateGroup( mv.addObject("form", form); mv.addObject("superGroups", this.superGroupFacade.getAll()); - if (bindingResult.hasErrors()) { + if (bindingResult != null && bindingResult.hasErrors()) { mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); } return mv; } + @GetMapping("/groups/create") + public ModelAndView getCreateGroup( + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest) { + return createGetCreateGroup(htmxRequest, null, null); + } + @PostMapping("/groups/create") public ModelAndView createGroup( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, @@ -472,7 +475,7 @@ public ModelAndView createGroup( validateObject(form, bindingResult); if (bindingResult.hasErrors()) { - return getCreateGroup(htmxRequest, form, bindingResult); + return createGetCreateGroup(htmxRequest, form, bindingResult); } UUID groupId = 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 d82c01f7f..53cd7cb29 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 @@ -4,7 +4,7 @@ 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; +import it.chalmers.gamma.app.user.activation.domain.UserActivationRepository; import it.chalmers.gamma.app.user.domain.AcceptanceYear.AcceptanceYearValidator; import it.chalmers.gamma.app.user.domain.Cid.CidValidator; import it.chalmers.gamma.app.user.domain.FirstName.FirstNameValidator; @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; @Controller @@ -34,16 +35,8 @@ public RegisterAccountController(UserCreationFacade userCreationFacade) { this.userCreationFacade = userCreationFacade; } - @GetMapping("/activate-cid") - public ModelAndView getActivateCid( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, - ActivateCidForm form, - BindingResult bindingResult) { - - if (form == null) { - form = new ActivateCidForm(""); - } - + public ModelAndView createGetActivateCid( + boolean htmxRequest, ActivateCidForm form, BindingResult bindingResult) { ModelAndView mv = new ModelAndView(); if (htmxRequest) { mv.setViewName("register-account/activate-cid"); @@ -52,12 +45,24 @@ public ModelAndView getActivateCid( mv.addObject("page", "register-account/activate-cid"); } + if (form == null) { + form = new ActivateCidForm(""); + } mv.addObject("form", form); - mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); + + if (bindingResult != null && bindingResult.hasErrors()) { + mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); + } return mv; } + @GetMapping("/activate-cid") + public ModelAndView getActivateCid( + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest) { + return createGetActivateCid(htmxRequest, null, null); + } + @PostMapping("/activate-cid") public ModelAndView activateCid( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, @@ -67,10 +72,10 @@ public ModelAndView activateCid( validateObject(form, bindingResult); if (bindingResult.hasErrors()) { - return getActivateCid(htmxRequest, form, bindingResult); + return createGetActivateCid(htmxRequest, form, bindingResult); } else { this.userCreationFacade.tryToActivateUser(form.cid); - return new ModelAndView("redirect:email-sent"); + return new ModelAndView("redirect:/email-sent"); } } @@ -86,18 +91,10 @@ public ModelAndView getEmailSent( return mv; } - @GetMapping("/register") public ModelAndView createGetRegister( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, - CreateAccountForm form, - BindingResult bindingResult) { + boolean htmxRequest, CreateAccountForm form, BindingResult bindingResult) { ModelAndView mv = new ModelAndView(); - if (form == null) { - form = - new CreateAccountForm("", "", "", "", "", "", "", "", Year.now().getValue(), "SV", false); - } - if (htmxRequest) { mv.setViewName("register-account/register-account"); } else { @@ -107,13 +104,23 @@ public ModelAndView createGetRegister( mv.addObject("form", form); - if (bindingResult.hasErrors()) { + if (bindingResult != null && bindingResult.hasErrors()) { mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); } return mv; } + @GetMapping("/register") + public ModelAndView getRegister( + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, + @RequestParam(value = "token", required = true) String token) { + CreateAccountForm form = + new CreateAccountForm(token, "", "", "", "", "", "", Year.now().getValue(), "SV", false); + + return createGetRegister(htmxRequest, form, null); + } + @PostMapping("/register") public ModelAndView registerAccount( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, @@ -125,14 +132,13 @@ public ModelAndView registerAccount( try { if (!bindingResult.hasErrors()) { this.userCreationFacade.createUserWithCode( - new UserCreationFacade.NewUser( + new UserCreationFacade.NewUserByCode( form.password, form.nick, form.firstName, form.lastName, form.email, form.acceptanceYear, - form.cid, form.language), form.code, form.confirmPassword, @@ -147,19 +153,22 @@ public ModelAndView registerAccount( "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())); + throw new RuntimeException(e); + } catch (UserActivationRepository.TokenNotActivatedRuntimeException e) { + bindingResult.addError( + new ObjectError( + "global", "Token not valid anymore. Please request a new registration url.")); } if (bindingResult.hasErrors()) { return createGetRegister(htmxRequest, form, bindingResult); } else { - return new ModelAndView("redirect:/login?account-created"); + return new ModelAndView("redirect:/"); } } public record CreateAccountForm( - @ValidatedWith(CidValidator.class) String cid, - @ValidatedWith(UserActivationTokenValidator.class) String code, + String code, @ValidatedWith(UnencryptedPasswordValidator.class) String password, @ValidatedWith(UnencryptedPasswordValidator.class) String confirmPassword, @ValidatedWith(NickValidator.class) String nick, diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/SuperGroupsController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/SuperGroupsController.java index 18a2fa582..8022f2874 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/SuperGroupsController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/SuperGroupsController.java @@ -1,23 +1,24 @@ package it.chalmers.gamma.adapter.primary.web; -import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; -import static it.chalmers.gamma.app.common.UUIDValidator.isValidUUID; - import it.chalmers.gamma.app.common.PrettyName.PrettyNameValidator; import it.chalmers.gamma.app.group.GroupFacade; import it.chalmers.gamma.app.supergroup.SuperGroupFacade; import it.chalmers.gamma.app.supergroup.domain.SuperGroupRepository; import it.chalmers.gamma.app.user.domain.Name.NameValidator; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.UUID; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static it.chalmers.gamma.adapter.primary.web.WebValidationHelper.validateObject; +import static it.chalmers.gamma.app.common.UUIDValidator.isValidUUID; + @Controller public class SuperGroupsController { @@ -216,11 +217,8 @@ public record CreateSuperGroupForm( String svDescription, String enDescription) {} - @GetMapping("/super-groups/create") - public ModelAndView getCreateSuperGroup( - @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, - CreateSuperGroupForm form, - BindingResult bindingResult) { + public ModelAndView createGetCreateSuperGroup( + boolean htmxRequest, CreateSuperGroupForm form, BindingResult bindingResult) { ModelAndView mv = new ModelAndView(); if (htmxRequest) { @@ -241,13 +239,19 @@ public ModelAndView getCreateSuperGroup( .sorted(Comparator.comparing(String::toLowerCase)) .toList()); - if (bindingResult.hasErrors()) { + if (bindingResult != null && bindingResult.hasErrors()) { mv.addObject(BindingResult.MODEL_KEY_PREFIX + "form", bindingResult); } return mv; } + @GetMapping("/super-groups/create") + public ModelAndView getCreateSuperGroup( + @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest) { + return createGetCreateSuperGroup(htmxRequest, null, null); + } + @PostMapping("/super-groups") public ModelAndView createSuperGroup( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, @@ -257,7 +261,7 @@ public ModelAndView createSuperGroup( validateObject(form, bindingResult); if (bindingResult.hasErrors()) { - return getCreateSuperGroup(htmxRequest, form, bindingResult); + return createGetCreateSuperGroup(htmxRequest, form, bindingResult); } try { @@ -269,7 +273,7 @@ public ModelAndView createSuperGroup( return new ModelAndView("redirect:/super-groups/" + superGroupId); } catch (SuperGroupRepository.SuperGroupAlreadyExistsException e) { bindingResult.addError(new FieldError("form", "name", e.getMessage())); - return getCreateSuperGroup(htmxRequest, form, bindingResult); + return createGetCreateSuperGroup(htmxRequest, form, bindingResult); } } } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ThymeleafAdvice.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ThymeleafAdvice.java index 407f54cb5..828908d46 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ThymeleafAdvice.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ThymeleafAdvice.java @@ -5,9 +5,13 @@ import it.chalmers.gamma.security.authentication.UserAuthentication; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.servlet.ModelAndView; @@ -46,4 +50,10 @@ public ModelAndView handleMaxSizeException(HttpServletResponse response) { return new ModelAndView("common/content-too-large"); } + + @ResponseStatus(HttpStatus.FORBIDDEN) + @ExceptionHandler(AccessDeniedException.class) + public void handleAccessDeniedException(HttpServletResponse response) throws IOException { + response.sendRedirect("/"); + } } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationEntity.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationEntity.java index 6874482fd..b4fea355c 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationEntity.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationEntity.java @@ -22,11 +22,8 @@ public class UserActivationEntity extends ImmutableEntity { protected UserActivationEntity() {} - protected UserActivationEntity(Cid cid) { + protected UserActivationEntity(Cid cid, UserActivationToken token) { this.cid = cid.getValue(); - } - - public void setToken(UserActivationToken token) { this.token = token.value(); } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationRepositoryAdapter.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationRepositoryAdapter.java index d0796b3bf..22f372c13 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationRepositoryAdapter.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserActivationRepositoryAdapter.java @@ -6,6 +6,7 @@ import it.chalmers.gamma.app.user.activation.domain.UserActivationRepository; import it.chalmers.gamma.app.user.activation.domain.UserActivationToken; import it.chalmers.gamma.app.user.domain.Cid; +import jakarta.transaction.Transactional; import java.util.List; import java.util.Optional; import org.springframework.dao.DataIntegrityViolationException; @@ -25,13 +26,12 @@ public UserActivationRepositoryAdapter(UserActivationJpaRepository userActivatio @Override public UserActivationToken createActivationToken(Cid cid) throws CidNotAllowedException { + UserActivationToken token = UserActivationToken.generate(); + UserActivationEntity entity = this.userActivationJpaRepository .findById(cid.value()) - .orElse(new UserActivationEntity(cid)); - - UserActivationToken token = UserActivationToken.generate(); - entity.setToken(token); + .orElse(new UserActivationEntity(cid, token)); try { this.userActivationJpaRepository.saveAndFlush(entity); @@ -47,13 +47,6 @@ public UserActivationToken createActivationToken(Cid cid) throws CidNotAllowedEx } } - @Override - public Optional get(Cid cid) { - return this.userActivationJpaRepository - .findById(cid.value()) - .map(UserActivationEntity::toDomain); - } - @Override public List getAll() { return this.userActivationJpaRepository.findAll().stream() @@ -62,11 +55,22 @@ public List getAll() { } @Override - public Cid getByToken(UserActivationToken token) { - return this.userActivationJpaRepository - .findByToken(token.value()) - .orElseThrow(TokenNotActivatedException::new) - .cid(); + public boolean doesTokenExist(UserActivationToken token) { + return this.userActivationJpaRepository.findByToken(token.value()).isPresent(); + } + + @Override + @Transactional + public Cid useToken(UserActivationToken token) { + Optional maybeActivation = + this.userActivationJpaRepository.findByToken(token.value()); + if (maybeActivation.isEmpty()) { + throw new TokenNotActivatedRuntimeException(); + } + + this.userActivationJpaRepository.deleteById(maybeActivation.get().getId()); + + return new Cid(maybeActivation.get().getId()); } @Override diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetEntity.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetEntity.java index e14b45294..842da341a 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetEntity.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetEntity.java @@ -12,7 +12,7 @@ @Entity @Table(name = "g_password_reset") -public class UserPasswordResetEntity extends ImmutableEntity { +public class UserPasswordResetEntity extends ImmutableEntity { @Id @Column(name = "user_id", columnDefinition = "uuid") @@ -24,12 +24,13 @@ public class UserPasswordResetEntity extends ImmutableEntity { protected UserPasswordResetEntity() {} @Override - public UserId getId() { - return new UserId(this.userId); + public UUID getId() { + return this.userId; } public PasswordReset toDomain() { - return new PasswordReset(this.getId(), new PasswordResetToken(this.token), this.getCreatedAt()); + return new PasswordReset( + new UserId(this.userId), new PasswordResetToken(this.token), this.getCreatedAt()); } public String getToken() { diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetJpaRepository.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetJpaRepository.java index f91a426b6..5fa9fb1bc 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetJpaRepository.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetJpaRepository.java @@ -8,7 +8,7 @@ @Repository public interface UserPasswordResetJpaRepository extends JpaRepository { - Optional findByUserId(UUID userId); + Optional findByToken(String token); void deleteByToken(String token); } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetRepositoryAdapter.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetRepositoryAdapter.java index 140c7793c..0f0436a13 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetRepositoryAdapter.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/user/UserPasswordResetRepositoryAdapter.java @@ -26,12 +26,11 @@ public UserPasswordResetRepositoryAdapter( private PasswordReset createNewToken(UserEntity userEntity) { PasswordResetToken token = PasswordResetToken.generate(); - UserPasswordResetEntity userPasswordResetEntity = - this.userPasswordResetJpaRepository - .findByUserId(userEntity.getId()) - .orElse(new UserPasswordResetEntity()); + this.userPasswordResetJpaRepository.deleteById(userEntity.id); - userPasswordResetEntity.userId = userEntity.getId(); + UserPasswordResetEntity userPasswordResetEntity = new UserPasswordResetEntity(); + + userPasswordResetEntity.userId = userEntity.id; userPasswordResetEntity.token = token.value(); this.userPasswordResetJpaRepository.save(userPasswordResetEntity); @@ -62,14 +61,22 @@ public PasswordReset createNewToken(Cid cid) throws UserNotFoundException { } @Override - public Optional getToken(UserId id) { - return userPasswordResetJpaRepository - .findByUserId(id.value()) - .map(userPasswordResetEntity -> new PasswordResetToken(userPasswordResetEntity.getToken())); + public boolean doesTokenExist(PasswordResetToken token) { + return this.userPasswordResetJpaRepository.findByToken(token.value()).isPresent(); } @Override - public void removeToken(PasswordResetToken token) { + @Transactional + public UserId useToken(PasswordResetToken token) { + Optional maybeReset = + this.userPasswordResetJpaRepository.findByToken(token.value()); + + if (maybeReset.isEmpty()) { + throw new TokenNotFoundRuntimeException(); + } + this.userPasswordResetJpaRepository.deleteByToken(token.value()); + + return new UserId(maybeReset.get().userId); } } diff --git a/app/src/main/java/it/chalmers/gamma/app/TokenUtils.java b/app/src/main/java/it/chalmers/gamma/app/Tokens.java similarity index 78% rename from app/src/main/java/it/chalmers/gamma/app/TokenUtils.java rename to app/src/main/java/it/chalmers/gamma/app/Tokens.java index 964a8e271..8d3ea2bbb 100644 --- a/app/src/main/java/it/chalmers/gamma/app/TokenUtils.java +++ b/app/src/main/java/it/chalmers/gamma/app/Tokens.java @@ -1,20 +1,20 @@ package it.chalmers.gamma.app; +import java.security.SecureRandom; import java.util.Arrays; -import java.util.Random; import java.util.stream.Collectors; -public final class TokenUtils { +public final class Tokens { - private TokenUtils() {} + private Tokens() {} - public static String generateToken(int length, CharacterTypes... types) { + public static String generate(int length, CharacterTypes... types) { String characters = Arrays.stream(types).map(CharacterTypes::getCharacters).collect(Collectors.joining()); - Random rand = new Random(); + SecureRandom rand = new SecureRandom(); StringBuilder code = new StringBuilder(); for (int i = 0; i < length; i++) { - code.append(characters.charAt(rand.nextInt(characters.length() - 1))); + code.append(characters.charAt(rand.nextInt(characters.length()))); } return code.toString(); } diff --git a/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java b/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java index 3c83a2449..ed5ecefdd 100644 --- a/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java +++ b/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java @@ -1,6 +1,6 @@ package it.chalmers.gamma.app.apikey.domain; -import it.chalmers.gamma.app.TokenUtils; +import it.chalmers.gamma.app.Tokens; import java.util.Objects; import java.util.regex.Pattern; import org.springframework.security.crypto.password.PasswordEncoder; @@ -21,11 +21,11 @@ public record GeneratedApiKeyToken(ApiKeyToken apiKeyToken, String rawToken) {} public static GeneratedApiKeyToken generate(PasswordEncoder passwordEncoder) { String value = - TokenUtils.generateToken( + Tokens.generate( 32, - TokenUtils.CharacterTypes.LOWERCASE, - TokenUtils.CharacterTypes.UPPERCASE, - TokenUtils.CharacterTypes.NUMBERS); + Tokens.CharacterTypes.LOWERCASE, + Tokens.CharacterTypes.UPPERCASE, + Tokens.CharacterTypes.NUMBERS); return new GeneratedApiKeyToken(new ApiKeyToken(passwordEncoder.encode(value)), value); } diff --git a/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientId.java b/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientId.java index f0211f5b7..6fea4797a 100644 --- a/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientId.java +++ b/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientId.java @@ -1,6 +1,6 @@ package it.chalmers.gamma.app.client.domain; -import it.chalmers.gamma.app.TokenUtils; +import it.chalmers.gamma.app.Tokens; import java.util.Objects; import java.util.regex.Pattern; @@ -19,9 +19,7 @@ public record ClientId(String value) { } public static ClientId generate() { - String id = - TokenUtils.generateToken( - 30, TokenUtils.CharacterTypes.UPPERCASE, TokenUtils.CharacterTypes.NUMBERS); + String id = Tokens.generate(30, Tokens.CharacterTypes.UPPERCASE, Tokens.CharacterTypes.NUMBERS); return new ClientId(id); } } diff --git a/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java b/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java index b7794e753..69a8ddd12 100644 --- a/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java +++ b/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java @@ -1,6 +1,6 @@ package it.chalmers.gamma.app.client.domain; -import it.chalmers.gamma.app.TokenUtils; +import it.chalmers.gamma.app.Tokens; import java.util.Objects; import java.util.regex.Pattern; import org.springframework.security.crypto.password.PasswordEncoder; @@ -21,11 +21,11 @@ public record GeneratedClientSecret(ClientSecret clientSecret, String rawSecret) public static GeneratedClientSecret generate(PasswordEncoder passwordEncoder) { String value = - TokenUtils.generateToken( + Tokens.generate( 32, - TokenUtils.CharacterTypes.LOWERCASE, - TokenUtils.CharacterTypes.UPPERCASE, - TokenUtils.CharacterTypes.NUMBERS); + Tokens.CharacterTypes.LOWERCASE, + Tokens.CharacterTypes.UPPERCASE, + Tokens.CharacterTypes.NUMBERS); return new GeneratedClientSecret(new ClientSecret(passwordEncoder.encode(value)), value); } 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 14ab96af0..9a39e50da 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 @@ -16,6 +16,7 @@ import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @@ -28,6 +29,7 @@ public class UserCreationFacade extends Facade { private final UserRepository userRepository; private final ThrottlingService throttlingService; private final AllowListRepository allowListRepository; + private final String baseUrl; public UserCreationFacade( AccessGuard accessGuard, @@ -35,13 +37,15 @@ public UserCreationFacade( UserActivationRepository userActivationRepository, UserRepository userRepository, ThrottlingService throttlingService, - AllowListRepository allowListRepository) { + AllowListRepository allowListRepository, + @Value("${application.base-url}") String baseUrl) { super(accessGuard); this.mailService = mailService; this.userActivationRepository = userActivationRepository; this.userRepository = userRepository; this.throttlingService = throttlingService; this.allowListRepository = allowListRepository; + this.baseUrl = baseUrl; } public void tryToActivateUser(String cidRaw) { @@ -90,7 +94,7 @@ public UUID createUser(NewUser newUser) throws EmailNotUniqueException, CidNotUn @Transactional public void createUserWithCode( - NewUser data, String token, String confirmPassword, boolean acceptsUserAgreement) { + NewUserByCode data, String token, String confirmPassword, boolean acceptsUserAgreement) { this.accessGuard.require(isNotSignedIn()); if (!data.password.equals(confirmPassword)) { @@ -101,40 +105,45 @@ public void createUserWithCode( throw new IllegalArgumentException("Must accept user agreement"); } - Cid tokenCid = this.userActivationRepository.getByToken(new UserActivationToken(token)); - - if (tokenCid.value().equals(data.cid)) { - Cid cid = new Cid(data.cid); - - try { - this.userRepository.create( - new GammaUser( - UserId.generate(), - cid, - new Nick(data.nick), - new FirstName(data.firstName), - new LastName(data.lastName), - new AcceptanceYear(data.acceptanceYear), - Language.valueOf(data.language), - new UserExtended(new Email(data.email), 0, false, null)), - new UnencryptedPassword(data.password)); - } catch (UserRepository.CidAlreadyInUseException - | UserRepository.EmailAlreadyInUseException e) { - throw new SomePropertyNotUniqueRuntimeException(); - } + Cid cid = this.userActivationRepository.useToken(new UserActivationToken(token)); - this.userActivationRepository.removeActivation(cid); - this.allowListRepository.remove(cid); + try { + this.userRepository.create( + new GammaUser( + UserId.generate(), + cid, + new Nick(data.nick), + new FirstName(data.firstName), + new LastName(data.lastName), + new AcceptanceYear(data.acceptanceYear), + Language.valueOf(data.language), + new UserExtended(new Email(data.email), 0, false, null)), + new UnencryptedPassword(data.password)); + } catch (UserRepository.CidAlreadyInUseException + | UserRepository.EmailAlreadyInUseException e) { + throw new SomePropertyNotUniqueRuntimeException(); } + + this.userActivationRepository.removeActivation(cid); + this.allowListRepository.remove(cid); } private void sendEmail(Cid cid, UserActivationToken userActivationToken) { String to = cid.getValue() + "@" + MAIL_POSTFIX; - String code = userActivationToken.value(); - String message = "Your code to Gamma is: " + code; - this.mailService.sendMail(to, "Gamma activation code", message); + String resetUrl = baseUrl + "/register?token=" + userActivationToken.value(); + String message = "Follow this link to finish up creating your account: " + resetUrl; + this.mailService.sendMail(to, "Gamma activation url", message); } + public record NewUserByCode( + String password, + String nick, + String firstName, + String lastName, + String email, + int acceptanceYear, + String language) {} + public record NewUser( String password, String nick, diff --git a/app/src/main/java/it/chalmers/gamma/app/user/activation/ActivationCodeFacade.java b/app/src/main/java/it/chalmers/gamma/app/user/activation/ActivationCodeFacade.java index 209cec5b6..28408dcc1 100644 --- a/app/src/main/java/it/chalmers/gamma/app/user/activation/ActivationCodeFacade.java +++ b/app/src/main/java/it/chalmers/gamma/app/user/activation/ActivationCodeFacade.java @@ -9,7 +9,6 @@ import it.chalmers.gamma.app.user.domain.Cid; import java.time.Instant; import java.util.List; -import java.util.Optional; import org.springframework.stereotype.Service; @Service @@ -23,12 +22,6 @@ public ActivationCodeFacade( this.userActivationRepository = userActivationRepository; } - public Optional get(String cid) { - this.accessGuard.require(isAdmin()); - - return this.userActivationRepository.get(new Cid(cid)).map(UserActivationDTO::new); - } - public List getAllUserActivations() { this.accessGuard.require(isAdmin()); @@ -41,10 +34,9 @@ public void removeUserActivation(String cid) { this.userActivationRepository.removeActivation(new Cid(cid)); } - public record UserActivationDTO(String cid, String token, Instant createdAt) { + public record UserActivationDTO(String cid, Instant createdAt) { public UserActivationDTO(UserActivation userActivation) { - this( - userActivation.cid().value(), userActivation.token().value(), userActivation.createdAt()); + this(userActivation.cid().value(), userActivation.createdAt()); } } } diff --git a/app/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationRepository.java b/app/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationRepository.java index f7be0d75b..7cf7da3c0 100644 --- a/app/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationRepository.java +++ b/app/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationRepository.java @@ -2,28 +2,20 @@ import it.chalmers.gamma.app.user.domain.Cid; import java.util.List; -import java.util.Optional; public interface UserActivationRepository { - /** - * Creates an activation rawToken that is connected to the cid. If there already is a rawToken - * generated, then a new one will be generated. - * - * @param cid A cid that has been allowed - * @return A rawToken that can be used to create an account with the given cid - */ UserActivationToken createActivationToken(Cid cid) throws CidNotAllowedException; - Optional get(Cid cid); - List getAll(); - Cid getByToken(UserActivationToken token) throws TokenNotActivatedException; + boolean doesTokenExist(UserActivationToken token); + + Cid useToken(UserActivationToken token); void removeActivation(Cid cid) throws CidNotActivatedException; - class TokenNotActivatedException extends RuntimeException {} + class TokenNotActivatedRuntimeException extends RuntimeException {} class CidNotActivatedException extends RuntimeException {} diff --git a/app/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationToken.java b/app/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationToken.java index a981ed866..bf3a4893f 100644 --- a/app/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationToken.java +++ b/app/src/main/java/it/chalmers/gamma/app/user/activation/domain/UserActivationToken.java @@ -1,32 +1,17 @@ package it.chalmers.gamma.app.user.activation.domain; -import static it.chalmers.gamma.app.validation.ValidationHelper.*; +import static it.chalmers.gamma.app.Tokens.CharacterTypes.*; -import it.chalmers.gamma.app.TokenUtils; -import it.chalmers.gamma.app.validation.ValidationResult; -import it.chalmers.gamma.app.validation.Validator; -import java.util.regex.Pattern; +import it.chalmers.gamma.app.Tokens; +import java.util.Objects; public record UserActivationToken(String value) { public UserActivationToken { - throwIfFailed(new UserActivationTokenValidator().validate(value)); + Objects.requireNonNull(value); } public static UserActivationToken generate() { - String value = TokenUtils.generateToken(9, TokenUtils.CharacterTypes.NUMBERS); - return new UserActivationToken(value); - } - - public static final class UserActivationTokenValidator implements Validator { - - private static final Pattern tokenPattern = Pattern.compile("^[0-9]{9}$"); - - @Override - public ValidationResult validate(String value) { - return withValidators( - IS_NOT_EMPTY, MATCHES_REGEX.apply(new RegexMatcher(tokenPattern, "Must be 9 number"))) - .validate(value); - } + return new UserActivationToken(Tokens.generate(100, UPPERCASE, NUMBERS, LOWERCASE)); } } 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 3817f5c3d..1f85956c0 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 @@ -8,15 +8,15 @@ 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.UserId; 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; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @@ -27,18 +27,21 @@ public class UserResetPasswordFacade extends Facade { private final UserRepository userRepository; private final PasswordResetRepository passwordResetRepository; private final ThrottlingService throttlingService; + private final String baseUrl; public UserResetPasswordFacade( AccessGuard accessGuard, MailService mailService, UserRepository userRepository, PasswordResetRepository passwordResetRepository, - ThrottlingService throttlingService) { + ThrottlingService throttlingService, + @Value("${application.base-url}") String baseUrl) { super(accessGuard); this.mailService = mailService; this.userRepository = userRepository; this.passwordResetRepository = passwordResetRepository; this.throttlingService = throttlingService; + this.baseUrl = baseUrl; } public void startResetPasswordProcess(String cidOrEmailString) @@ -70,52 +73,28 @@ public void startResetPasswordProcess(String cidOrEmailString) } public void finishResetPasswordProcess( - String emailString, String inputTokenRaw, String newPassword, String confirmPassword) + String inputToken, String newPassword, String confirmPassword) throws PasswordResetProcessException { this.accessGuard.require(isNotSignedIn()); if (!newPassword.equals(confirmPassword)) { - throw new IllegalArgumentException("please properly confirm password"); + throw new IllegalArgumentException("Please properly confirm password"); } - Email email = new Email(emailString); + UserId userId = this.passwordResetRepository.useToken(new PasswordResetToken(inputToken)); - Optional maybeUser = this.userRepository.get(email); - - if (maybeUser.isEmpty()) { - LOGGER.debug( - "Someone tried to finish the reset value process for " - + emailString - + " that doesn't exist"); - throw new PasswordResetProcessException(); - } - - GammaUser user = maybeUser.get(); - Optional maybeToken = this.passwordResetRepository.getToken(user.id()); - - if (maybeToken.isEmpty()) { - LOGGER.debug("No code exists for the user " + user); - throw new PasswordResetProcessException(); - } - - PasswordResetToken token = maybeToken.get(); - PasswordResetToken inputToken = new PasswordResetToken(inputTokenRaw); - - if (token.equals(inputToken)) { - this.passwordResetRepository.removeToken(token); - this.userRepository.setPassword(user.id(), new UnencryptedPassword(newPassword)); - } else { - LOGGER.debug("Incorrect value reset code for user " + user); - throw new PasswordResetProcessException(); - } + this.userRepository.setPassword(userId, new UnencryptedPassword(newPassword)); } private void sendPasswordResetTokenMail(Email email, PasswordResetToken token) { String subject = "Password reset for Account at IT division of Chalmers"; + + String resetUrl = this.baseUrl + "/forgot-password/finalize?token=" + token.value(); + String message = "A password reset have been requested for this account, if you have not requested " - + "this mail, feel free to ignore it. \n Your reset code : " - + token.value(); + + "this mail, feel free to ignore it. \n Click here to reset password: " + + resetUrl; this.mailService.sendMail(email.value(), subject, message); } 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 33a06aeb3..7cabe06b0 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 @@ -3,7 +3,6 @@ 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 { @@ -13,9 +12,11 @@ record PasswordReset(PasswordResetToken token, Email email) {} PasswordReset createNewToken(Cid cid) throws UserNotFoundException; - Optional getToken(UserId id); + boolean doesTokenExist(PasswordResetToken token); - void removeToken(PasswordResetToken token); + UserId useToken(PasswordResetToken token); class UserNotFoundException extends Exception {} + + class TokenNotFoundRuntimeException extends RuntimeException {} } diff --git a/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetToken.java b/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetToken.java index 51050724e..2a7eb1225 100644 --- a/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetToken.java +++ b/app/src/main/java/it/chalmers/gamma/app/user/passwordreset/domain/PasswordResetToken.java @@ -1,21 +1,17 @@ package it.chalmers.gamma.app.user.passwordreset.domain; -import it.chalmers.gamma.app.TokenUtils; +import static it.chalmers.gamma.app.Tokens.CharacterTypes.*; + +import it.chalmers.gamma.app.Tokens; import java.util.Objects; -import org.springframework.web.util.HtmlUtils; public record PasswordResetToken(String value) { public PasswordResetToken { Objects.requireNonNull(value); - - value = HtmlUtils.htmlEscape(value, "UTF-8"); } public static PasswordResetToken generate() { - String value = - TokenUtils.generateToken( - 20, TokenUtils.CharacterTypes.UPPERCASE, TokenUtils.CharacterTypes.NUMBERS); - return new PasswordResetToken(value); + return new PasswordResetToken(Tokens.generate(100, UPPERCASE, NUMBERS, LOWERCASE)); } } diff --git a/app/src/main/java/it/chalmers/gamma/bootstrap/EnsureAnAdminUserBootstrap.java b/app/src/main/java/it/chalmers/gamma/bootstrap/EnsureAnAdminUserBootstrap.java index de5991eed..0e6bd5894 100644 --- a/app/src/main/java/it/chalmers/gamma/bootstrap/EnsureAnAdminUserBootstrap.java +++ b/app/src/main/java/it/chalmers/gamma/bootstrap/EnsureAnAdminUserBootstrap.java @@ -1,6 +1,6 @@ package it.chalmers.gamma.bootstrap; -import it.chalmers.gamma.app.TokenUtils; +import it.chalmers.gamma.app.Tokens; import it.chalmers.gamma.app.admin.domain.AdminRepository; import it.chalmers.gamma.app.common.Email; import it.chalmers.gamma.app.user.domain.*; @@ -62,11 +62,11 @@ public void ensureAnAdminUser() { password = "password1337"; } else { password = - TokenUtils.generateToken( + Tokens.generate( 75, - TokenUtils.CharacterTypes.LOWERCASE, - TokenUtils.CharacterTypes.UPPERCASE, - TokenUtils.CharacterTypes.NUMBERS); + Tokens.CharacterTypes.LOWERCASE, + Tokens.CharacterTypes.UPPERCASE, + Tokens.CharacterTypes.NUMBERS); } UserId adminId = UserId.generate(); diff --git a/app/src/main/java/it/chalmers/gamma/security/SecurityFiltersConfig.java b/app/src/main/java/it/chalmers/gamma/security/SecurityFiltersConfig.java index d86216da5..6aac19e07 100644 --- a/app/src/main/java/it/chalmers/gamma/security/SecurityFiltersConfig.java +++ b/app/src/main/java/it/chalmers/gamma/security/SecurityFiltersConfig.java @@ -206,6 +206,10 @@ SecurityFilterChain webSecurityFilterChain( .cors(Customizer.withDefaults()) .csrf((csrf) -> csrf.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())) .requestCache(cacheConfig -> cacheConfig.requestCache(requestCache)) + .exceptionHandling( + exceptionConfig -> + exceptionConfig.accessDeniedHandler( + (request, response, accessDeniedException) -> response.sendRedirect("/"))) .headers( headers -> headers.contentSecurityPolicy( diff --git a/app/src/main/resources/db/migration/README.md b/app/src/main/resources/db/migration/README.md deleted file mode 100644 index b2922e18c..000000000 --- a/app/src/main/resources/db/migration/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# How to write migrations - -Create a sql file with a V that is +1 of the last one. - -Here're some examples with an sql file named `V99__website-changes.sql`: - -```sql - --- Add column -ALTER TABLE website - ADD test_column varchar(100) not null; - --- Rename column -ALTER TABLE website - RENAME COLUMN name TO name_new; - --- Modify column -ALTER TABLE website - ALTER COLUMN pretty_name TYPE varchar(200); - -``` - -More examples here, check for PostgresSQL: https://www.postgresql.org/docs/9.4/ddl-alter.html diff --git a/app/src/main/resources/db/migration/V2__TOKENS.sql b/app/src/main/resources/db/migration/V2__TOKENS.sql new file mode 100644 index 000000000..13733f527 --- /dev/null +++ b/app/src/main/resources/db/migration/V2__TOKENS.sql @@ -0,0 +1,2 @@ +ALTER TABLE g_user_activation + ALTER COLUMN token TYPE VARCHAR(100); \ No newline at end of file diff --git a/app/src/main/resources/templates/pages/activation-codes.html b/app/src/main/resources/templates/pages/activation-codes.html index 658dc1a1f..fbf402a2e 100644 --- a/app/src/main/resources/templates/pages/activation-codes.html +++ b/app/src/main/resources/templates/pages/activation-codes.html @@ -4,7 +4,6 @@ Cid - Token Created at @@ -12,7 +11,6 @@ -
diff --git a/app/src/main/resources/templates/pages/finalize-forgot-password.html b/app/src/main/resources/templates/pages/finalize-forgot-password.html index e0365b147..e945a3d46 100644 --- a/app/src/main/resources/templates/pages/finalize-forgot-password.html +++ b/app/src/main/resources/templates/pages/finalize-forgot-password.html @@ -5,16 +5,10 @@
Finalize resetting password
-

- You should have received an email with a token for resetting your password. -

-

- Please check your spam email if you can't find it. You might also have typed your cid/email incorrectly. -

-
-
+
+
diff --git a/app/src/main/resources/templates/pages/forgot-password.html b/app/src/main/resources/templates/pages/forgot-password.html index 8e42b658b..5554ade76 100644 --- a/app/src/main/resources/templates/pages/forgot-password.html +++ b/app/src/main/resources/templates/pages/forgot-password.html @@ -5,6 +5,19 @@
Reset password
+ +

+ + You should have received an email with a link for resetting your password. + +

+

+ + Please check your spam email if you can't find it. You might also have typed your cid/email incorrectly. + +

+
+

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

diff --git a/app/src/main/resources/templates/register-account/email-sent.html b/app/src/main/resources/templates/register-account/email-sent.html index a63ca3da8..c4181bbd8 100644 --- a/app/src/main/resources/templates/register-account/email-sent.html +++ b/app/src/main/resources/templates/register-account/email-sent.html @@ -13,9 +13,6 @@ I have not received a code - - I have received a code - \ No newline at end of file diff --git a/app/src/main/resources/templates/register-account/register-account.html b/app/src/main/resources/templates/register-account/register-account.html index 4f54f2e6f..8a4117a2f 100644 --- a/app/src/main/resources/templates/register-account/register-account.html +++ b/app/src/main/resources/templates/register-account/register-account.html @@ -5,14 +5,13 @@
Finish setting up your account
-
-
-
-
+ +
-
+
+