diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index c27ba0a6a3b5..1adbd6b62a4e 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3252,7 +3252,7 @@ userInvitedOrganization_one=Invite to user sent userInvitedOrganizationError=Could not invite user to the organizations\: {{error}} userInvitedOrganization_other={{count}} invites to users sent sentInvitation=Sent invitation -loggedInAsTempAdminUser=You are logged in as a temporary admin user. To harden security, create a permanent admin account and delete the temporary one. +loggedInAsTempAdminUser=You are logged in as a temporary admin user. To harden security, <1>create a permanent admin account and delete the temporary one. temporaryAdmin=Temporary admin user account. Ensure it is replaced with a permanent admin user account as soon as possible. temporaryService=Temporary admin service account. Ensure it is replaced with a permanent admin service account as soon as possible. addOrganizationAttributes.label=Add organization attributes diff --git a/js/apps/admin-ui/src/Banners.tsx b/js/apps/admin-ui/src/Banners.tsx index 7ddd2ef14135..1708cfe1a269 100644 --- a/js/apps/admin-ui/src/Banners.tsx +++ b/js/apps/admin-ui/src/Banners.tsx @@ -1,24 +1,32 @@ import { Banner, Flex, FlexItem } from "@patternfly/react-core"; import { ExclamationTriangleIcon } from "@patternfly/react-icons"; +import { ReactNode } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { useRealm } from "./context/realm-context/RealmContext"; import { useWhoAmI } from "./context/whoami/WhoAmI"; -import { useTranslation } from "react-i18next"; +import { toAddAdminUser } from "./user/routes/AddUser"; type WarnBannerProps = { - msg: string; + msg: string | ReactNode; }; const WarnBanner = ({ msg }: WarnBannerProps) => { const { t } = useTranslation(); return ( - + - {t(msg)} + {typeof msg === "string" ? t(msg) : msg} @@ -27,6 +35,20 @@ const WarnBanner = ({ msg }: WarnBannerProps) => { export const Banners = () => { const { whoAmI } = useWhoAmI(); + const { realm } = useRealm(); - if (whoAmI.isTemporary()) return ; + if (whoAmI.isTemporary()) + return ( + + You are logged in as a temporary admin user. To harden security, + + create a permanent admin account + + and delete the temporary one. + + } + /> + ); }; diff --git a/js/apps/admin-ui/src/user/CreateAdminUser.tsx b/js/apps/admin-ui/src/user/CreateAdminUser.tsx new file mode 100644 index 000000000000..2b599a6e6500 --- /dev/null +++ b/js/apps/admin-ui/src/user/CreateAdminUser.tsx @@ -0,0 +1,128 @@ +import { UserProfileMetadata } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; +import { + isUserProfileError, + KeycloakSpinner, + setUserProfileServerError, + useAlerts, + useFetch, + UserProfileFields, +} from "@keycloak/keycloak-ui-shared"; +import { PageSection } from "@patternfly/react-core"; +import { TFunction } from "i18next"; +import { useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useAdminClient } from "../admin-client"; +import { FixedButtonsGroup } from "../components/form/FixedButtonGroup"; +import { FormAccess } from "../components/form/FormAccess"; +import { ViewHeader } from "../components/view-header/ViewHeader"; +import { useRealm } from "../context/realm-context/RealmContext"; +import { useWhoAmI } from "../context/whoami/WhoAmI"; +import { toUserRepresentation, UserFormFields } from "./form-state"; +import { toUser } from "./routes/User"; +import { toUsers } from "./routes/Users"; +import { ResetPasswordForm } from "./user-credentials/ResetPasswordForm"; + +type AdminUserFields = UserFormFields & { + password: string; + passwordConfirmation: string; +}; + +export default function CreateAdminUser() { + const { t } = useTranslation(); + const form = useForm({ mode: "onChange" }); + const { handleSubmit } = form; + + const { whoAmI } = useWhoAmI(); + const currentLocale = whoAmI.getLocale(); + const { adminClient } = useAdminClient(); + const { addAlert, addError } = useAlerts(); + const navigate = useNavigate(); + const { realm: realmName, realmRepresentation: realm } = useRealm(); + const [userProfileMetadata, setUserProfileMetadata] = + useState(); + + useFetch( + () => adminClient.users.getProfileMetadata({ realm: realmName }), + (userProfileMetadata) => { + if (!userProfileMetadata) { + throw new Error(t("notFound")); + } + + form.setValue("attributes.locale", realm?.defaultLocale || ""); + setUserProfileMetadata(userProfileMetadata); + }, + [], + ); + + const save = async (data: AdminUserFields) => { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password, passwordConfirmation, ...user } = data; + const createdUser = await adminClient.users.create({ + ...toUserRepresentation(user), + enabled: true, + }); + await adminClient.users.resetPassword({ + id: createdUser.id!, + credential: { + type: "password", + value: password, + }, + }); + const role = await adminClient.roles.findOneByName({ name: "admin" }); + await adminClient.users.addRealmRoleMappings({ + id: createdUser.id, + roles: [{ name: role!.name!, id: role!.id! }], + }); + + addAlert(t("userCreated")); + navigate( + toUser({ id: createdUser.id, realm: realmName, tab: "settings" }), + ); + } catch (error) { + if (isUserProfileError(error)) { + setUserProfileServerError(error, form.setError, ((key, param) => + t(key as string, param as any)) as TFunction); + } else { + addError("userCreateError", error); + } + } + }; + + if (!realm || !userProfileMetadata) { + return ; + } + + return ( + <> + + + + + + + + navigate(toUsers({ realm: realm.realm! }))} + resetText={t("cancel")} + isSubmit + /> + + + + ); +} diff --git a/js/apps/admin-ui/src/user/EditUser.tsx b/js/apps/admin-ui/src/user/EditUser.tsx index b8bf43987a4b..b03dd25e3f03 100644 --- a/js/apps/admin-ui/src/user/EditUser.tsx +++ b/js/apps/admin-ui/src/user/EditUser.tsx @@ -3,6 +3,7 @@ import type { UserProfileMetadata, } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; import { + KeycloakSpinner, isUserProfileError, setUserProfileServerError, useAlerts, @@ -27,7 +28,6 @@ import { useNavigate } from "react-router-dom"; import { useAdminClient } from "../admin-client"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { KeyValueType } from "../components/key-value-form/key-value-convert"; -import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared"; import { RoutableTabs, useRoutableTab, @@ -35,19 +35,10 @@ import { import { ViewHeader } from "../components/view-header/ViewHeader"; import { useAccess } from "../context/access/Access"; import { useRealm } from "../context/realm-context/RealmContext"; +import { UserEvents } from "../events/UserEvents"; import { UserProfileProvider } from "../realm-settings/user-profile/UserProfileContext"; import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled"; import { useParams } from "../utils/useParams"; -import { Organizations } from "./Organizations"; -import { UserAttributes } from "./UserAttributes"; -import { UserConsents } from "./UserConsents"; -import { UserCredentials } from "./UserCredentials"; -import { BruteForced, UserForm } from "./UserForm"; -import { UserGroups } from "./UserGroups"; -import { UserIdentityProviderLinks } from "./UserIdentityProviderLinks"; -import { UserRoleMapping } from "./UserRoleMapping"; -import { UserSessions } from "./UserSessions"; -import { UserEvents } from "../events/UserEvents"; import { UIUserRepresentation, UserFormFields, @@ -55,8 +46,17 @@ import { toUserFormFields, toUserRepresentation, } from "./form-state"; +import { Organizations } from "./Organizations"; import { UserParams, UserTab, toUser } from "./routes/User"; import { toUsers } from "./routes/Users"; +import { UserAttributes } from "./UserAttributes"; +import { UserConsents } from "./UserConsents"; +import { UserCredentials } from "./UserCredentials"; +import { BruteForced, UserForm } from "./UserForm"; +import { UserGroups } from "./UserGroups"; +import { UserIdentityProviderLinks } from "./UserIdentityProviderLinks"; +import { UserRoleMapping } from "./UserRoleMapping"; +import { UserSessions } from "./UserSessions"; import { isLightweightUser } from "./utils"; import "./user-section.css"; @@ -102,18 +102,18 @@ export default function EditUser() { tab, }); - const useTab = (tab: UserTab) => useRoutableTab(toTab(tab)); - - const settingsTab = useTab("settings"); - const attributesTab = useTab("attributes"); - const credentialsTab = useTab("credentials"); - const roleMappingTab = useTab("role-mapping"); - const groupsTab = useTab("groups"); - const organizationsTab = useTab("organizations"); - const consentsTab = useTab("consents"); - const identityProviderLinksTab = useTab("identity-provider-links"); - const sessionsTab = useTab("sessions"); - const userEventsTab = useTab("user-events"); + const settingsTab = useRoutableTab(toTab("settings")); + const attributesTab = useRoutableTab(toTab("attributes")); + const credentialsTab = useRoutableTab(toTab("credentials")); + const roleMappingTab = useRoutableTab(toTab("role-mapping")); + const groupsTab = useRoutableTab(toTab("groups")); + const organizationsTab = useRoutableTab(toTab("organizations")); + const consentsTab = useRoutableTab(toTab("consents")); + const identityProviderLinksTab = useRoutableTab( + toTab("identity-provider-links"), + ); + const sessionsTab = useRoutableTab(toTab("sessions")); + const userEventsTab = useRoutableTab(toTab("user-events")); useFetch( async () => @@ -142,17 +142,19 @@ export default function EditUser() { const { userProfileMetadata, ...user } = userData; setUserProfileMetadata(userProfileMetadata); - user.unmanagedAttributes = unmanagedAttributes; - user.attributes = filterManagedAttributes( - user.attributes, + setUser({ + ...user, unmanagedAttributes, - ); + attributes: filterManagedAttributes( + user.attributes, + unmanagedAttributes, + ), + }); if (upConfig.unmanagedAttributePolicy !== undefined) { setUnmanagedAttributesEnabled(true); } - setUser(user); setUpConfig(upConfig); const isBruteForceProtected = realm.bruteForceProtected; diff --git a/js/apps/admin-ui/src/user/routes.ts b/js/apps/admin-ui/src/user/routes.ts index 53e6e8307657..fe17d4883f7b 100644 --- a/js/apps/admin-ui/src/user/routes.ts +++ b/js/apps/admin-ui/src/user/routes.ts @@ -1,10 +1,11 @@ import type { AppRouteObject } from "../routes"; -import { AddUserRoute } from "./routes/AddUser"; +import { AddAdminUserRoute, AddUserRoute } from "./routes/AddUser"; import { UserRoute } from "./routes/User"; import { UsersRoute, UsersRouteWithTab } from "./routes/Users"; const routes: AppRouteObject[] = [ AddUserRoute, + AddAdminUserRoute, UsersRoute, UsersRouteWithTab, UserRoute, diff --git a/js/apps/admin-ui/src/user/routes/AddUser.tsx b/js/apps/admin-ui/src/user/routes/AddUser.tsx index 9743c5dae77e..8c3c0c8b7aa9 100644 --- a/js/apps/admin-ui/src/user/routes/AddUser.tsx +++ b/js/apps/admin-ui/src/user/routes/AddUser.tsx @@ -7,6 +7,7 @@ import type { AppRouteObject } from "../../routes"; export type AddUserParams = { realm: string }; const CreateUser = lazy(() => import("../CreateUser")); +const CreateAdminUser = lazy(() => import("../CreateAdminUser")); export const AddUserRoute: AppRouteObject = { path: "/:realm/users/add-user", @@ -17,6 +18,19 @@ export const AddUserRoute: AppRouteObject = { }, }; +export const AddAdminUserRoute: AppRouteObject = { + path: "/:realm/users/add-admin-user", + element: , + breadcrumb: (t) => t("createUser"), + handle: { + access: ["query-users"], + }, +}; + +export const toAddAdminUser = (params: AddUserParams): Partial => ({ + pathname: generateEncodedPath(AddAdminUserRoute.path, params), +}); + export const toAddUser = (params: AddUserParams): Partial => ({ pathname: generateEncodedPath(AddUserRoute.path, params), }); diff --git a/js/apps/admin-ui/src/user/user-credentials/ResetPasswordDialog.tsx b/js/apps/admin-ui/src/user/user-credentials/ResetPasswordDialog.tsx index 622a8625a9d3..2566bcb810bf 100644 --- a/js/apps/admin-ui/src/user/user-credentials/ResetPasswordDialog.tsx +++ b/js/apps/admin-ui/src/user/user-credentials/ResetPasswordDialog.tsx @@ -1,22 +1,17 @@ import { RequiredActionAlias } from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation"; import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; -import { - AlertVariant, - ButtonVariant, - Form, - FormGroup, -} from "@patternfly/react-core"; +import { useAlerts } from "@keycloak/keycloak-ui-shared"; +import { AlertVariant, ButtonVariant, Form } from "@patternfly/react-core"; import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { FormErrorText, PasswordInput } from "@keycloak/keycloak-ui-shared"; import { useAdminClient } from "../../admin-client"; -import { DefaultSwitchControl } from "../../components/SwitchControl"; -import { useAlerts } from "@keycloak/keycloak-ui-shared"; import { ConfirmDialogModal, useConfirmDialog, } from "../../components/confirm-dialog/ConfirmDialog"; +import { DefaultSwitchControl } from "../../components/SwitchControl"; import useToggle from "../../utils/useToggle"; +import { ResetPasswordForm } from "./ResetPasswordForm"; type ResetPasswordDialogProps = { user: UserRepresentation; @@ -53,17 +48,11 @@ export const ResetPasswordDialog = ({ mode: "onChange", }); const { - register, - formState: { isValid, errors }, - watch, + formState: { isValid }, handleSubmit, - clearErrors, - setError, } = form; const [confirm, toggle] = useToggle(true); - const password = watch("password", ""); - const passwordConfirmation = watch("passwordConfirmation", ""); const { addAlert, addError } = useAlerts(); @@ -123,7 +112,6 @@ export const ResetPasswordDialog = ({ onClose(); }; - const { onChange, ...rest } = register("password", { required: true }); return ( <> @@ -145,56 +133,8 @@ export const ResetPasswordDialog = ({ isHorizontal className="keycloak__user-credentials__reset-form" > - - { - onChange(e); - if (passwordConfirmation !== e.currentTarget.value) { - setError("passwordConfirmation", { - message: t("confirmPasswordDoesNotMatch").toString(), - }); - } else { - clearErrors("passwordConfirmation"); - } - }} - {...rest} - /> - {errors.password && } - - - - value === password || - t("confirmPasswordDoesNotMatch").toString(), - })} - /> - {errors.passwordConfirmation && ( - - )} - + { + const { t } = useTranslation(); + const form = useFormContext(); + const { + register, + formState: { errors }, + watch, + clearErrors, + setError, + } = form; + + const password = watch("password", ""); + const passwordConfirmation = watch("passwordConfirmation", ""); + const { onChange, ...rest } = register("password", { required: true }); + + return ( + <> + + { + onChange(e); + if (passwordConfirmation !== e.currentTarget.value) { + setError("passwordConfirmation", { + message: t("confirmPasswordDoesNotMatch").toString(), + }); + } else { + clearErrors("passwordConfirmation"); + } + }} + {...rest} + /> + {errors.password && } + + + + value === password || t("confirmPasswordDoesNotMatch").toString(), + })} + /> + {errors.passwordConfirmation && ( + + )} + + + ); +};