diff --git a/cypress/integration/realm_settings_test.spec.ts b/cypress/integration/realm_settings_test.spec.ts new file mode 100644 index 0000000000..ee8aa79e58 --- /dev/null +++ b/cypress/integration/realm_settings_test.spec.ts @@ -0,0 +1,40 @@ +import SidebarPage from "../support/pages/admin_console/SidebarPage"; +import LoginPage from "../support/pages/LoginPage"; +import RealmSettingsPage from "../support/pages/admin_console/manage/realm_settings/RealmSettingsPage"; +import { keycloakBefore } from "../support/util/keycloak_before"; + +describe("Realm settings test", () => { + const loginPage = new LoginPage(); + 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(); + loginPage.logIn(); + }); + + it("Go to general tab", function () { + sidebarPage.goToRealmSettings(); + realmSettingsPage.toggleSwitch(managedAccessSwitch); + realmSettingsPage.save(); + realmSettingsPage.toggleSwitch(managedAccessSwitch); + realmSettingsPage.save(); + }); + + 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); + }); + }); +}); diff --git a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts new file mode 100644 index 0000000000..2f91179c8d --- /dev/null +++ b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts @@ -0,0 +1,40 @@ +export default class RealmSettingsPage { + saveBtn: string; + loginTab: string; + managedAccessSwitch: string; + userRegSwitch: string; + forgotPwdSwitch: string; + rememberMeSwitch: string; + emailAsUsernameSwitch: string; + loginWithEmailSwitch: string; + duplicateEmailsSwitch: string; + verifyEmailSwitch: string; + + + constructor() { + this.saveBtn = "general-tab-save"; + this.loginTab = "rs-login-tab"; + this.managedAccessSwitch = "user-managed-access-switch"; + this.userRegSwitch = "user-reg-switch" + this.forgotPwdSwitch = "forgot-password-switch" + this.rememberMeSwitch = "remember-me-switch" + this.emailAsUsernameSwitch = "email-as-username-switch" + this.loginWithEmailSwitch = "login-with-email-switch" + this.duplicateEmailsSwitch = "duplicate-emails-switch" + this.verifyEmailSwitch = "verify-email-switch" + + } + + toggleSwitch(switchName: string) { + cy.getId(switchName).next().click(); + + return this; + } + + save() { + cy.getId(this.saveBtn).click(); + + return this; + } + } + \ No newline at end of file diff --git a/src/components/scroll-form/FormPanel.tsx b/src/components/scroll-form/FormPanel.tsx index d598db7f7e..fa5751bd22 100644 --- a/src/components/scroll-form/FormPanel.tsx +++ b/src/components/scroll-form/FormPanel.tsx @@ -11,7 +11,7 @@ import "./form-panel.css"; type FormPanelProps = { title: string; - scrollId: string; + scrollId?: string; children: ReactNode; }; diff --git a/src/realm-settings/GeneralTab.tsx b/src/realm-settings/GeneralTab.tsx new file mode 100644 index 0000000000..1897aaa275 --- /dev/null +++ b/src/realm-settings/GeneralTab.tsx @@ -0,0 +1,230 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Controller, useForm } from "react-hook-form"; +import { useErrorHandler } from "react-error-boundary"; +import { + ActionGroup, + AlertVariant, + Button, + ClipboardCopy, + FormGroup, + PageSection, + Select, + SelectOption, + SelectVariant, + Stack, + StackItem, + Switch, + TextInput, +} from "@patternfly/react-core"; + +import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation"; +import { getBaseUrl } from "../util"; +import { useAdminClient, asyncStateFetch } from "../context/auth/AdminClient"; +import { useRealm } from "../context/realm-context/RealmContext"; +import { useAlerts } from "../components/alert/Alerts"; +import { FormAccess } from "../components/form-access/FormAccess"; +import { HelpItem } from "../components/help-enabler/HelpItem"; +import { FormattedLink } from "../components/external-link/FormattedLink"; + +export const RealmSettingsGeneralTab = () => { + const { t } = useTranslation("realm-settings"); + const adminClient = useAdminClient(); + const handleError = useErrorHandler(); + const { realm: realmName } = useRealm(); + const { addAlert } = useAlerts(); + const { register, control, setValue, handleSubmit } = useForm(); + const [realm, setRealm] = useState(); + const [open, setOpen] = useState(false); + + const baseUrl = getBaseUrl(adminClient); + + const requireSslTypes = ["all", "external", "none"]; + + useEffect(() => { + return asyncStateFetch( + () => adminClient.realms.findOne({ realm: realmName }), + (realm) => { + setRealm(realm); + setupForm(realm); + }, + handleError + ); + }, []); + + const setupForm = (realm: RealmRepresentation) => { + Object.entries(realm).map((entry) => setValue(entry[0], entry[1])); + }; + + const save = async (realm: RealmRepresentation) => { + try { + await adminClient.realms.update({ realm: realmName }, realm); + setRealm(realm); + addAlert(t("saveSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("saveError", { error }), AlertVariant.danger); + } + }; + + return ( + <> + + + + {realmName} + + + + + + + + + } + > + + + + } + > + ( + + )} + /> + + + } + fieldId="kc-user-manged-access" + > + ( + + )} + /> + + + } + fieldId="kc-endpoints" + > + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/realm-settings/LoginTab.tsx b/src/realm-settings/LoginTab.tsx new file mode 100644 index 0000000000..c59bc1372e --- /dev/null +++ b/src/realm-settings/LoginTab.tsx @@ -0,0 +1,240 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + AlertVariant, + FormGroup, + PageSection, + Switch, +} from "@patternfly/react-core"; +import { FormAccess } from "../components/form-access/FormAccess"; +import { HelpItem } from "../components/help-enabler/HelpItem"; +import { FormPanel } from "../components/scroll-form/FormPanel"; +import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient"; +import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation"; +import { useErrorHandler } from "react-error-boundary"; +import { useRealm } from "../context/realm-context/RealmContext"; +import { useAlerts } from "../components/alert/Alerts"; + +export const RealmSettingsLoginTab = () => { + const { t } = useTranslation("realm-settings"); + const [realm, setRealm] = useState(); + const handleError = useErrorHandler(); + const adminClient = useAdminClient(); + const { realm: realmName } = useRealm(); + const { addAlert } = useAlerts(); + + useEffect(() => { + return asyncStateFetch( + () => adminClient.realms.findOne({ realm: realmName }), + (realm) => { + setRealm(realm); + }, + handleError + ); + }, []); + + const save = async (realm: RealmRepresentation) => { + try { + await adminClient.realms.update({ realm: realmName }, realm); + setRealm(realm); + addAlert(t("saveSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("saveError", { error }), AlertVariant.danger); + } + }; + + return ( + <> + + + { + + + } + hasNoPaddingTop + > + { + save({ ...realm, registrationAllowed: value }); + }} + /> + + + } + hasNoPaddingTop + > + { + save({ ...realm, resetPasswordAllowed: value }); + }} + /> + + + } + hasNoPaddingTop + > + { + save({ ...realm, rememberMe: value }); + }} + /> + + + } + + + { + + + } + hasNoPaddingTop + > + { + save({ ...realm, registrationEmailAsUsername: value }); + }} + /> + + + } + hasNoPaddingTop + > + { + save({ ...realm, loginWithEmailAllowed: value }); + }} + /> + + + } + hasNoPaddingTop + > + { + save({ ...realm, duplicateEmailsAllowed: value }); + }} + isDisabled={ + realm?.loginWithEmailAllowed || + realm?.registrationEmailAsUsername + } + /> + + + } + hasNoPaddingTop + > + { + save({ ...realm, verifyEmail: value }); + }} + /> + + + } + + + + ); +}; diff --git a/src/realm-settings/RealmSettingsSection.tsx b/src/realm-settings/RealmSettingsSection.tsx index 075550e58d..48dc386278 100644 --- a/src/realm-settings/RealmSettingsSection.tsx +++ b/src/realm-settings/RealmSettingsSection.tsx @@ -4,37 +4,25 @@ import { useTranslation } from "react-i18next"; import { Controller, useForm } from "react-hook-form"; import { useErrorHandler } from "react-error-boundary"; import { - ActionGroup, AlertVariant, - Button, ButtonVariant, - ClipboardCopy, DropdownItem, DropdownSeparator, - FormGroup, PageSection, - Select, - SelectOption, - SelectVariant, - Stack, - StackItem, - Switch, Tab, TabTitleText, - TextInput, } from "@patternfly/react-core"; import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation"; -import { getBaseUrl, toUpperCase } from "../util"; +import { toUpperCase } from "../util"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useAdminClient, asyncStateFetch } from "../context/auth/AdminClient"; import { useRealm } from "../context/realm-context/RealmContext"; import { ViewHeader } from "../components/view-header/ViewHeader"; import { useAlerts } from "../components/alert/Alerts"; -import { FormAccess } from "../components/form-access/FormAccess"; -import { HelpItem } from "../components/help-enabler/HelpItem"; -import { FormattedLink } from "../components/external-link/FormattedLink"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; +import { RealmSettingsLoginTab } from "./LoginTab"; +import { RealmSettingsGeneralTab } from "./GeneralTab"; import { PartialImportDialog } from "./PartialImport"; type RealmSettingsHeaderProps = { @@ -128,25 +116,18 @@ const RealmSettingsHeader = ({ ); }; -const requireSslTypes = ["all", "external", "none"]; - export const RealmSettingsSection = () => { const { t } = useTranslation("realm-settings"); const adminClient = useAdminClient(); const handleError = useErrorHandler(); const { realm: realmName } = useRealm(); const { addAlert } = useAlerts(); - const { register, control, getValues, setValue, handleSubmit } = useForm(); - const [realm, setRealm] = useState(); - const [open, setOpen] = useState(false); - - const baseUrl = getBaseUrl(adminClient); + const { control, getValues, setValue } = useForm(); useEffect(() => { return asyncStateFetch( () => adminClient.realms.findOne({ realm: realmName }), (realm) => { - setRealm(realm); setupForm(realm); }, handleError @@ -160,7 +141,6 @@ export const RealmSettingsSection = () => { const save = async (realm: RealmRepresentation) => { try { await adminClient.realms.update({ realm: realmName }, realm); - setRealm(realm); addAlert(t("saveSuccess"), AlertVariant.success); } catch (error) { addAlert(t("saveError", { error }), AlertVariant.danger); @@ -182,169 +162,21 @@ export const RealmSettingsSection = () => { /> )} /> - {t("general")}} + title={{t("realm-settings:general")}} + data-testid="rs-general-tab" > - - - - {realmName} - - - - - - - - - } - > - - - - } - > - ( - - )} - /> - - - } - fieldId="kc-user-manged-access" - > - ( - - )} - /> - - - } - fieldId="kc-endpoints" - > - - - - - - - - - - - - - - - - + + + {t("realm-settings:login")}} + data-testid="rs-login-tab" + > + diff --git a/src/realm-settings/RealmSettingsTabs.tsx b/src/realm-settings/RealmSettingsTabs.tsx new file mode 100644 index 0000000000..fb80ad4dad --- /dev/null +++ b/src/realm-settings/RealmSettingsTabs.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { PageSection, Tab, TabTitleText } from "@patternfly/react-core"; +import { useTranslation } from "react-i18next"; +import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; +import { RealmSettingsLoginTab } from "./LoginTab"; +import { RealmSettingsGeneralTab } from "./GeneralTab"; + +export const RealmSettingsTabs = () => { + const { t } = useTranslation("roles"); + + return ( + <> + + + {t("realm-settings:general")}} + > + + + {t("realm-settings:login")}} + > + + + + + + ); +}; diff --git a/src/realm-settings/messages.json b/src/realm-settings/messages.json index d2e9745cba..87a6dff61c 100644 --- a/src/realm-settings/messages.json +++ b/src/realm-settings/messages.json @@ -12,6 +12,21 @@ "saveSuccess": "Realm successfully updated", "saveError": "Realm could not be updated: {error}", "general": "General", + "login": "Login", + "userRegistration": "User registration", + "userRegistrationHelpText": "Enable/disable the registration page. A link for registration will show on login page too.", + "forgotPassword": "Forgot password", + "forgotPasswordHelpText": "Show a link on login page for user to click when they have forgotten their credentials.", + "rememberMe": "Remember me", + "rememberMeHelpText": "Show checkbox on login page to allow user to remain logged in between browser restarts until session expires.", + "emailAsUsername": "Email as username", + "emailAsUsernameHelpText": "Allow users to set email as username.", + "loginWithEmail": "Login with email", + "loginWithEmailHelpText": "Allow users to log in with their email address.", + "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.", + "verifyEmail": "Verify email", + "verifyEmailHelpText": "Require user to verify their email address after initial login or after address changes are submitted.", "realmId": "Realm ID", "displayName": "Display name", "htmlDisplayName": "HTML Display name", diff --git a/src/route-config.ts b/src/route-config.ts index 5c169490f9..30b4960a3b 100644 --- a/src/route-config.ts +++ b/src/route-config.ts @@ -30,6 +30,7 @@ import { RealmRoleTabs } from "./realm-roles/RealmRoleTabs"; import { SearchGroups } from "./groups/SearchGroups"; import { CreateInitialAccessToken } from "./clients/initial-access/CreateInitialAccessToken"; import { LdapMappingDetails } from "./user-federation/ldap/mappers/LdapMappingDetails"; +import { RealmSettingsTabs } from "./realm-settings/RealmSettingsTabs"; export type RouteDef = BreadcrumbsRoute & { access: AccessType; @@ -172,11 +173,17 @@ export const routes: RoutesFn = (t: TFunction) => [ access: "view-events", }, { - path: "/:realm/realm-settings", + path: "/:realm/realm-settings/:tab?", component: RealmSettingsSection, breadcrumb: t("realmSettings"), access: "view-realm", }, + { + path: "/:realm/realm-settings/general", + component: RealmSettingsTabs, + breadcrumb: t("realmSettings"), + access: "view-realm", + }, { path: "/:realm/authentication", component: AuthenticationSection,