diff --git a/cypress/integration/realm_settings_test.spec.ts b/cypress/integration/realm_settings_test.spec.ts index 4a610acb4c..40623786b8 100644 --- a/cypress/integration/realm_settings_test.spec.ts +++ b/cypress/integration/realm_settings_test.spec.ts @@ -8,12 +8,6 @@ describe("Realm settings test", () => { const sidebarPage = new SidebarPage(); const realmSettingsPage = new RealmSettingsPage(); - const managedAccessSwitch = "user-managed-access-switch"; - const userRegSwitch = "user-reg-switch"; - const forgotPwdSwitch = "forgot-pw-switch"; - const rememberMeSwitch = "remember-me-switch"; - const verifyEmailSwitch = "verify-email-switch"; - describe("Realm settings", function () { beforeEach(function () { keycloakBefore(); @@ -22,19 +16,33 @@ describe("Realm settings test", () => { it("Go to general tab", function () { sidebarPage.goToRealmSettings(); - realmSettingsPage.toggleSwitch(managedAccessSwitch); - realmSettingsPage.saveGeneral(); - realmSettingsPage.toggleSwitch(managedAccessSwitch); - realmSettingsPage.saveGeneral(); + realmSettingsPage.toggleSwitch(realmSettingsPage.managedAccessSwitch); + realmSettingsPage.save(realmSettingsPage.generalSaveBtn); + realmSettingsPage.toggleSwitch(realmSettingsPage.managedAccessSwitch); + realmSettingsPage.save(realmSettingsPage.generalSaveBtn); }); it("Go to login tab", function () { sidebarPage.goToRealmSettings(); cy.getId("rs-login-tab").click(); - realmSettingsPage.toggleSwitch(userRegSwitch); - realmSettingsPage.toggleSwitch(forgotPwdSwitch); - realmSettingsPage.toggleSwitch(rememberMeSwitch); - realmSettingsPage.toggleSwitch(verifyEmailSwitch); + realmSettingsPage.toggleSwitch(realmSettingsPage.userRegSwitch); + realmSettingsPage.toggleSwitch(realmSettingsPage.forgotPwdSwitch); + realmSettingsPage.toggleSwitch(realmSettingsPage.rememberMeSwitch); + realmSettingsPage.toggleSwitch(realmSettingsPage.verifyEmailSwitch); + }); + + it("Go to email tab", function () { + sidebarPage.goToRealmSettings(); + cy.getId("rs-email-tab").click(); + + realmSettingsPage.addSenderEmail("example@example.com"); + + cy.wait(100); + + realmSettingsPage.toggleCheck(realmSettingsPage.enableSslCheck); + realmSettingsPage.toggleCheck(realmSettingsPage.enableStartTlsCheck); + + realmSettingsPage.save(realmSettingsPage.emailSaveBtn); }); it("Go to themes tab", function () { @@ -42,7 +50,7 @@ describe("Realm settings test", () => { cy.getId("rs-themes-tab").click(); realmSettingsPage.selectLoginThemeType("keycloak"); realmSettingsPage.selectAccountThemeType("keycloak"); - realmSettingsPage.selectAdminThemeType("keycloak.v2"); + realmSettingsPage.selectAdminThemeType("base"); realmSettingsPage.selectEmailThemeType("base"); realmSettingsPage.saveThemes(); 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 291a95e16c..59a9fc2814 100644 --- a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts +++ b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts @@ -1,33 +1,30 @@ export default class RealmSettingsPage { - saveBtnGeneral: string; - saveBtnThemes: string; - loginTab: string; - selectLoginTheme: string; - loginThemeList: string; - selectAccountTheme: string; - accountThemeList: string; - selectAdminTheme: string; - adminThemeList: string; - selectEmailTheme: string; - emailThemeList: string; - selectDefaultLocale: string; - defaultLocaleList: string; - - constructor() { - this.saveBtnGeneral = "general-tab-save"; - this.saveBtnThemes = "themes-tab-save"; - this.loginTab = "rs-login-tab"; - this.selectLoginTheme = "#kc-login-theme"; - this.loginThemeList = "#kc-login-theme + ul"; - this.selectAccountTheme = "#kc-account-theme"; - this.accountThemeList = "#kc-account-theme + ul"; - this.selectAdminTheme = "#kc-admin-console-theme"; - this.adminThemeList = "#kc-admin-console-theme + ul"; - this.selectEmailTheme = "#kc-email-theme"; - this.emailThemeList = "#kc-email-theme + ul"; - this.selectDefaultLocale = "select-default-locale"; - this.defaultLocaleList = "select-default-locale + ul"; - } + generalSaveBtn = "general-tab-save"; + themesSaveBtn = "themes-tab-save"; + loginTab = "rs-login-tab"; + selectLoginTheme = "#kc-login-theme"; + loginThemeList = "#kc-login-theme + ul"; + selectAccountTheme = "#kc-account-theme"; + accountThemeList = "#kc-account-theme + ul"; + selectAdminTheme = "#kc-admin-console-theme"; + adminThemeList = "#kc-admin-console-theme + ul"; + selectEmailTheme = "#kc-email-theme"; + emailThemeList = "#kc-email-theme + ul"; + selectDefaultLocale = "select-default-locale"; + defaultLocaleList = "select-default-locale + ul"; + emailSaveBtn = "email-tab-save"; + managedAccessSwitch = "user-managed-access-switch"; + userRegSwitch = "user-reg-switch"; + forgotPwdSwitch = "forgot-pw-switch"; + rememberMeSwitch = "remember-me-switch"; + emailAsUsernameSwitch = "email-as-username-switch"; + loginWithEmailSwitch = "login-with-email-switch"; + duplicateEmailsSwitch = "duplicate-emails-switch"; + verifyEmailSwitch = "verify-email-switch"; + authSwitch = "email-authentication-switch"; + fromInput = "sender-email-address"; + enableSslCheck = "enable-ssl"; + enableStartTlsCheck = "enable-start-tls"; selectLoginThemeType(themeType: string) { cy.get(this.selectLoginTheme).click(); @@ -59,20 +56,42 @@ export default class RealmSettingsPage { return this; } + saveGeneral() { + cy.getId(this.generalSaveBtn).click(); + + return this; + } + + saveThemes() { + cy.getId(this.themesSaveBtn).click(); + + return this; + } + + addSenderEmail(senderEmail: string) { + cy.getId(this.fromInput).clear(); + + if (senderEmail) { + cy.getId(this.fromInput).type(senderEmail); + } + + return this; + } + toggleSwitch(switchName: string) { cy.getId(switchName).next().click(); return this; } - saveGeneral() { - cy.getId(this.saveBtnGeneral).click(); + toggleCheck(switchName: string) { + cy.getId(switchName).click(); return this; } - saveThemes() { - cy.getId(this.saveBtnThemes).click(); + save(saveBtn: string) { + cy.getId(saveBtn).click(); return this; } diff --git a/src/components/scroll-form/FormPanel.tsx b/src/components/scroll-form/FormPanel.tsx index fa5751bd22..4793dfde56 100644 --- a/src/components/scroll-form/FormPanel.tsx +++ b/src/components/scroll-form/FormPanel.tsx @@ -13,12 +13,18 @@ type FormPanelProps = { title: string; scrollId?: string; children: ReactNode; + className?: string; }; -export const FormPanel = ({ title, children, scrollId }: FormPanelProps) => { +export const FormPanel = ({ + title, + children, + scrollId, + className, +}: FormPanelProps) => { return ( - - + + { - {children} + {children} ); }; diff --git a/src/realm-settings/EmailTab.tsx b/src/realm-settings/EmailTab.tsx new file mode 100644 index 0000000000..42dada29cb --- /dev/null +++ b/src/realm-settings/EmailTab.tsx @@ -0,0 +1,295 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Controller, useFormContext, UseFormMethods } from "react-hook-form"; +import { + ActionGroup, + Button, + Checkbox, + FormGroup, + PageSection, + Switch, + TextInput, +} from "@patternfly/react-core"; + +import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation"; +import { FormAccess } from "../components/form-access/FormAccess"; +import { HelpItem } from "../components/help-enabler/HelpItem"; +import { FormPanel } from "../components/scroll-form/FormPanel"; + +import "./RealmSettingsSection.css"; +import { emailRegexPattern } from "../util"; + +export type UserFormProps = { + form: UseFormMethods; +}; + +type RealmSettingsEmailTabProps = { + save: (realm: RealmRepresentation) => void; + reset: () => void; +}; + +export const RealmSettingsEmailTab = ({ + save, + reset, +}: RealmSettingsEmailTabProps) => { + const { t } = useTranslation("realm-settings"); + const [isAuthenticationEnabled, setAuthenticationEnabled] = useState(""); + const { register, control, handleSubmit, errors } = useFormContext(); + + return ( + <> + + + + + + + + } + > + + + + + + + } + > + + + + } + > + + + + + + + + + + + + + + ( + onChange("" + value)} + /> + )} + /> + ( + onChange("" + value)} + /> + )} + /> + + + ( + { + onChange("" + value); + setAuthenticationEnabled(String(value)); + }} + /> + )} + /> + + {isAuthenticationEnabled === "true" && ( + <> + + + + + } + > + + + + )} + + + + + + + + + + ); +}; diff --git a/src/realm-settings/LoginTab.tsx b/src/realm-settings/LoginTab.tsx index a40ba619be..7329a5bf31 100644 --- a/src/realm-settings/LoginTab.tsx +++ b/src/realm-settings/LoginTab.tsx @@ -21,192 +21,188 @@ export const RealmSettingsLoginTab = ({ <> - { - - - } - hasNoPaddingTop - > - { - save({ ...realm, registrationAllowed: value }); - }} + + - - - } - hasNoPaddingTop - > - { - save({ ...realm, resetPasswordAllowed: value }); - }} + } + hasNoPaddingTop + > + { + save({ ...realm, registrationAllowed: value }); + }} + /> + + - - - } - hasNoPaddingTop - > - { - save({ ...realm, rememberMe: value }); - }} + } + hasNoPaddingTop + > + { + save({ ...realm, resetPasswordAllowed: value }); + }} + /> + + - - - } + } + hasNoPaddingTop + > + { + save({ ...realm, rememberMe: value }); + }} + /> + + - { - - - } - hasNoPaddingTop - > - { - save({ ...realm, registrationEmailAsUsername: value }); - }} + + - - - } - hasNoPaddingTop - > - { - save({ ...realm, loginWithEmailAllowed: value }); - }} + } + hasNoPaddingTop + > + { + save({ ...realm, registrationEmailAsUsername: value }); + }} + /> + + - - - } - hasNoPaddingTop - > - { - save({ ...realm, duplicateEmailsAllowed: value }); - }} - isDisabled={ - realm?.loginWithEmailAllowed || - realm?.registrationEmailAsUsername - } + } + hasNoPaddingTop + > + { + save({ ...realm, loginWithEmailAllowed: value }); + }} + /> + + - - + } + hasNoPaddingTop + > + - { - save({ ...realm, verifyEmail: value }); - }} + onChange={(value) => { + save({ ...realm, duplicateEmailsAllowed: value }); + }} + isDisabled={ + realm?.loginWithEmailAllowed || + realm?.registrationEmailAsUsername + } + /> + + - - - } + } + hasNoPaddingTop + > + { + save({ ...realm, verifyEmail: value }); + }} + /> + + diff --git a/src/realm-settings/RealmSettingsSection.css b/src/realm-settings/RealmSettingsSection.css new file mode 100644 index 0000000000..e7732d2dce --- /dev/null +++ b/src/realm-settings/RealmSettingsSection.css @@ -0,0 +1,18 @@ +.pf-c-card.pf-m-flat.kc-email-template, +.pf-c-card.pf-m-flat.kc-email-connection { + border: none; + margin-top: 0px; + margin-bottom: 0px; + padding-bottom: var(--pf-global--spacer--sm); +} + +div.pf-c-card__header.kc-form-panel__header { + padding-bottom: 0px; + padding-left: 0px; + padding-top: 0px; +} + +div.pf-c-card__body.kc-form-panel__body { + padding-left: 0px; + padding-bottom: var(--pf-global--spacer--2xl); +} diff --git a/src/realm-settings/RealmSettingsSection.tsx b/src/realm-settings/RealmSettingsSection.tsx index aa94adde9f..003aaa46f9 100644 --- a/src/realm-settings/RealmSettingsSection.tsx +++ b/src/realm-settings/RealmSettingsSection.tsx @@ -25,6 +25,7 @@ import { RealmSettingsLoginTab } from "./LoginTab"; import { RealmSettingsGeneralTab } from "./GeneralTab"; import { PartialImportDialog } from "./PartialImport"; import { RealmSettingsThemesTab } from "./ThemesTab"; +import { RealmSettingsEmailTab } from "./EmailTab"; type RealmSettingsHeaderProps = { onChange: (value: boolean) => void; @@ -186,6 +187,16 @@ export const RealmSettingsSection = () => { > + {t("realm-settings:email")}} + data-testid="rs-email-tab" + > + setupForm(realm!)} + /> + {t("realm-settings:themes")}} diff --git a/src/realm-settings/help.json b/src/realm-settings/help.json index fce3d33d47..bff586f239 100644 --- a/src/realm-settings/help.json +++ b/src/realm-settings/help.json @@ -1,5 +1,8 @@ { "realm-settings-help": { + "fromDisplayName": "A user-friendly name for the 'From' address (optional).", + "replyToDisplayName": "A user-friendly name for the 'Reply-To' address (optional).", + "envelopeFrom": "An email address used for bounces (optional).", "frontendUrl": "Set the frontend URL for the realm. Use in combination with the default hostname provider to override the base URL for frontend requests for a specific realm.", "requireSsl": "Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.", "userManagedAccess": "If enabled, users are allowed to manage their resources and permissions using the Account Management Console.", diff --git a/src/realm-settings/messages.json b/src/realm-settings/messages.json index a10dbb24ad..1608450234 100644 --- a/src/realm-settings/messages.json +++ b/src/realm-settings/messages.json @@ -14,6 +14,22 @@ "general": "General", "login": "Login", "themes": "Themes", + "email": "Email", + "template": "Template", + "connectionAndAuthentication": "Connection & Authentication", + "from": "From", + "fromDisplayName": "From display name", + "replyTo": "Reply to", + "replyToDisplayName": "Reply to display name", + "envelopeFrom": "Envelope from", + "host": "Host", + "port": "Port", + "encryption": "Encryption", + "authentication": "Authentication", + "enableSSL": "Enable SSL", + "enableStartTLS": "Enable StartTLS", + "username": "Username", + "password": "Password", "userRegistration": "User registration", "userRegistrationHelpText": "Enable/disable the registration page. A link for registration will show on login page too.", "forgotPassword": "Forgot password", diff --git a/src/user/UserForm.tsx b/src/user/UserForm.tsx index 7f5996dc54..9f6c3b9b5d 100644 --- a/src/user/UserForm.tsx +++ b/src/user/UserForm.tsx @@ -25,6 +25,7 @@ import moment from "moment"; import { JoinGroupDialog } from "./JoinGroupDialog"; import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation"; import { useAlerts } from "../components/alert/Alerts"; +import { emailRegexPattern } from "../util"; export type UserFormProps = { form: UseFormMethods; @@ -86,8 +87,6 @@ export const UserForm = ({ }); }; - const emailRegexPattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - const requiredUserActionsOptions = [ {t("configureOTP")} diff --git a/src/util.ts b/src/util.ts index 1eeff112a7..60ccf160cb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -91,3 +91,5 @@ export const getBaseUrl = (adminClient: KeycloakAdminClient) => { ? adminClient.keycloak.authServerUrl! : adminClient.baseUrl + "/"; }; + +export const emailRegexPattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;