diff --git a/cypress/integration/realm_roles_test.spec.ts b/cypress/integration/realm_roles_test.spec.ts index d93f14304b..115915aad1 100644 --- a/cypress/integration/realm_roles_test.spec.ts +++ b/cypress/integration/realm_roles_test.spec.ts @@ -56,6 +56,8 @@ describe("Realm roles test", function () { // Delete listingPage.deleteItem(itemId); + cy.wait(500); + modalUtils.checkModalTitle("Delete role?").confirmModal(); masthead.checkNotificationMessage("The role has been deleted"); diff --git a/cypress/integration/realm_settings_test.spec.ts b/cypress/integration/realm_settings_test.spec.ts index a4eecb5656..9c34110e99 100644 --- a/cypress/integration/realm_settings_test.spec.ts +++ b/cypress/integration/realm_settings_test.spec.ts @@ -69,6 +69,17 @@ describe("Realm settings", () => { realmSettingsPage.toggleCheck(realmSettingsPage.enableStartTlsCheck); realmSettingsPage.save(realmSettingsPage.emailSaveBtn); + + realmSettingsPage.fillHostField("localhost"); + cy.getId(realmSettingsPage.testConnectionButton).click(); + + realmSettingsPage.fillEmailField( + "example" + (Math.random() + 1).toString(36).substring(7) + "@example.com" + ); + + cy.getId(realmSettingsPage.modalTestConnectionButton).click(); + + masthead.checkNotificationMessage("Error! Failed to send email."); }); it("Go to themes tab", () => { diff --git a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts index f30ac56054..db84f5aa99 100644 --- a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts +++ b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts @@ -11,6 +11,7 @@ export default class RealmSettingsPage { adminThemeList = "#kc-admin-console-theme + ul"; selectEmailTheme = "#kc-email-theme"; emailThemeList = "#kc-email-theme + ul"; + hostInput = "#kc-host"; selectDefaultLocale = "select-default-locale"; defaultLocaleList = "select-default-locale + ul"; emailSaveBtn = "email-tab-save"; @@ -35,6 +36,9 @@ export default class RealmSettingsPage { filterSelectMenu = ".kc-filter-type-select"; passiveKeysOption = "passive-keys-option"; disabledKeysOption = "disabled-keys-option"; + testConnectionButton = "test-connection-button"; + modalTestConnectionButton = "modal-test-connection-button"; + emailAddressInput = "email-address-input"; selectLoginThemeType(themeType: string) { const themesUrl = "/auth/admin/realms/master/themes"; @@ -64,6 +68,16 @@ export default class RealmSettingsPage { return this; } + fillEmailField(email: string) { + cy.getId(this.emailAddressInput).type(email); + return this; + } + + fillHostField(host: string) { + cy.get(this.hostInput).type(host); + return this; + } + setDefaultLocale(locale: string) { cy.get(this.selectDefaultLocale).click(); cy.get(this.defaultLocaleList).contains(locale).click(); diff --git a/src/components/confirm-dialog/ConfirmDialog.tsx b/src/components/confirm-dialog/ConfirmDialog.tsx index ea0ea80318..54ef203737 100644 --- a/src/components/confirm-dialog/ConfirmDialog.tsx +++ b/src/components/confirm-dialog/ConfirmDialog.tsx @@ -36,6 +36,7 @@ export type ConfirmDialogProps = { titleKey: string; messageKey?: string; noCancelButton?: boolean; + confirmButtonDisabled?: boolean; cancelButtonLabel?: string; continueButtonLabel?: string; continueButtonVariant?: ButtonVariant; @@ -58,6 +59,7 @@ export const ConfirmDialogModal = ({ open = true, variant = ModalVariant.small, toggleDialog, + confirmButtonDisabled, }: ConfirmDialogModalProps) => { const { t } = useTranslation(); return ( @@ -71,6 +73,7 @@ export const ConfirmDialogModal = ({ id="modal-confirm" data-testid="modalConfirm" key="confirm" + isDisabled={confirmButtonDisabled} variant={continueButtonVariant || ButtonVariant.primary} onClick={() => { onConfirm(); diff --git a/src/context/whoami/WhoAmI.tsx b/src/context/whoami/WhoAmI.tsx index 3376635005..7d7aa8d423 100644 --- a/src/context/whoami/WhoAmI.tsx +++ b/src/context/whoami/WhoAmI.tsx @@ -23,6 +23,12 @@ export class WhoAmI { return this.me.displayName; } + public getUserId(): string { + if (this.me === undefined) return ""; + + return this.me.userId; + } + /** * Return the realm I am signed in to. */ diff --git a/src/realm-settings/AddUserEmailModal.tsx b/src/realm-settings/AddUserEmailModal.tsx new file mode 100644 index 0000000000..0b87a7b190 --- /dev/null +++ b/src/realm-settings/AddUserEmailModal.tsx @@ -0,0 +1,96 @@ +import React from "react"; +import { + Button, + ButtonVariant, + Form, + FormGroup, + Modal, + ModalVariant, + TextContent, + TextInput, + ValidatedOptions, +} from "@patternfly/react-core"; +import { useTranslation } from "react-i18next"; +import { useForm, UseFormMethods } from "react-hook-form"; + +import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation"; +import { emailRegexPattern } from "../util"; + +type AddUserEmailModalProps = { + id?: string; + form: UseFormMethods; + rename?: string; + handleModalToggle: () => void; + testConnection: () => void; + user: UserRepresentation; + save: (user?: UserRepresentation) => void; +}; + +export const AddUserEmailModal = ({ + handleModalToggle, + save, +}: AddUserEmailModalProps) => { + const { t } = useTranslation("groups"); + const { register, errors, handleSubmit, watch } = useForm(); + + const watchEmailInput = watch("email", ""); + + return ( + + {t("realm-settings:testConnection")} + , + , + ]} + > + + {t("realm-settings:provideEmail")} + +
+ + + +
+
+ ); +}; diff --git a/src/realm-settings/EmailTab.tsx b/src/realm-settings/EmailTab.tsx index 995f3435da..0bc5f2485c 100644 --- a/src/realm-settings/EmailTab.tsx +++ b/src/realm-settings/EmailTab.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Controller, useForm } from "react-hook-form"; +import { Controller, useForm, useWatch } from "react-hook-form"; import { ActionGroup, AlertVariant, @@ -20,36 +20,64 @@ import { emailRegexPattern } from "../util"; import { useAdminClient } from "../context/auth/AdminClient"; import { useAlerts } from "../components/alert/Alerts"; import { useRealm } from "../context/realm-context/RealmContext"; +import { getBaseUrl } from "../util"; import "./RealmSettingsSection.css"; +import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation"; +import { WhoAmIContext } from "../context/whoami/WhoAmI"; +import { AddUserEmailModal } from "./AddUserEmailModal"; type RealmSettingsEmailTabProps = { realm: RealmRepresentation; + user: UserRepresentation; }; export const RealmSettingsEmailTab = ({ realm: initialRealm, + user, }: RealmSettingsEmailTabProps) => { const { t } = useTranslation("realm-settings"); const adminClient = useAdminClient(); const { realm: realmName } = useRealm(); const { addAlert } = useAlerts(); + const { whoAmI } = useContext(WhoAmIContext); - const [isAuthenticationEnabled, setAuthenticationEnabled] = useState("true"); const [realm, setRealm] = useState(initialRealm); + const [userEmailModalOpen, setUserEmailModalOpen] = useState(false); + const [currentUser, setCurrentUser] = useState(); const { register, control, handleSubmit, errors, + watch, setValue, reset: resetForm, + getValues, } = useForm(); + const userForm = useForm({ mode: "onChange" }); + const watchFromValue = watch("smtpServer.from", ""); + const watchHostValue = watch("smtpServer.host", ""); + + const authenticationEnabled = useWatch({ + control, + name: "smtpServer.authentication", + defaultValue: realm?.smtpServer!.authentication, + }); + useEffect(() => { reset(); }, [realm]); + useEffect(() => { + setCurrentUser(user); + }, []); + + const handleModalToggle = () => { + setUserEmailModalOpen(!userEmailModalOpen); + }; + const save = async (form: RealmRepresentation) => { try { const savedRealm = { ...realm, ...form }; @@ -64,6 +92,27 @@ export const RealmSettingsEmailTab = ({ } }; + const saveAndTestEmail = async (email?: UserRepresentation) => { + if (email) { + await adminClient.users.update({ id: whoAmI.getUserId() }, email); + const updated = await adminClient.users.findOne({ + id: whoAmI.getUserId(), + }); + setCurrentUser(updated); + + await save(getValues()); + testConnection(); + } else { + const user = await adminClient.users.findOne({ id: whoAmI.getUserId() }); + if (!user.email) { + handleModalToggle(); + } else { + await save(getValues()); + testConnection(); + } + } + }; + const reset = () => { if (realm) { resetForm(realm); @@ -71,8 +120,40 @@ export const RealmSettingsEmailTab = ({ } }; + const testConnection = async () => { + const response = await fetch( + `${getBaseUrl(adminClient)}admin/realms/${ + realm.realm + }/testSMTPConnection`, + { + method: "POST", + headers: { + Authorization: `bearer ${await adminClient.getAccessToken()}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(getValues()["smtpServer"] as BodyInit), + } + ); + response.ok + ? addAlert(t("testConnectionSuccess"), AlertVariant.success) + : addAlert(t("testConnectionError"), AlertVariant.danger); + }; + return ( <> + {userEmailModalOpen && ( + { + saveAndTestEmail(email!); + handleModalToggle(); + }} + form={userForm} + user={currentUser!} + /> + )} ( { onChange("" + value); - setAuthenticationEnabled(String(value)); }} /> )} /> - {isAuthenticationEnabled === "true" && ( + {authenticationEnabled === "true" && ( <> {t("common:save")} + diff --git a/src/realm-settings/LocalizationTab.tsx b/src/realm-settings/LocalizationTab.tsx index 15dd89a548..9ef39eb270 100644 --- a/src/realm-settings/LocalizationTab.tsx +++ b/src/realm-settings/LocalizationTab.tsx @@ -239,7 +239,7 @@ export const LocalizationTab = ({ - + {t("messageBundleDescription")} diff --git a/src/realm-settings/RealmSettingsSection.css b/src/realm-settings/RealmSettingsSection.css index 1119867da4..0a326c772a 100644 --- a/src/realm-settings/RealmSettingsSection.css +++ b/src/realm-settings/RealmSettingsSection.css @@ -1,7 +1,8 @@ .pf-c-card.pf-m-flat.kc-login-screen, .pf-c-card.pf-m-flat.kc-email-settings, .pf-c-card.pf-m-flat.kc-email-template, -.pf-c-card.pf-m-flat.kc-email-connection { +.pf-c-card.pf-m-flat.kc-email-connection, +.pf-c-card.pf-m-flat.kc-message-bundles { border: none; margin-top: 0px; margin-bottom: 0px; @@ -86,9 +87,17 @@ button.pf-c-button.pf-m-link.add-provider { padding-bottom: var(--pf-global--spacer--lg); } -div.tableBorder { +.kc-message-bundles > .pf-c-card__body.kc-form-panel__body > div.tableBorder { border-style: solid; border-width: 1px; border-color: var(--pf-global--BorderColor--100); max-width: 1024px; } + +.pf-c-form__group.kc-email-form-group { + display: inline-block !important; +} + +.pf-c-content.kc-provide-email-text { + padding-bottom: var(--pf-global--spacer--md); +} diff --git a/src/realm-settings/RealmSettingsSection.tsx b/src/realm-settings/RealmSettingsSection.tsx index 85d58b7dc5..fceb9e2294 100644 --- a/src/realm-settings/RealmSettingsSection.tsx +++ b/src/realm-settings/RealmSettingsSection.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useContext } from "react"; import { useHistory } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Controller, FormProvider, useForm } from "react-hook-form"; @@ -32,6 +32,8 @@ import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepre import { KeysProviderTab } from "./KeysProvidersTab"; import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { LocalizationTab } from "./LocalizationTab"; +import { WhoAmIContext } from "../context/whoami/WhoAmI"; +import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation"; type RealmSettingsHeaderProps = { onChange: (value: boolean) => void; @@ -136,6 +138,8 @@ export const RealmSettingsSection = () => { const [realmComponents, setRealmComponents] = useState< ComponentRepresentation[] >(); + const [currentUser, setCurrentUser] = useState(); + const { whoAmI } = useContext(WhoAmIContext); const kpComponentTypes = useServerInfo().componentTypes![ "org.keycloak.keys.KeyProvider" @@ -150,6 +154,26 @@ export const RealmSettingsSection = () => { [] ); + useFetch( + () => adminClient.users.findOne({ id: whoAmI.getUserId()! }), + + (user) => { + setCurrentUser(user); + }, + [] + ); + + useEffect(() => { + const update = async () => { + const realmComponents = await adminClient.components.find({ + type: "org.keycloak.keys.KeyProvider", + realm: realmName, + }); + setRealmComponents(realmComponents); + }; + setTimeout(update, 100); + }, [key]); + useFetch( async () => { const realm = await adminClient.realms.findOne({ realm: realmName }); @@ -233,7 +257,9 @@ export const RealmSettingsSection = () => { title={{t("email")}} data-testid="rs-email-tab" > - {realm && } + {realm && ( + + )}