Merge pull request #696 from jenny-s51/testEmailConnection

Test email connection
This commit is contained in:
mfrances17 2021-06-22 16:17:31 -04:00 committed by GitHub
commit 125fd1fe3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 273 additions and 11 deletions

View file

@ -56,6 +56,8 @@ describe("Realm roles test", function () {
// Delete // Delete
listingPage.deleteItem(itemId); listingPage.deleteItem(itemId);
cy.wait(500);
modalUtils.checkModalTitle("Delete role?").confirmModal(); modalUtils.checkModalTitle("Delete role?").confirmModal();
masthead.checkNotificationMessage("The role has been deleted"); masthead.checkNotificationMessage("The role has been deleted");

View file

@ -69,6 +69,17 @@ describe("Realm settings", () => {
realmSettingsPage.toggleCheck(realmSettingsPage.enableStartTlsCheck); realmSettingsPage.toggleCheck(realmSettingsPage.enableStartTlsCheck);
realmSettingsPage.save(realmSettingsPage.emailSaveBtn); 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", () => { it("Go to themes tab", () => {

View file

@ -11,6 +11,7 @@ export default class RealmSettingsPage {
adminThemeList = "#kc-admin-console-theme + ul"; adminThemeList = "#kc-admin-console-theme + ul";
selectEmailTheme = "#kc-email-theme"; selectEmailTheme = "#kc-email-theme";
emailThemeList = "#kc-email-theme + ul"; emailThemeList = "#kc-email-theme + ul";
hostInput = "#kc-host";
selectDefaultLocale = "select-default-locale"; selectDefaultLocale = "select-default-locale";
defaultLocaleList = "select-default-locale + ul"; defaultLocaleList = "select-default-locale + ul";
emailSaveBtn = "email-tab-save"; emailSaveBtn = "email-tab-save";
@ -35,6 +36,9 @@ export default class RealmSettingsPage {
filterSelectMenu = ".kc-filter-type-select"; filterSelectMenu = ".kc-filter-type-select";
passiveKeysOption = "passive-keys-option"; passiveKeysOption = "passive-keys-option";
disabledKeysOption = "disabled-keys-option"; disabledKeysOption = "disabled-keys-option";
testConnectionButton = "test-connection-button";
modalTestConnectionButton = "modal-test-connection-button";
emailAddressInput = "email-address-input";
selectLoginThemeType(themeType: string) { selectLoginThemeType(themeType: string) {
const themesUrl = "/auth/admin/realms/master/themes"; const themesUrl = "/auth/admin/realms/master/themes";
@ -64,6 +68,16 @@ export default class RealmSettingsPage {
return this; 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) { setDefaultLocale(locale: string) {
cy.get(this.selectDefaultLocale).click(); cy.get(this.selectDefaultLocale).click();
cy.get(this.defaultLocaleList).contains(locale).click(); cy.get(this.defaultLocaleList).contains(locale).click();

View file

@ -36,6 +36,7 @@ export type ConfirmDialogProps = {
titleKey: string; titleKey: string;
messageKey?: string; messageKey?: string;
noCancelButton?: boolean; noCancelButton?: boolean;
confirmButtonDisabled?: boolean;
cancelButtonLabel?: string; cancelButtonLabel?: string;
continueButtonLabel?: string; continueButtonLabel?: string;
continueButtonVariant?: ButtonVariant; continueButtonVariant?: ButtonVariant;
@ -58,6 +59,7 @@ export const ConfirmDialogModal = ({
open = true, open = true,
variant = ModalVariant.small, variant = ModalVariant.small,
toggleDialog, toggleDialog,
confirmButtonDisabled,
}: ConfirmDialogModalProps) => { }: ConfirmDialogModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -71,6 +73,7 @@ export const ConfirmDialogModal = ({
id="modal-confirm" id="modal-confirm"
data-testid="modalConfirm" data-testid="modalConfirm"
key="confirm" key="confirm"
isDisabled={confirmButtonDisabled}
variant={continueButtonVariant || ButtonVariant.primary} variant={continueButtonVariant || ButtonVariant.primary}
onClick={() => { onClick={() => {
onConfirm(); onConfirm();

View file

@ -23,6 +23,12 @@ export class WhoAmI {
return this.me.displayName; return this.me.displayName;
} }
public getUserId(): string {
if (this.me === undefined) return "";
return this.me.userId;
}
/** /**
* Return the realm I am signed in to. * Return the realm I am signed in to.
*/ */

View file

@ -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<UserRepresentation>;
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 (
<Modal
variant={ModalVariant.small}
title={t("realm-settings:provideEmailTitle")}
isOpen={true}
onClose={handleModalToggle}
actions={[
<Button
data-testid="modal-test-connection-button"
key="confirm"
variant="primary"
type="submit"
form="email-form"
isDisabled={!watchEmailInput}
>
{t("realm-settings:testConnection")}
</Button>,
<Button
id="modal-cancel"
key="cancel"
variant={ButtonVariant.link}
onClick={() => {
handleModalToggle();
}}
>
{t("common:cancel")}
</Button>,
]}
>
<TextContent className="kc-provide-email-text">
{t("realm-settings:provideEmail")}
</TextContent>
<Form id="email-form" isHorizontal onSubmit={handleSubmit(save)}>
<FormGroup
className="kc-email-form-group"
name="add-email-address"
fieldId="email-id"
helperTextInvalid={t("users:emailInvalid")}
validated={
errors.email ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<TextInput
data-testid="email-address-input"
ref={register({ required: true, pattern: emailRegexPattern })}
autoFocus
type="text"
id="add-email"
name="email"
validated={
errors.email ? ValidatedOptions.error : ValidatedOptions.default
}
/>
</FormGroup>
</Form>
</Modal>
);
};

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm, useWatch } from "react-hook-form";
import { import {
ActionGroup, ActionGroup,
AlertVariant, AlertVariant,
@ -20,36 +20,64 @@ import { emailRegexPattern } from "../util";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { getBaseUrl } from "../util";
import "./RealmSettingsSection.css"; import "./RealmSettingsSection.css";
import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
import { WhoAmIContext } from "../context/whoami/WhoAmI";
import { AddUserEmailModal } from "./AddUserEmailModal";
type RealmSettingsEmailTabProps = { type RealmSettingsEmailTabProps = {
realm: RealmRepresentation; realm: RealmRepresentation;
user: UserRepresentation;
}; };
export const RealmSettingsEmailTab = ({ export const RealmSettingsEmailTab = ({
realm: initialRealm, realm: initialRealm,
user,
}: RealmSettingsEmailTabProps) => { }: RealmSettingsEmailTabProps) => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { realm: realmName } = useRealm(); const { realm: realmName } = useRealm();
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const { whoAmI } = useContext(WhoAmIContext);
const [isAuthenticationEnabled, setAuthenticationEnabled] = useState("true");
const [realm, setRealm] = useState(initialRealm); const [realm, setRealm] = useState(initialRealm);
const [userEmailModalOpen, setUserEmailModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<UserRepresentation>();
const { const {
register, register,
control, control,
handleSubmit, handleSubmit,
errors, errors,
watch,
setValue, setValue,
reset: resetForm, reset: resetForm,
getValues,
} = useForm<RealmRepresentation>(); } = useForm<RealmRepresentation>();
const userForm = useForm<UserRepresentation>({ mode: "onChange" });
const watchFromValue = watch("smtpServer.from", "");
const watchHostValue = watch("smtpServer.host", "");
const authenticationEnabled = useWatch({
control,
name: "smtpServer.authentication",
defaultValue: realm?.smtpServer!.authentication,
});
useEffect(() => { useEffect(() => {
reset(); reset();
}, [realm]); }, [realm]);
useEffect(() => {
setCurrentUser(user);
}, []);
const handleModalToggle = () => {
setUserEmailModalOpen(!userEmailModalOpen);
};
const save = async (form: RealmRepresentation) => { const save = async (form: RealmRepresentation) => {
try { try {
const savedRealm = { ...realm, ...form }; 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 = () => { const reset = () => {
if (realm) { if (realm) {
resetForm(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 ( return (
<> <>
{userEmailModalOpen && (
<AddUserEmailModal
handleModalToggle={handleModalToggle}
testConnection={testConnection}
save={(email) => {
saveAndTestEmail(email!);
handleModalToggle();
}}
form={userForm}
user={currentUser!}
/>
)}
<PageSection variant="light"> <PageSection variant="light">
<FormPanel title={t("template")} className="kc-email-template"> <FormPanel title={t("template")} className="kc-email-template">
<FormAccess <FormAccess
@ -253,7 +334,7 @@ export const RealmSettingsEmailTab = ({
<Controller <Controller
name="smtpServer.authentication" name="smtpServer.authentication"
control={control} control={control}
defaultValue="true" defaultValue={authenticationEnabled}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Switch <Switch
id="kc-authentication" id="kc-authentication"
@ -263,13 +344,12 @@ export const RealmSettingsEmailTab = ({
isChecked={value === "true"} isChecked={value === "true"}
onChange={(value) => { onChange={(value) => {
onChange("" + value); onChange("" + value);
setAuthenticationEnabled(String(value));
}} }}
/> />
)} )}
/> />
</FormGroup> </FormGroup>
{isAuthenticationEnabled === "true" && ( {authenticationEnabled === "true" && (
<> <>
<FormGroup <FormGroup
label={t("username")} label={t("username")}
@ -325,6 +405,16 @@ export const RealmSettingsEmailTab = ({
> >
{t("common:save")} {t("common:save")}
</Button> </Button>
<Button
variant="secondary"
onClick={() => saveAndTestEmail()}
data-testid="test-connection-button"
isDisabled={
!(emailRegexPattern.test(watchFromValue) && watchHostValue)
}
>
{t("realm-settings:testConnection")}
</Button>
<Button variant="link" onClick={reset}> <Button variant="link" onClick={reset}>
{t("common:revert")} {t("common:revert")}
</Button> </Button>

View file

@ -239,7 +239,7 @@ export const LocalizationTab = ({
</FormAccess> </FormAccess>
</FormPanel> </FormPanel>
<FormPanel className="kc-login-screen" title="Edit message bundles"> <FormPanel className="kc-message-bundles" title="Edit message bundles">
<TextContent className="messageBundleDescription"> <TextContent className="messageBundleDescription">
{t("messageBundleDescription")} {t("messageBundleDescription")}
</TextContent> </TextContent>

View file

@ -1,7 +1,8 @@
.pf-c-card.pf-m-flat.kc-login-screen, .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-settings,
.pf-c-card.pf-m-flat.kc-email-template, .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; border: none;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
@ -86,9 +87,17 @@ button.pf-c-button.pf-m-link.add-provider {
padding-bottom: var(--pf-global--spacer--lg); 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-style: solid;
border-width: 1px; border-width: 1px;
border-color: var(--pf-global--BorderColor--100); border-color: var(--pf-global--BorderColor--100);
max-width: 1024px; 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);
}

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useContext } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, FormProvider, useForm } from "react-hook-form"; 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 { KeysProviderTab } from "./KeysProvidersTab";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { LocalizationTab } from "./LocalizationTab"; import { LocalizationTab } from "./LocalizationTab";
import { WhoAmIContext } from "../context/whoami/WhoAmI";
import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
type RealmSettingsHeaderProps = { type RealmSettingsHeaderProps = {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
@ -136,6 +138,8 @@ export const RealmSettingsSection = () => {
const [realmComponents, setRealmComponents] = useState< const [realmComponents, setRealmComponents] = useState<
ComponentRepresentation[] ComponentRepresentation[]
>(); >();
const [currentUser, setCurrentUser] = useState<UserRepresentation>();
const { whoAmI } = useContext(WhoAmIContext);
const kpComponentTypes = useServerInfo().componentTypes![ const kpComponentTypes = useServerInfo().componentTypes![
"org.keycloak.keys.KeyProvider" "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( useFetch(
async () => { async () => {
const realm = await adminClient.realms.findOne({ realm: realmName }); const realm = await adminClient.realms.findOne({ realm: realmName });
@ -233,7 +257,9 @@ export const RealmSettingsSection = () => {
title={<TabTitleText>{t("email")}</TabTitleText>} title={<TabTitleText>{t("email")}</TabTitleText>}
data-testid="rs-email-tab" data-testid="rs-email-tab"
> >
{realm && <RealmSettingsEmailTab realm={realm} />} {realm && (
<RealmSettingsEmailTab user={currentUser!} realm={realm} />
)}
</Tab> </Tab>
<Tab <Tab
eventKey="themes" eventKey="themes"

View file

@ -92,8 +92,13 @@
"loginWithEmailHelpText": "Allow users to log in with their email address.", "loginWithEmailHelpText": "Allow users to log in with their email address.",
"duplicateEmails": "Duplicate emails", "duplicateEmails": "Duplicate emails",
"duplicateEmailsHelpText": "Allow multiple users to have the same email address. Changing this setting will also clear the user's cache. It is recommended to manually update email constraints of existing users in the database after switching off support for duplicate email addresses.", "duplicateEmailsHelpText": "Allow multiple users to have the same email address. Changing this setting will also clear the user's cache. It is recommended to manually update email constraints of existing users in the database after switching off support for duplicate email addresses.",
"provideEmailTitle": "Provide your email address",
"provideEmail": "To test connection, you should provide your email address first.",
"verifyEmail": "Verify email", "verifyEmail": "Verify email",
"verifyEmailHelpText": "Require user to verify their email address after initial login or after address changes are submitted.", "verifyEmailHelpText": "Require user to verify their email address after initial login or after address changes are submitted.",
"testConnection": "Test connection",
"testConnectionSuccess": "Success! SMTP connection successful. E-mail was sent!",
"testConnectionError": "Error! Failed to send email.",
"realmId": "Realm ID", "realmId": "Realm ID",
"displayName": "Display name", "displayName": "Display name",
"htmlDisplayName": "HTML Display name", "htmlDisplayName": "HTML Display name",