From 9f657e17c22c928fdc3943d0b8113451802c7aa0 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Mon, 30 Sep 2024 13:47:33 +0200 Subject: [PATCH] Help user to choose a valid password in the reset password input (#819) * Overhaul Reset Password Functionality * Harmonize password error message if a too short password is provided in SetNewPasswordComponent * Update Javadocs to allow for more clear distinguish between new components * Update JD For cardlayout * Update password reset error message if a password was too short during registration and setting a new password * Add helper text to password field during user registration * Address code review * Remove CardLayout.java and copy paste css class name * Move JD description --------- Co-authored-by: Tobias Koch --- .../themes/datamanager/components/div.css | 30 +-- .../themes/datamanager/components/main.css | 24 +++ .../views/landing/LandingPageLayout.java | 3 +- .../datamanager/views/layouts/BoxLayout.java | 182 ---------------- .../login/newpassword/NewPasswordHandler.java | 28 --- .../login/newpassword/NewPasswordLayout.java | 198 ------------------ .../newpassword/NewPasswordSetLayout.java | 38 ---- .../login/passwordreset/LinkSentLayout.java | 36 ---- .../NewPasswordSetComponent.java | 44 ++++ .../ResetEmailSentComponent.java | 42 ++++ .../passwordreset/ResetPasswordComponent.java | 128 +++++++++++ .../passwordreset/ResetPasswordLayout.java | 172 --------------- .../passwordreset/ResetPasswordMain.java | 117 +++++++++++ .../SetNewPasswordComponent.java | 110 ++++++++++ .../passwordreset/SetNewPasswordMain.java | 128 +++++++++++ .../views/register/RegistrationOrcIdMain.java | 28 ++- .../register/UserRegistrationComponent.java | 11 +- .../views/register/UserRegistrationMain.java | 32 ++- .../UserRegistrationOrcIdComponent.java | 4 +- 19 files changed, 656 insertions(+), 699 deletions(-) delete mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/layouts/BoxLayout.java delete mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordHandler.java delete mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordLayout.java delete mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordSetLayout.java delete mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/LinkSentLayout.java create mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/NewPasswordSetComponent.java create mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetEmailSentComponent.java create mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordComponent.java delete mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordLayout.java create mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordMain.java create mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/SetNewPasswordComponent.java create mode 100644 user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/SetNewPasswordMain.java diff --git a/user-interface/frontend/themes/datamanager/components/div.css b/user-interface/frontend/themes/datamanager/components/div.css index 946b01b6e..64a133b1a 100644 --- a/user-interface/frontend/themes/datamanager/components/div.css +++ b/user-interface/frontend/themes/datamanager/components/div.css @@ -36,6 +36,21 @@ align-items: baseline; } +.card-layout { + background-color: var(--lumo-base-color); + border: var(--lumo-contrast-10pct); + border-radius: var(--lumo-border-radius-m); + box-shadow: var(--lumo-box-shadow-l); + font-size: var(--lumo-font-size-s); + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: var(--lumo-space-s); + width: clamp(300px, 300px, 15vw); + padding-bottom: var(--lumo-space-l); + padding-inline: var(--lumo-space-l); +} + .disclaimer { display: flex; justify-content: center; @@ -162,18 +177,3 @@ gap: var(--lumo-space-xs); white-space: nowrap; } - -.user-registration-component { - background-color: var(--lumo-base-color); - border: var(--lumo-contrast-10pct); - border-radius: var(--lumo-border-radius-m); - box-shadow: var(--lumo-box-shadow-l); - font-size: var(--lumo-font-size-s); - box-sizing: border-box; - display: flex; - flex-direction: column; - gap: var(--lumo-space-s); - width: clamp(300px, 300px, 15vw); - padding-bottom: var(--lumo-space-l); - padding-inline: var(--lumo-space-l); -} diff --git a/user-interface/frontend/themes/datamanager/components/main.css b/user-interface/frontend/themes/datamanager/components/main.css index fd796bd99..c50ed3a96 100644 --- a/user-interface/frontend/themes/datamanager/components/main.css +++ b/user-interface/frontend/themes/datamanager/components/main.css @@ -425,6 +425,22 @@ grid-area: sampledetails; } +.main.reset-password { + grid-template-columns: auto; + grid-template-rows: auto; + justify-content: center; + /*Should be moved to the parent layout once Login and forgot password components are overhauled*/ + padding-bottom: var(--lumo-space-m); +} + +.main.set-new-password { + grid-template-columns: auto; + grid-template-rows: auto; + justify-content: center; + /*Should be moved to the parent layout once Login and forgot password components are overhauled*/ + padding-bottom: var(--lumo-space-m); +} + .main.user-profile { grid-template-columns: minmax(min-content, auto); grid-template-rows: minmax(min-content, auto); @@ -432,6 +448,14 @@ "user-profile-component" } +.main.user-registration { + grid-template-columns: auto; + grid-template-rows: auto; + justify-content: center; + /*Should be moved to the parent layout once Login and forgot password components are overhauled*/ + padding-bottom: var(--lumo-space-m); +} + /*Large devices (large desktops, 1200px and up)*/ @media only screen and (max-width: 1200px) { .main.experiment { diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/landing/LandingPageLayout.java b/user-interface/src/main/java/life/qbic/datamanager/views/landing/LandingPageLayout.java index c5dffef3f..732892522 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/landing/LandingPageLayout.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/landing/LandingPageLayout.java @@ -3,7 +3,6 @@ import com.vaadin.flow.component.Component; import com.vaadin.flow.component.HasElement; import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; @@ -65,7 +64,7 @@ private HorizontalLayout createHeaderButtonLayout() { } private void styleHeaderButtons() { - login.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + login.addClassName("primary"); } /** diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/layouts/BoxLayout.java b/user-interface/src/main/java/life/qbic/datamanager/views/layouts/BoxLayout.java deleted file mode 100644 index 9ba8cce32..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/views/layouts/BoxLayout.java +++ /dev/null @@ -1,182 +0,0 @@ -package life.qbic.datamanager.views.layouts; - -import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.Text; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.orderedlayout.FlexComponent; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import life.qbic.datamanager.views.notifications.DisplayMessage; - -/** - * Box Layout - * - *

A box with a shadow containing a title, description, a layout for fields, a layout for buttons - * and a span to add links. - * Furthermore, the description text can be toggled visible or invisible - * - * @since 1.0.0 - */ -public class BoxLayout extends VerticalLayout { - - private H2 layoutTitle; - private Text descriptionText; - private VerticalLayout notificationLayout; - private VerticalLayout fieldLayout; - private VerticalLayout textLayout; - private VerticalLayout buttonLayout; - private Span linkSpan; - - private final VerticalLayout contentLayout; - - public BoxLayout() { - this.addClassName("grid"); - contentLayout = new VerticalLayout(); - - initLayout(); - styleLayout(); - } - - private void initLayout() { - layoutTitle = new H2("Set Title"); - - textLayout = new VerticalLayout(); - descriptionText = new Text("Enter description text"); - textLayout.add(descriptionText); - notificationLayout = new VerticalLayout(); - fieldLayout = new VerticalLayout(); - buttonLayout = new VerticalLayout(); - - linkSpan = new Span(); - add(contentLayout); - } - - private void styleLayout() { - styleNotificationLayout(); - styleFieldLayout(); - styleFormLayout(); - styleButtonLayout(); - styleDescriptionText(); - - setSizeFull(); - setAlignItems(FlexComponent.Alignment.CENTER); - setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); - } - - private void styleFormLayout() { - contentLayout.setPadding(false); - contentLayout.setMargin(false); - contentLayout.addClassNames( - "bg-base", - "border", - "rounded-m", - "border-contrast-10", - "box-border", - "flex", - "flex-col", - "w-full", - "text-s", - "shadow-l", - "min-width-300px", - "max-width-15vw", - "pb-l", - "pr-l", - "pl-l"); - contentLayout.add(layoutTitle, notificationLayout, descriptionText, fieldLayout, buttonLayout, - linkSpan); - } - - private void styleFieldLayout() { - fieldLayout.setSpacing(false); - fieldLayout.setMargin(false); - fieldLayout.setPadding(false); - } - - private void styleButtonLayout() { - buttonLayout.setSpacing(false); - buttonLayout.setMargin(false); - buttonLayout.setPadding(false); - } - - private void styleNotificationLayout() { - notificationLayout.setSpacing(false); - notificationLayout.setMargin(false); - notificationLayout.setPadding(false); - } - - private void styleDescriptionText() { - textLayout.addClassName("text-contrast-70"); - } - - /** - * Sets the title text - * - * @param text The text for the title - */ - public void setTitleText(String text) { - layoutTitle.setText(text); - } - - /** - * Adds the field components to the field layout - * - * @param fields The fields could be TextFields, EmailFields, PasswordFields - */ - public void addFields(Component... fields) { - fieldLayout.add(fields); - } - - /** - * Adds buttons to the button layout - * - * @param buttons The buttons that need to be part of the layout - */ - public void addButtons(Button... buttons) { - buttonLayout.add(buttons); - } - - /** - * Sets the description text - * - * @param text The text that allows to enter a description of the process - */ - public void setDescriptionText(String text) { - descriptionText.setText(text); - } - - /** - * Adds a DisplayMessage {@link DisplayMessage} based notification to the BoxLayout - * - * @param displayMessage The notification to be shown to the viewer. - */ - public void setNotification(DisplayMessage displayMessage) { - notificationLayout.add(displayMessage); - } - - /** - * Removes all DisplayMessage {@link DisplayMessage} based Notifications from the BoxLayout - */ - public void removeNotifications() { - notificationLayout.removeAll(); - } - - /** - * Toggles the description text visible or invisible - * - * @param visible The visibility status of the text - */ - public void setDescriptionTextVisible(boolean visible) { - descriptionText.setVisible(visible); - } - - /** - * Span that will hold content like small texts and links that should be not so present as a - * button - * - * @param components Components like Text, RouterLink, or Tertiary Buttons - */ - public void addLinkSpanContent(Component... components) { - linkSpan.add(components); - } -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordHandler.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordHandler.java deleted file mode 100644 index ec4f3b7f0..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -package life.qbic.datamanager.views.login.newpassword; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -/** - * Handles setting new passwords - * - *

When a new password is set the handler triggers the use case to update the users password

- * - * @since 1.0.0 - */ -@Component -public class NewPasswordHandler { - - private final String passwordResetQueryParameter; - - @Autowired - NewPasswordHandler(@Value("${routing.password-reset.reset-parameter}") String newPasswordParam) { - this.passwordResetQueryParameter = newPasswordParam; - } - - public String passwordResetQueryParameter() { - return passwordResetQueryParameter; - } - -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordLayout.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordLayout.java deleted file mode 100644 index b2c741e74..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordLayout.java +++ /dev/null @@ -1,198 +0,0 @@ -package life.qbic.datamanager.views.login.newpassword; - -import static life.qbic.logging.service.LoggerFactory.logger; - -import com.vaadin.flow.component.Key; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.textfield.PasswordField; -import com.vaadin.flow.router.BeforeEvent; -import com.vaadin.flow.router.HasUrlParameter; -import com.vaadin.flow.router.OptionalParameter; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.flow.spring.annotation.UIScope; -import java.io.Serial; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Predicate; -import life.qbic.application.commons.Result; -import life.qbic.datamanager.views.AppRoutes; -import life.qbic.datamanager.views.landing.LandingPageLayout; -import life.qbic.datamanager.views.layouts.BoxLayout; -import life.qbic.identity.application.user.IdentityService; -import life.qbic.identity.domain.model.EncryptedPassword.PasswordValidationException; -import life.qbic.logging.api.Logger; -import org.apache.commons.lang3.NotImplementedException; -import org.springframework.beans.factory.annotation.Autowired; - - -/** - * Defines the look of the password reset layout. - * - * @since 1.0.0 - */ -@PageTitle("New Password") -@Route(value = AppRoutes.NEW_PASSWORD, layout = LandingPageLayout.class) -@AnonymousAllowed -@UIScope -public class NewPasswordLayout extends VerticalLayout implements HasUrlParameter { - - private static final Logger log = logger(NewPasswordLayout.class); - - @Serial - private static final long serialVersionUID = 4884878964166607894L; - private final IdentityService identityService; - private PasswordField newPassword; - private Button sendButton; - - private BoxLayout provideNewPasswordLayout; - private NewPasswordSetLayout newPasswordSetLayout; - - private transient String currentUserId; - - private final NewPasswordHandler newPasswordHandler; - - public NewPasswordLayout(@Autowired NewPasswordHandler newPasswordHandler, - @Autowired IdentityService identityService) { - this.newPasswordHandler = Objects.requireNonNull(newPasswordHandler); - this.identityService = Objects.requireNonNull(identityService); - initLayout(); - styleLayout(); - addClickListeners(); - } - - public PasswordField newPassword() { - return newPassword; - } - - public Button sendButton() { - return sendButton; - } - - public BoxLayout provideNewPasswordLayout() { - return provideNewPasswordLayout; - } - - public NewPasswordSetLayout newPasswordSetLayout() { - return newPasswordSetLayout; - } - - private void initLayout() { - initPasswordSetLayout(); - - initEnterEmailLayout(); - - add(provideNewPasswordLayout, newPasswordSetLayout); - } - - private void initEnterEmailLayout() { - provideNewPasswordLayout = new BoxLayout(); - - provideNewPasswordLayout.setTitleText("Set new password"); - provideNewPasswordLayout.setDescriptionText("Please provide a new password for your account:"); - - newPassword = new PasswordField("Password"); - provideNewPasswordLayout.addFields(newPassword); - - createSendButton(); - provideNewPasswordLayout.addButtons(sendButton); - - } - - private void initPasswordSetLayout() { - newPasswordSetLayout = new NewPasswordSetLayout(); - newPasswordSetLayout.setVisible(false); - } - - private void styleLayout() { - - styleFieldLayout(); - styleSendButton(); - setAlignItems(Alignment.CENTER); - setJustifyContentMode(JustifyContentMode.CENTER); - } - - private void createSendButton() { - sendButton = new Button("Send"); - } - - private void styleFieldLayout() { - newPassword.setWidthFull(); - } - - private void styleSendButton() { - sendButton.setWidthFull(); - sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - } - - @Override - public void setParameter(BeforeEvent beforeEvent, @OptionalParameter String parameter) { - handle(beforeEvent); - } - - public void handle(BeforeEvent beforeEvent) { - Map> params = beforeEvent.getLocation().getQueryParameters() - .getParameters(); - var resetParam = params.keySet().stream() - .filter(entry -> Objects.equals( - entry, newPasswordHandler.passwordResetQueryParameter())).findAny(); - if (resetParam.isPresent()) { - currentUserId = params.get(newPasswordHandler.passwordResetQueryParameter()).get(0); - } else { - throw new NotImplementedException(); - } - } - - private void addClickListeners() { - sendButton().addClickListener(buttonClickEvent -> { - Result applicationResponse = identityService.newUserPassword( - currentUserId, - newPassword().getValue().toCharArray()); - if (applicationResponse.isError()) { - handleNewPasswordError(applicationResponse); - } - handleSuccess(); - } - ); - sendButton().addClickShortcut(Key.ENTER); - - newPasswordSetLayout().loginButton().addClickListener( - buttonClickEvent -> - newPasswordSetLayout().getUI() - .ifPresent(ui -> ui.navigate("login"))); - } - - private void handleNewPasswordError(Result applicationResponse) { - Predicate isPasswordValidationException = e -> e instanceof PasswordValidationException; - applicationResponse - .onErrorMatching(isPasswordValidationException, ignored -> { - log.error("Could not set new password for user."); - onPasswordValidationFailure(); - }) - .onErrorMatching(isPasswordValidationException.negate(), ignored -> { - log.error("Unexpected failure on password reset for user."); - onUnexpectedFailure(); - }); - } - - private void handleSuccess() { - onSuccessfulNewPassword(); - } - - public void onSuccessfulNewPassword() { - provideNewPasswordLayout().setVisible(false); - newPasswordSetLayout().setVisible(true); - } - - public void onPasswordValidationFailure() { - throw new UnsupportedOperationException(); - } - - public void onUnexpectedFailure() { - throw new UnsupportedOperationException(); - } -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordSetLayout.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordSetLayout.java deleted file mode 100644 index fde1646e8..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/views/login/newpassword/NewPasswordSetLayout.java +++ /dev/null @@ -1,38 +0,0 @@ -package life.qbic.datamanager.views.login.newpassword; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import life.qbic.datamanager.views.layouts.BoxLayout; - -/** - * Defines the look of the password reset layout. - * - * @since 1.0.0 - */ -public class NewPasswordSetLayout extends BoxLayout { - - private Button loginButton; - - public NewPasswordSetLayout() { - fillLayoutComponents(); - } - - private void fillLayoutComponents() { - setTitleText("New password saved!"); - setDescriptionText("You can now log in with your new password."); - - loginButton = new Button("Login"); - addButtons(loginButton); - - styleButtons(); - } - - private void styleButtons() { - loginButton.setWidthFull(); - loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - } - - public Button loginButton() { - return loginButton; - } -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/LinkSentLayout.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/LinkSentLayout.java deleted file mode 100644 index 059169c42..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/LinkSentLayout.java +++ /dev/null @@ -1,36 +0,0 @@ -package life.qbic.datamanager.views.login.passwordreset; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import life.qbic.datamanager.views.layouts.BoxLayout; - -/** - * Defines the look of the password reset layout. - * - * @since 1.0.0 - */ -public class LinkSentLayout extends BoxLayout { - - public Button loginButton; - - public LinkSentLayout() { - fillLayoutComponents(); - } - - private void fillLayoutComponents() { - setTitleText("Email has been sent!"); - setDescriptionText( - "Please check your inbox and follow the instructions to reset your password."); - - loginButton = new Button("Login"); - addButtons(loginButton); - - styleButtons(); - } - - private void styleButtons() { - loginButton.setWidthFull(); - loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - } - -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/NewPasswordSetComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/NewPasswordSetComponent.java new file mode 100644 index 000000000..f39824928 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/NewPasswordSetComponent.java @@ -0,0 +1,44 @@ +package life.qbic.datamanager.views.login.passwordreset; + +import com.vaadin.flow.component.ClickEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import java.io.Serial; + +/** + * Nwe Password Set Component + *

+ * Card Stylized component similar to {@link com.vaadin.flow.component.login.LoginOverlay} + * component. Informing the user that a reset password email was sent and directing her to the login + * layout + */ +@SpringComponent +@UIScope +public class NewPasswordSetComponent extends Div { + + @Serial + private static final long serialVersionUID = -1138757655198857262L; + + private final Button loginButton = new Button("Login"); + + public NewPasswordSetComponent() { + addClassName("new-password-set-component"); + loginButton.addClassName("primary"); + Div introduction = new Div(); + introduction.add("You can now log in with your new password."); + introduction.addClassName("introduction"); + H2 titleSpan = new H2("New Password saved!"); + addClassName("card-layout"); + add(titleSpan, introduction, loginButton); + } + + public void addLoginButtonListener(ComponentEventListener> listener) { + loginButton.addClickShortcut(Key.ENTER); + loginButton.addClickListener(listener); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetEmailSentComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetEmailSentComponent.java new file mode 100644 index 000000000..ab3d09f19 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetEmailSentComponent.java @@ -0,0 +1,42 @@ +package life.qbic.datamanager.views.login.passwordreset; + +import com.vaadin.flow.component.ClickEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import java.io.Serial; + +/** + * Reset Email Sent Component + *

+ * Card Stylized component similar to {@link com.vaadin.flow.component.login.LoginOverlay} + * component. Informing the user that a reset password email was sent and directing her to the login + * layout + */ +@SpringComponent +@UIScope +public class ResetEmailSentComponent extends Div { + + @Serial + private static final long serialVersionUID = -1138757655198857262L; + + private final Button loginButton = new Button("Login"); + + + public ResetEmailSentComponent() { + loginButton.addClassName("primary"); + Div introduction = new Div(); + introduction.add("Please check your inbox and follow the instructions to reset your password."); + introduction.addClassName("introduction"); + H2 titleSpan = new H2("Email has been sent"); + addClassName("card-layout"); + add(titleSpan, introduction, loginButton); + } + + public void addLoginButtonListener(ComponentEventListener> listener) { + loginButton.addClickListener(listener); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordComponent.java new file mode 100644 index 000000000..8527c97df --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordComponent.java @@ -0,0 +1,128 @@ +package life.qbic.datamanager.views.login.passwordreset; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.validator.EmailValidator; +import com.vaadin.flow.router.RouterLink; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import java.io.Serial; +import java.util.Objects; +import life.qbic.datamanager.views.notifications.ErrorMessage; +import life.qbic.datamanager.views.register.UserRegistrationMain; +import life.qbic.identity.api.UserInformationService; + +/** + * Reset Password Component + *

+ * Card Stylized component similar to {@link com.vaadin.flow.component.login.LoginOverlay} + * component, Providing the input fields necessary to enable a user to specify his email for which + * the password should be reset + */ +@SpringComponent +@UIScope +public class ResetPasswordComponent extends Div { + + @Serial + private static final long serialVersionUID = 6918803421532658723L; + + /*Validation via Binder does not show error message if an email field is used, see + * https://github.com/vaadin/flow-components/issues/4618 for details */ + private final TextField emailField = new TextField("Email"); + private final Button confirmButton = new Button("Send"); + private final Div notificationLayout = new Div(); + private final Binder emailBinder = new Binder<>(String.class); + private final transient UserInformationService userInformationService; + + public ResetPasswordComponent(UserInformationService userInformationService) { + this.userInformationService = Objects.requireNonNull(userInformationService, + "userInformationService is required"); + addClassName("reset-password-component"); + confirmButton.addClassName("primary"); + Div introduction = new Div(); + introduction.add( + "Enter the mail address associated with your account and we'll send you a link to reset your password:"); + introduction.addClassName("introduction"); + H2 titleSpan = new H2("Reset Password"); + add(titleSpan, notificationLayout, introduction, emailField, confirmButton); + setFieldValidation(); + addRegistrationButtonListener(); + RouterLink routerLink = new RouterLink("Register", UserRegistrationMain.class); + Span registerSpan = new Span(new Text("Don't have an account? "), routerLink); + registerSpan.addClassName("registration-link"); + addClassName("card-layout"); + add(registerSpan); + } + + private void setFieldValidation() { + emailField.setRequired(true); + emailBinder.forField(emailField) + .withValidator(new EmailValidator("Please provide a valid email address")) + .withValidator((Validator) (value, context) -> { + if (userInformationService.isEmailAvailable(value)) { + return ValidationResult.error("No user with the provided mail address is known."); + } else { + return ValidationResult.ok(); + } + }) + .bind(value -> value, (bean, value) -> { + }); + } + + private void addRegistrationButtonListener() { + confirmButton.addClickShortcut(Key.ENTER); + confirmButton.addClickListener(event -> { + clearNotifications(); + emailBinder.validate(); + if (emailBinder.isValid()) { + fireEvent(new ResetPasswordEvent(this, event.isFromClient(), emailField.getValue())); + } + }); + } + + public void clearNotifications() { + notificationLayout.removeAll(); + } + + public void showError(String title, String description) { + clearNotifications(); + ErrorMessage errorMessage = new ErrorMessage(title, description); + notificationLayout.add(errorMessage); + } + + public void addResetPasswordListener(ComponentEventListener listener) { + addListener(ResetPasswordEvent.class, listener); + } + + public static class ResetPasswordEvent extends ComponentEvent { + + private final String email; + + /** + * Creates a new event using the given source and indicator whether the event originated from + * the client side or the server side. + * + * @param source the source component + * @param fromClient true if the event originated from the client + * side, false otherwise + */ + public ResetPasswordEvent(ResetPasswordComponent source, boolean fromClient, String email) { + super(source, fromClient); + this.email = email; + } + + public String getEmail() { + return email; + } + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordLayout.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordLayout.java deleted file mode 100644 index 9c422693b..000000000 --- a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordLayout.java +++ /dev/null @@ -1,172 +0,0 @@ -package life.qbic.datamanager.views.login.passwordreset; - -import com.vaadin.flow.component.Key; -import com.vaadin.flow.component.Text; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.orderedlayout.FlexComponent; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.textfield.EmailField; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouterLink; -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.flow.spring.annotation.UIScope; -import java.util.Objects; -import life.qbic.application.commons.ApplicationResponse; -import life.qbic.datamanager.views.AppRoutes; -import life.qbic.datamanager.views.landing.LandingPageLayout; -import life.qbic.datamanager.views.layouts.BoxLayout; -import life.qbic.datamanager.views.notifications.ErrorMessage; -import life.qbic.datamanager.views.register.UserRegistrationMain; -import life.qbic.identity.application.user.IdentityService; -import life.qbic.identity.application.user.UserNotFoundException; -import life.qbic.identity.domain.model.EmailAddress; -import org.springframework.beans.factory.annotation.Autowired; - - -/** - * Defines the look of the password reset layout. - * - * @since 1.0.0 - */ -@PageTitle("Reset Password") -@Route(value = AppRoutes.RESET_PASSWORD, layout = LandingPageLayout.class) -@AnonymousAllowed -@UIScope -public class ResetPasswordLayout extends VerticalLayout { - - private final IdentityService identityService; - public EmailField email; - public Button sendButton; - public Span registerSpan; - public BoxLayout enterEmailLayout; - public LinkSentLayout linkSentLayout; - - public ResetPasswordLayout(@Autowired IdentityService identityService) { - this.identityService = Objects.requireNonNull(identityService); - initLayout(); - styleLayout(); - addClickListeners(); - } - - - private void initLayout() { - initLinkSentLayout(); - - initEnterEmailLayout(); - - add(enterEmailLayout, linkSentLayout); - } - - private void initEnterEmailLayout() { - enterEmailLayout = new BoxLayout(); - - enterEmailLayout.setTitleText("Reset password"); - enterEmailLayout.setDescriptionText( - "Enter the mail address associated with your account and we'll send you a link to reset your password:"); - - email = new EmailField("Email"); - enterEmailLayout.addFields(email); - - createSendButton(); - enterEmailLayout.addButtons(sendButton); - - createSpan(); - enterEmailLayout.addLinkSpanContent(registerSpan); - } - - private void initLinkSentLayout() { - linkSentLayout = new LinkSentLayout(); - linkSentLayout.setVisible(false); - } - - private void styleLayout() { - - styleFieldLayout(); - styleSendButton(); - setAlignItems(FlexComponent.Alignment.CENTER); - setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); - } - - private void createSpan() { - RouterLink routerLink = new RouterLink("Register", UserRegistrationMain.class); - registerSpan = new Span(new Text("Don't have an account? "), routerLink); - registerSpan.addClassName("registration-link"); - } - - private void createSendButton() { - sendButton = new Button("Send"); - } - - private void styleFieldLayout() { - email.setWidthFull(); - } - - private void styleSendButton() { - sendButton.setWidthFull(); - sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - } - - private void addClickListeners() { - sendButton.addClickListener( - buttonClickEvent -> { - clearNotifications(); - resetPassword(email.getValue()); - }); - sendButton.addClickShortcut(Key.ENTER); - - linkSentLayout.loginButton.addClickListener( - buttonClickEvent -> - linkSentLayout - .getUI() - .ifPresent(ui -> ui.navigate("login"))); - } - - private void resetPassword(String value) { - var response = identityService.requestPasswordReset(value); - if (response.hasFailures()) { - onPasswordResetFailed(response); - } else { - onPasswordResetSucceeded(); - } - } - - public void clearNotifications() { - enterEmailLayout.removeNotifications(); - } - - public void showError(String title, String description) { - clearNotifications(); - ErrorMessage errorMessage = new ErrorMessage(title, description); - enterEmailLayout.setNotification(errorMessage); - } - - private void showPasswordResetFailedError(String error, String description) { - showError(error, description); - } - - public void onPasswordResetSucceeded() { - linkSentLayout.setVisible(true); - enterEmailLayout.setVisible(false); - } - - public void onPasswordResetFailed(ApplicationResponse response) { - for (RuntimeException failure : response.failures()) { - if (failure instanceof EmailAddress.EmailValidationException) { - showPasswordResetFailedError("Invalid mail address format", - "Please provide a valid mail address."); - } else if (failure instanceof UserNotFoundException) { - showPasswordResetFailedError( - "User not found", "No user with the provided mail address is known."); - } else if (failure instanceof IdentityService.UserNotActivatedException) { - showPasswordResetFailedError("User not active", - "Please activate your account first to reset the password."); - } else { - showPasswordResetFailedError( - "An unexpected error occurred", "Please contact support@qbic.zendesk.com for help."); - } - } - } -} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordMain.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordMain.java new file mode 100644 index 000000000..32d906b1e --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/ResetPasswordMain.java @@ -0,0 +1,117 @@ +package life.qbic.datamanager.views.login.passwordreset; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.spring.annotation.UIScope; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import life.qbic.datamanager.views.AppRoutes; +import life.qbic.datamanager.views.general.Main; +import life.qbic.datamanager.views.landing.LandingPageLayout; +import life.qbic.identity.application.user.IdentityService; +import life.qbic.identity.application.user.UserNotFoundException; +import life.qbic.identity.domain.model.EmailAddress.EmailValidationException; +import life.qbic.logging.api.Logger; +import life.qbic.logging.service.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Defines the look of the password reset layout. + * It hosts the {@link ResetPasswordComponent} to enable the user to input the email account which + * should receive a password reset email and a {@link ResetEmailSentComponent} component informing + * the user that an email has been sent to the provided email account /b> + * + * @since 1.0.0 + */ +@PageTitle("Reset Password") +@Route(value = AppRoutes.RESET_PASSWORD, layout = LandingPageLayout.class) +@AnonymousAllowed +@UIScope +public class ResetPasswordMain extends Main implements BeforeEnterObserver { + + private static final Logger log = + LoggerFactory.logger(ResetPasswordMain.class.getName()); + private final transient IdentityService identityService; + private final ResetEmailSentComponent resetEmailSentComponent; + private final ResetPasswordComponent resetPasswordComponent; + + public ResetPasswordMain(@Autowired IdentityService identityService, + @Autowired ResetPasswordComponent resetPasswordComponent, + @Autowired ResetEmailSentComponent resetEmailSentComponent) { + this.identityService = Objects.requireNonNull(identityService, + "Identity service cannot be null"); + this.resetPasswordComponent = Objects.requireNonNull(resetPasswordComponent, + "ResetPasswordComponent cannot be null"); + this.resetEmailSentComponent = Objects.requireNonNull(resetEmailSentComponent, + "ResetEmailSentComponent cannot be null"); + resetPasswordComponent.addResetPasswordListener(event -> resetPassword(event.getEmail())); + resetEmailSentComponent.addLoginButtonListener(event -> { + UI.getCurrent().navigate(AppRoutes.LOGIN); + resetEmailSentComponent.setVisible(false); + resetPasswordComponent.setVisible(true); + }); + addClassName("reset-password"); + add(resetEmailSentComponent, resetPasswordComponent); + log.debug(String.format( + "New instance for %s(#%s) created with %s(#%s) and %s(#%s)", + this.getClass().getSimpleName(), System.identityHashCode(this), + resetPasswordComponent.getClass().getSimpleName(), + System.identityHashCode(resetPasswordComponent), + resetEmailSentComponent.getClass().getSimpleName(), + System.identityHashCode(resetEmailSentComponent))); + } + + private void resetPassword(String email) { + identityService.requestPasswordReset(email) + .ifSuccessOrElse(response -> { + resetPasswordComponent.clearNotifications(); + resetPasswordComponent.setVisible(false); + resetEmailSentComponent.setVisible(true); + }, response -> handleRegistrationFailure(response.failures())); + } + + private void handleRegistrationFailure(List exceptionList) { + if (exceptionList.isEmpty()) { + return; + } + for (RuntimeException e : exceptionList) { + if (e instanceof EmailValidationException) { + resetPasswordComponent.showError("Invalid mail address format", + "Please provide a valid mail address."); + break; + } else if (e instanceof UserNotFoundException) { + resetPasswordComponent.showError( + "User not found", "No user with the provided mail address is known."); + break; + } else if (e instanceof IdentityService.UserNotActivatedException) { + resetPasswordComponent.showError("User not active", + "Please activate your account first to reset the password."); + break; + } else { + resetPasswordComponent.showError( + "An unexpected error occurred", "Please contact support@qbic.zendesk.com for help."); + break; + } + } + String allErrorMessages = exceptionList.stream().map(Throwable::getMessage) + .collect(Collectors.joining("\n")); + log.error(allErrorMessages); + exceptionList.forEach(e -> log.debug(e.getMessage(), e)); + } + + /** + * Callback executed before navigation to attaching Component chain is made. + * + * @param event before navigation event with event details + */ + @Override + public void beforeEnter(BeforeEnterEvent event) { + resetPasswordComponent.setVisible(true); + resetEmailSentComponent.setVisible(false); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/SetNewPasswordComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/SetNewPasswordComponent.java new file mode 100644 index 000000000..5d8b30167 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/SetNewPasswordComponent.java @@ -0,0 +1,110 @@ +package life.qbic.datamanager.views.login.passwordreset; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import java.io.Serial; +import life.qbic.datamanager.views.notifications.ErrorMessage; + +/** + * Set New Password Component + *

+ * Card Stylized component similar to {@link com.vaadin.flow.component.login.LoginOverlay} + * component, Providing the input fields necessary to enable a user to set a new password + */ +@AnonymousAllowed +@UIScope +@SpringComponent +public class SetNewPasswordComponent extends Div { + + @Serial + private static final long serialVersionUID = 4482422913026333378L; + + private final PasswordField password = new PasswordField("Password"); + + private final Button confirmButton = new Button("Confirm"); + + private final Div notificationLayout = new Div(); + + private final Binder passwordResetBinder = new Binder<>(String.class); + + public SetNewPasswordComponent() { + addClassName("set-new-password-component"); + addClassName("card-layout"); + confirmButton.addClassName("primary"); + password.setHelperText("Please provide a password with at least 12 characters"); + Div introduction = new Div(); + introduction.add("Please provide a new password for your account:"); + introduction.addClassName("introduction"); + H2 titleSpan = new H2("Set New Password"); + add(titleSpan, notificationLayout, introduction, password, confirmButton); + setFieldValidation(); + addConfirmButtonListeners(); + } + + private void setFieldValidation() { + passwordResetBinder.forField(password) + .asRequired("Please provide a password") + .withValidator( + name -> name.strip().length() >= 12, + "Password does not contain at least 12 characters") + .bind(value -> value, (bean, value) -> { + }); + } + + private void addConfirmButtonListeners() { + confirmButton.addClickShortcut(Key.ENTER); + confirmButton.addClickListener(event -> { + passwordResetBinder.validate(); + if (passwordResetBinder.isValid()) { + clearNotifications(); + fireEvent(new SetNewPasswordEvent(this, event.isFromClient(), password.getValue())); + } + }); + } + + private void clearNotifications() { + notificationLayout.removeAll(); + } + + public void showError(String title, String description) { + clearNotifications(); + ErrorMessage errorMessage = new ErrorMessage(title, description); + notificationLayout.add(errorMessage); + } + + public void addSetNewPasswordListener(ComponentEventListener listener) { + addListener(SetNewPasswordEvent.class, listener); + } + + public static class SetNewPasswordEvent extends ComponentEvent { + + private final String password; + + /** + * Creates a new event using the given source and indicator whether the event originated from + * the client side or the server side. + * + * @param source the source component + * @param fromClient true if the event originated from the client + * side, false otherwise + */ + public SetNewPasswordEvent(SetNewPasswordComponent source, boolean fromClient, + String password) { + super(source, fromClient); + this.password = password; + } + + public String getPassword() { + return password; + } + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/SetNewPasswordMain.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/SetNewPasswordMain.java new file mode 100644 index 000000000..791d1df9c --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/login/passwordreset/SetNewPasswordMain.java @@ -0,0 +1,128 @@ +package life.qbic.datamanager.views.login.passwordreset; + +import static life.qbic.logging.service.LoggerFactory.logger; + +import com.vaadin.flow.router.BeforeEvent; +import com.vaadin.flow.router.HasUrlParameter; +import com.vaadin.flow.router.OptionalParameter; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import java.io.Serial; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import life.qbic.application.commons.Result; +import life.qbic.datamanager.views.AppRoutes; +import life.qbic.datamanager.views.general.Main; +import life.qbic.datamanager.views.landing.LandingPageLayout; +import life.qbic.datamanager.views.login.LoginLayout; +import life.qbic.datamanager.views.login.passwordreset.SetNewPasswordComponent.SetNewPasswordEvent; +import life.qbic.identity.application.user.IdentityService; +import life.qbic.identity.domain.model.EncryptedPassword.PasswordValidationException; +import life.qbic.logging.api.Logger; +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +/** + * Set New Password Main + *

+ * Main component hosting the components necessary for a user to reset her password {@link SetNewPasswordComponent} a + * and informing her that her provided input is now set as a new password for her account + * {@link NewPasswordSetComponent} + * component. + */ +@PageTitle("New Password") +@Route(value = AppRoutes.NEW_PASSWORD, layout = LandingPageLayout.class) +@AnonymousAllowed +@UIScope +@SpringComponent +public class SetNewPasswordMain extends Main implements HasUrlParameter { + + private static final Logger log = logger(SetNewPasswordMain.class); + @Serial + private static final long serialVersionUID = 2083979684773021467L; + + private final SetNewPasswordComponent setNewPasswordComponent; + private final NewPasswordSetComponent newPasswordSetComponent; + private final transient IdentityService identityService; + @Value("${routing.password-reset.reset-parameter}") + String newPasswordParam; + private String currentUserId; + + public SetNewPasswordMain(@Autowired IdentityService identityService, + @Autowired SetNewPasswordComponent setNewPasswordComponent, + @Autowired NewPasswordSetComponent newPasswordSetComponent) { + this.identityService = Objects.requireNonNull(identityService, + "Identity service cannot be null"); + this.setNewPasswordComponent = Objects.requireNonNull(setNewPasswordComponent, + "SetNewPasswordComponent cannot be null"); + this.newPasswordSetComponent = Objects.requireNonNull(newPasswordSetComponent, + "NewPasswordSetComponent cannot be null"); + setNewPasswordComponent.addSetNewPasswordListener(this::handleSetNewPassword); + newPasswordSetComponent.addLoginButtonListener(event -> getUI().orElseThrow().navigate( + LoginLayout.class)); + add(setNewPasswordComponent, newPasswordSetComponent); + addClassName("set-new-password"); + log.debug(String.format( + "New instance for %s(#%s) created with %s(#%s) and %s(#%s)", + this.getClass().getSimpleName(), System.identityHashCode(this), + setNewPasswordComponent.getClass().getSimpleName(), + System.identityHashCode(setNewPasswordComponent), + newPasswordSetComponent.getClass().getSimpleName(), + System.identityHashCode(newPasswordSetComponent))); + } + + private void handleSetNewPassword(SetNewPasswordEvent event) { + var result = identityService.newUserPassword(currentUserId, + event.getPassword().toCharArray()); + if (result.isValue()) { + setNewPasswordComponent.setVisible(false); + newPasswordSetComponent.setVisible(true); + } + if (result.isError()) { + handleNewPasswordError(result); + } + } + + @Override + public void setParameter(BeforeEvent beforeEvent, @OptionalParameter String parameter) { + setNewPasswordComponent.setVisible(true); + newPasswordSetComponent.setVisible(false); + retrieveUserIdFromUrl(beforeEvent); + } + + public void retrieveUserIdFromUrl(BeforeEvent beforeEvent) { + Map> params = beforeEvent.getLocation().getQueryParameters() + .getParameters(); + var resetParam = params.keySet().stream() + .filter(entry -> Objects.equals( + entry, newPasswordParam)).findAny(); + if (resetParam.isPresent()) { + currentUserId = params.get(newPasswordParam).get(0); + } else { + throw new NotImplementedException(); + } + } + + private void handleNewPasswordError(Result applicationResponse) { + Predicate isPasswordValidationException = e -> e instanceof PasswordValidationException; + /*These Cases should not happen anymore since we validate before we send the event, + however they can still be used as a failsafe*/ + applicationResponse + .onErrorMatching(isPasswordValidationException, ignored -> { + log.error("Invalid password provided during reset"); + setNewPasswordComponent.showError("Invalid Password provided", + "The provided password does not meet security standard"); + }) + .onErrorMatching(isPasswordValidationException.negate(), ignored -> { + log.error("Unexpected failure on password reset for user."); + setNewPasswordComponent.showError("An unexpected error occurred", + "Please contact support@qbic.zendesk.com for help."); + }); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/register/RegistrationOrcIdMain.java b/user-interface/src/main/java/life/qbic/datamanager/views/register/RegistrationOrcIdMain.java index fb491ec22..810115833 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/register/RegistrationOrcIdMain.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/register/RegistrationOrcIdMain.java @@ -99,17 +99,23 @@ private void handleRegistrationFailure(List exceptionList) { if (exceptionList.isEmpty()) { return; } - if (exceptionList.contains(UserExistsException.class)) { - userRegistrationOrcIdComponent.showError("Email address already in use", - "If you have difficulties with your password you can reset it."); - } else if (exceptionList.contains(UserNameNotAvailableException.class)) { - userRegistrationOrcIdComponent.showError("Username already in use", - "Please try another username"); - } else if (exceptionList.contains(EmptyUserNameException.class)) { - userRegistrationOrcIdComponent.showError("Username must not be empty", - "Please try another username"); - } else { - userRegistrationOrcIdComponent.showError("Registration failed", "Please try again."); + for (RuntimeException e : exceptionList) { + if (e instanceof UserExistsException) { + userRegistrationOrcIdComponent.showError("Email address already in use", + "If you have difficulties with your password you can reset it."); + break; + } else if (e instanceof UserNameNotAvailableException) { + userRegistrationOrcIdComponent.showError("Username already in use", + "Please try another username"); + break; + } else if (e instanceof EmptyUserNameException) { + userRegistrationOrcIdComponent.showError("Username must not be empty", + "Please try another username"); + break; + } else { + userRegistrationOrcIdComponent.showError("Registration failed", "Please try again."); + break; + } } String allErrorMessages = exceptionList.stream().map(Throwable::getMessage) .collect(Collectors.joining("\n")); diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationComponent.java index a2b1e552b..86f98f655 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationComponent.java @@ -5,7 +5,6 @@ import com.vaadin.flow.component.Key; import com.vaadin.flow.component.Text; import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.html.Span; @@ -23,7 +22,7 @@ import java.io.Serial; import java.io.Serializable; import java.util.Objects; -import life.qbic.datamanager.views.login.passwordreset.ResetPasswordLayout; +import life.qbic.datamanager.views.login.passwordreset.ResetPasswordMain; import life.qbic.datamanager.views.notifications.ErrorMessage; import life.qbic.identity.api.UserInformationService; @@ -64,16 +63,18 @@ public class UserRegistrationComponent extends Div { public UserRegistrationComponent(UserInformationService userInformationService) { this.userInformationService = Objects.requireNonNull(userInformationService); addClassName("user-registration-component"); - registerButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + registerButton.addClassName("primary"); username.setHelperText("Your unique user name, visible to other users"); + password.setHelperText("Please provide a password with at least 12 characters"); add(titleSpan, notificationLayout, fullName, email, username, password, registerButton); setFieldValidation(); addRegistrationButtonListener(); addRoutingLinks(); + addClassName("card-layout"); } private void addRoutingLinks() { - RouterLink resetLink = new RouterLink("RESET", ResetPasswordLayout.class); + RouterLink resetLink = new RouterLink("RESET", ResetPasswordMain.class); Span resetSpan = new Span(new Text("Forgot your password? "), resetLink); add(resetSpan); } @@ -111,7 +112,7 @@ private void setFieldValidation() { .asRequired("Please provide a password") .withValidator( name -> name.strip().length() >= 12, - "Please provide a password with at least 12 characters") + "Password does not contain at least 12 characters") .bind(UserRegistrationInformation::password, UserRegistrationInformation::setPassword); } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationMain.java b/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationMain.java index fe6df038c..b8a20e24b 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationMain.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationMain.java @@ -8,6 +8,7 @@ import java.io.Serial; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import life.qbic.datamanager.views.AppRoutes; import life.qbic.datamanager.views.general.Main; import life.qbic.datamanager.views.landing.LandingPageLayout; @@ -81,16 +82,27 @@ private void handleRegistrationFailure(List exceptionList) { if (exceptionList.isEmpty()) { return; } - if (exceptionList.contains(UserExistsException.class)) { - userRegistrationComponent.showError("Email address already in use", - "If you have difficulties with your password you can reset it."); - } else if (exceptionList.contains(UserNameNotAvailableException.class)) { - userRegistrationComponent.showError("Username already in use", "Please try another username"); - } else if (exceptionList.contains(EmptyUserNameException.class)) { - userRegistrationComponent.showError("Username must not be empty", - "Please try another username"); - } else { - userRegistrationComponent.showError("Registration failed", "Please try again."); + for (RuntimeException e : exceptionList) { + if (e instanceof UserExistsException) { + userRegistrationComponent.showError("Email address already in use", + "If you have difficulties with your password you can reset it."); + break; + } else if (e instanceof UserNameNotAvailableException) { + userRegistrationComponent.showError("Username already in use", + "Please try another username"); + break; + } else if (e instanceof EmptyUserNameException) { + userRegistrationComponent.showError("Username must not be empty", + "Please try another username"); + break; + } else { + userRegistrationComponent.showError("Registration failed", "Please try again."); + break; + } } + String allErrorMessages = exceptionList.stream().map(Throwable::getMessage) + .collect(Collectors.joining("\n")); + log.error(allErrorMessages); + exceptionList.forEach(e -> log.debug(e.getMessage(), e)); } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationOrcIdComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationOrcIdComponent.java index c60909235..f1a26842c 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationOrcIdComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/register/UserRegistrationOrcIdComponent.java @@ -4,7 +4,6 @@ import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.Key; import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.textfield.TextField; @@ -60,10 +59,11 @@ public class UserRegistrationOrcIdComponent extends Div { public UserRegistrationOrcIdComponent(UserInformationService userInformationService) { this.userInformationService = Objects.requireNonNull(userInformationService); addClassName("user-registration-component"); - registerButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + registerButton.addClassName("primary"); username.setHelperText("Your unique user name, visible to other users"); description.add( "Please complete missing information to create an account with us."); + addClassName("card-layout"); add(titleSpan, notificationLayout, description, fullName, email, username, registerButton); setFieldValidation(); addRegistrationButtonListener();