From 22763113349e541ce30e0917ee0c88d0d4ecace9 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 21 Dec 2021 07:22:44 +0100 Subject: [PATCH] Credentials Reset Flow (#1699) * Add credential reset modal * Add i18n labels * Refactor to align with marvelapp mockup TODO: tests * Add e2e tests * Implement code review change requests Add menuAppendTo to CredentialResetActionMultiSelect -> Select component Add optional menuAppendTo prop to TimeSelectorComponent Refactor CredentialsPage constructor --- cypress/integration/users_test.spec.ts | 63 +++ .../manage/users/CredentialsPage.ts | 86 +++ .../manage/users/UserDetailsPage.ts | 6 +- src/clients/help.ts | 3 + .../list-empty-state/ListEmptyState.tsx | 3 + src/components/time-selector/TimeSelector.tsx | 16 +- src/user/UserCredentials.tsx | 532 ++++++++++++------ src/user/messages.ts | 16 + src/user/user-section.css | 5 + 9 files changed, 562 insertions(+), 168 deletions(-) create mode 100644 cypress/support/pages/admin_console/manage/users/CredentialsPage.ts diff --git a/cypress/integration/users_test.spec.ts b/cypress/integration/users_test.spec.ts index 5fba5ac9ba..03697884be 100644 --- a/cypress/integration/users_test.spec.ts +++ b/cypress/integration/users_test.spec.ts @@ -10,6 +10,7 @@ import { keycloakBefore } from "../support/util/keycloak_before"; import GroupModal from "../support/pages/admin_console/manage/groups/GroupModal"; import UserGroupsPage from "../support/pages/admin_console/manage/users/UserGroupsPage"; import AdminClient from "../support/util/AdminClient"; +import CredentialsPage from "../support/pages/admin_console/manage/users/CredentialsPage"; let groupName = "group"; let groupsList: string[] = []; @@ -54,9 +55,11 @@ describe("Users test", () => { const modalUtils = new ModalUtils(); const listingPage = new ListingPage(); const userDetailsPage = new UserDetailsPage(); + const credentialsPage = new CredentialsPage(); const attributesTab = new AttributesTab(); let itemId = "user_crud"; + let itemIdWithCred = "user_crud_cred"; describe("User creation", () => { beforeEach(() => { @@ -106,6 +109,34 @@ describe("Users test", () => { sidebarPage.goToUsers(); }); + it("Create user with credentials test", () => { + itemIdWithCred += "_" + (Math.random() + 1).toString(36).substring(7); + + createUserPage.goToCreateUser(); + + createUserPage.createUser(itemIdWithCred); + + createUserPage.save(); + + masthead.checkNotificationMessage("The user has been created"); + + sidebarPage.goToUsers(); + + listingPage.goToItemDetails(itemIdWithCred); + + userDetailsPage.fillUserData().save(); + masthead.checkNotificationMessage("The user has been saved"); + + credentialsPage + .goToCredentialsTab() + .clickEmptyStatePasswordBtn() + .fillPasswordForm() + .clickConfirmationBtn() + .clickSetPasswordBtn(); + + sidebarPage.goToUsers(); + }); + it("User details test", () => { cy.wait("@brute-force"); listingPage.searchItem(itemId).itemExist(itemId); @@ -199,6 +230,27 @@ describe("Users test", () => { cy.findByTestId("empty-state").contains("No consents"); }); + it("Reset credential of User with empty state", () => { + cy.wait("@brute-force"); + listingPage.goToItemDetails(itemId); + credentialsPage + .goToCredentialsTab() + .clickEmptyStateResetBtn() + .fillResetCredentialForm(); + masthead.checkNotificationMessage("Failed to send email to user."); + }); + + it("Reset credential of User with existing credentials", () => { + cy.wait("@brute-force"); + listingPage.goToItemDetails(itemIdWithCred); + credentialsPage + .goToCredentialsTab() + .clickResetBtn() + .fillResetCredentialForm(); + + masthead.checkNotificationMessage("Failed to send email to user."); + }); + it("Delete user test", () => { // Delete listingPage.deleteItem(itemId); @@ -209,5 +261,16 @@ describe("Users test", () => { listingPage.itemExist(itemId, false); }); + + it("Delete user with credential test", () => { + // Delete + listingPage.deleteItem(itemIdWithCred); + + modalUtils.checkModalTitle("Delete user?").confirmModal(); + + masthead.checkNotificationMessage("The user has been deleted"); + + listingPage.itemExist(itemIdWithCred, false); + }); }); }); diff --git a/cypress/support/pages/admin_console/manage/users/CredentialsPage.ts b/cypress/support/pages/admin_console/manage/users/CredentialsPage.ts new file mode 100644 index 0000000000..64e3edcf79 --- /dev/null +++ b/cypress/support/pages/admin_console/manage/users/CredentialsPage.ts @@ -0,0 +1,86 @@ +export default class CredentialsPage { + private readonly credentialsTab = "credentials"; + private readonly emptyStatePasswordBtn = "no-credentials-empty-action"; + private readonly emptyStateResetBtn = "credential-reset-empty-action"; + private readonly resetBtn = "credentialResetBtn"; + private readonly setPasswordBtn = "setPasswordBtn"; + private readonly credentialResetModal = "credential-reset-modal"; + private readonly resetModalActionsToggleBtn = + "[data-testid=credential-reset-modal] #actions"; + private readonly passwordField = + ".kc-password > .pf-c-input-group > .pf-c-form-control"; + private readonly passwordConfirmationField = + ".kc-passwordConfirmation > .pf-c-input-group > .pf-c-form-control"; + private readonly resetActions = [ + "VERIFY_EMAIL-option", + "UPDATE_PROFILE-option", + "CONFIGURE_TOTP-option", + "UPDATE_PASSWORD-option", + "terms_and_conditions-option", + ]; + private readonly confirmationButton = "okBtn"; + + goToCredentialsTab() { + cy.findByTestId(this.credentialsTab).click(); + + return this; + } + clickEmptyStatePasswordBtn() { + cy.findByTestId(this.emptyStatePasswordBtn).click(); + + return this; + } + + clickEmptyStateResetBtn() { + cy.findByTestId(this.emptyStateResetBtn).click(); + + return this; + } + + clickResetBtn() { + cy.findByTestId(this.resetBtn).click(); + + return this; + } + + clickResetModalActionsToggleBtn() { + cy.get(this.resetModalActionsToggleBtn).click(); + + return this; + } + + clickResetModalAction(index: number) { + cy.findByTestId(this.resetActions[index]).click(); + + return this; + } + + clickConfirmationBtn() { + cy.findByTestId(this.confirmationButton).dblclick(); + + return this; + } + + fillPasswordForm() { + cy.get(this.passwordField).type("test"); + cy.get(this.passwordConfirmationField).type("test"); + + return this; + } + + fillResetCredentialForm() { + cy.findByTestId(this.credentialResetModal); + this.clickResetModalActionsToggleBtn() + .clickResetModalAction(2) + .clickResetModalAction(3) + .clickConfirmationBtn(); + + return this; + } + + clickSetPasswordBtn() { + cy.findByTestId(this.setPasswordBtn).click(); + + return this; + } +} diff --git a/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts b/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts index be7bb5aad7..8bccba7d6c 100644 --- a/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts +++ b/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts @@ -4,7 +4,7 @@ export default class UserDetailsPage { saveBtn: string; cancelBtn: string; emailInput: string; - emailValue: string; + emailValue: () => string; firstNameInput: string; firstNameValue: string; lastNameInput: string; @@ -17,7 +17,7 @@ export default class UserDetailsPage { this.saveBtn = "save-user"; this.cancelBtn = "cancel-create-user"; this.emailInput = "email-input"; - this.emailValue = + this.emailValue = () => "example" + "_" + (Math.random() + 1).toString(36).substring(7) + @@ -32,7 +32,7 @@ export default class UserDetailsPage { } fillUserData() { - cy.findByTestId(this.emailInput).type(this.emailValue); + cy.findByTestId(this.emailInput).type(this.emailValue()); cy.findByTestId(this.firstNameInput).type(this.firstNameValue); cy.findByTestId(this.lastNameInput).type(this.lastNameValue); cy.findByTestId(this.enabledSwitch).check({ force: true }); diff --git a/src/clients/help.ts b/src/clients/help.ts index 2bb356bb9e..8d6885832d 100644 --- a/src/clients/help.ts +++ b/src/clients/help.ts @@ -173,5 +173,8 @@ export default { ownerManagedAccess: "If enabled, the access to this resource can be managed by the resource owner.", resourceAttribute: "The attributes associated wth the resource.", + resetActions: + "Set of actions to execute when sending the user a Reset Actions Email. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure OTP' requires setup of a mobile password generator.", + lifespan: "Maximum time before the action permit expires.", }, }; diff --git a/src/components/list-empty-state/ListEmptyState.tsx b/src/components/list-empty-state/ListEmptyState.tsx index ef7ea17b74..6237f311c1 100644 --- a/src/components/list-empty-state/ListEmptyState.tsx +++ b/src/components/list-empty-state/ListEmptyState.tsx @@ -66,6 +66,9 @@ export const ListEmptyState = ({ {secondaryActions.map((action) => ( , + , + ]} + > +
+ + + + + )} {showData && Object.keys(selectedCredential).length !== 0 && ( { )} {userCredentials.length !== 0 ? ( - - - - - - - {t("type")} - {t("userLabel")} - {t("data")} - - - - - - {userCredentials.map((credential) => ( - - <> - + {user.email && ( + + )} + + + + + - - {credential.type?.charAt(0).toUpperCase()! + - credential.type?.slice(1)} - - - - -
- {isUserLabelEdit?.status && - isUserLabelEdit.rowKey === credential.id ? ( - <> - -
+ + {t("type")} + {t("userLabel")} + {t("data")} + + + + + + {userCredentials.map((credential) => ( + + <> + + + {credential.type?.charAt(0).toUpperCase()! + + credential.type?.slice(1)} + + + + +
+ {isUserLabelEdit?.status && + isUserLabelEdit.rowKey === credential.id ? ( + <> + +
+
+ + ) : ( + <> + {credential.userLabel ?? ""}
- - ) : ( - <> - {credential.userLabel ?? ""} -
- - - - - - - {credential.type === "password" ? ( + + )} +
+
+
+ - ) : ( - - )} - - - setKebabOpen({ - status, - rowKey: credential.id!, - }) - } - /> - } - isOpen={ - kebabOpen.status && kebabOpen.rowKey === credential.id - } - onSelect={() => { - setSelectedCredential(credential); - }} - dropdownItems={[ - { - toggleDeleteDialog(); - setKebabOpen({ - status: false, - rowKey: credential.id!, - }); - }} + {credential.type === "password" ? ( + + + + ) : ( + + )} + + + setKebabOpen({ + status, + rowKey: credential.id!, + }) + } + /> + } + isOpen={ + kebabOpen.status && kebabOpen.rowKey === credential.id + } + onSelect={() => { + setSelectedCredential(credential); + }} + dropdownItems={[ + { + toggleDeleteDialog(); + setKebabOpen({ + status: false, + rowKey: credential.id!, + }); + }} + > + {t("deleteBtn")} + , + ]} + /> + + + + ))} + +
+ ) : ( { instructions={t("noCredentialsText")} primaryActionText={t("setPassword")} onPrimaryAction={toggleModal} + secondaryActions={ + user.email + ? [ + { + text: t("credentialResetBtn"), + onClick: toggleCredentialsResetModal, + type: ButtonVariant.link, + }, + ] + : undefined + } /> )} diff --git a/src/user/messages.ts b/src/user/messages.ts index 8da6dc6a54..c63d893ded 100644 --- a/src/user/messages.ts +++ b/src/user/messages.ts @@ -169,5 +169,21 @@ export default { updateCredentialUserLabelSuccess: "The user label has been changed successfully.", updateCredentialUserLabelError: "Error changing user label: {{error}}", + credentialReset: "Credentials Reset", + credentialResetBtn: "Credential Reset", + resetActions: "Reset Actions", + lifespan: "Expires In", + VERIFY_EMAIL: "Verify Email (VERIFY_EMAIL)", + UPDATE_PASSWORD: "Update password (UPDATE_PASSWORD)", + UPDATE_PROFILE: "Update Profile (UPDATE_PROFILE)", + CONFIGURE_TOTP: "Configure OTP (CONFIGURE_TOTP)", + terms_and_conditions: "Terms and Conditions (terms_and_conditions)", + hours: "Hours", + minutes: "Minutes", + seconds: "Seconds", + credentialResetConfirm: "Send Email", + credentialResetConfirmText: "Are you sure you want to send email to user", + credentialResetEmailSuccess: "Email sent to user.", + credentialResetEmailError: "Failed to send email to user.", }, }; diff --git a/src/user/user-section.css b/src/user/user-section.css index 9bef038ce0..01bf3246a1 100644 --- a/src/user/user-section.css +++ b/src/user/user-section.css @@ -182,3 +182,8 @@ article.pf-c-card.pf-m-flat.kc-available-idps > div > div > h1 { .setPasswordBtn-table { margin: 25px 0 25px 25px; } + +.resetCredentialBtn-header { + margin: 10px 25px 10px 0; + float: right; +}