import React, { Fragment, FunctionComponent, useMemo, useState } from "react"; import { AlertVariant, Button, ButtonVariant, Divider, Dropdown, DropdownItem, DropdownPosition, Form, FormGroup, KebabToggle, Modal, ModalVariant, Select, SelectOption, SelectVariant, Switch, Text, TextInput, TextVariants, ValidatedOptions, } from "@patternfly/react-core"; import { Table, TableBody, TableComposable, TableHeader, TableVariant, Td, Th, Thead, Tr, } from "@patternfly/react-table"; import { PencilAltIcon, CheckIcon, TimesIcon } from "@patternfly/react-icons"; import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import { useTranslation } from "react-i18next"; import { isEmpty } from "lodash/fp"; import { useAlerts } from "../components/alert/Alerts"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useWhoAmI } from "../context/whoami/WhoAmI"; import { Controller, useForm, UseFormMethods, useWatch } from "react-hook-form"; import { PasswordInput } from "../components/password-input/PasswordInput"; import { HelpItem } from "../components/help-enabler/HelpItem"; import "./user-credentials.css"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation"; import { FormAccess } from "../components/form-access/FormAccess"; import { RequiredActionAlias } from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation"; import { TimeSelector } from "../components/time-selector/TimeSelector"; type UserCredentialsProps = { user: UserRepresentation; }; type CredentialsForm = { password: string; passwordConfirmation: string; temporaryPassword: boolean; }; type CredentialResetForm = { actions: RequiredActionAlias[]; lifespan: number; }; const credFormDefaultValues: CredentialsForm = { password: "", passwordConfirmation: "", temporaryPassword: true, }; const credResetFormDefaultValues: CredentialResetForm = { actions: [], lifespan: 43200, // 12 hours }; type DisplayDialogProps = { titleKey: string; onClose: () => void; }; type UserLabelForm = { userLabel: string; }; const userLabelDefaultValues: UserLabelForm = { userLabel: "", }; type ExpandableCredentialRepresentation = { key: string; value: CredentialRepresentation[]; isExpanded: boolean; }; const DisplayDialog: FunctionComponent = ({ titleKey, onClose, children, }) => { const { t } = useTranslation("users"); return ( {children} ); }; const CredentialsResetActionMultiSelect = (props: { form: UseFormMethods; }) => { const { t } = useTranslation("users"); const [open, setOpen] = useState(false); const { form } = props; const { control } = form; return ( } fieldId="actions" > ( )} /> ); }; const LifespanField = ({ form: { control }, }: { form: UseFormMethods; }) => { const { t } = useTranslation("users"); return ( } > ( )} /> ); }; export const UserCredentials = ({ user }: UserCredentialsProps) => { const { t } = useTranslation("users"); const { whoAmI } = useWhoAmI(); const { addAlert, addError } = useAlerts(); const [key, setKey] = useState(0); const refresh = () => setKey(key + 1); const [open, setOpen] = useState(false); const [openSaveConfirm, setOpenSaveConfirm] = useState(false); const [openCredentialReset, setOpenCredentialReset] = useState(false); const [kebabOpen, setKebabOpen] = useState({ status: false, rowKey: "", }); const adminClient = useAdminClient(); const form = useForm({ defaultValues: credFormDefaultValues, }); const resetForm = useForm({ defaultValues: credResetFormDefaultValues, }); const userLabelForm = useForm({ defaultValues: userLabelDefaultValues, }); const { control, errors, handleSubmit, register } = form; const { control: resetControl, handleSubmit: resetHandleSubmit } = resetForm; const { getValues: getValues1, handleSubmit: handleSubmit1, register: register1, } = userLabelForm; const [credentials, setCredentials] = useState(); const [credentialsReset, setCredentialReset] = useState( {} as CredentialResetForm ); const [userCredentials, setUserCredentials] = useState< CredentialRepresentation[] >([]); const [groupedUserCredentials, setGroupedUserCredentials] = useState< ExpandableCredentialRepresentation[] >([]); const [selectedCredential, setSelectedCredential] = useState({}); const [isResetPassword, setIsResetPassword] = useState(false); const [showData, setShowData] = useState(false); const [editedUserCredential, setEditedUserCredential] = useState({}); const [isUserLabelEdit, setIsUserLabelEdit] = useState<{ status: boolean; rowKey: string; }>(); useFetch( () => adminClient.users.getCredentials({ id: user.id! }), (credentials) => { setUserCredentials(credentials); const groupedCredentials = credentials.reduce((r, a) => { r[a.type!] = r[a.type!] || []; r[a.type!].push(a); return r; }, Object.create(null)); const groupedCredentialsArray = Object.keys(groupedCredentials).map( (key) => ({ key, value: groupedCredentials[key] }) ); setGroupedUserCredentials( groupedCredentialsArray.map((groupedCredential) => ({ ...groupedCredential, isExpanded: false, })) ); }, [key] ); const passwordTypeFinder = userCredentials.find( (credential) => credential.type === "password" ); const passwordWatcher = useWatch({ control, name: "password", }); const resetActionWatcher = useWatch({ control: resetControl, name: "actions", }); const passwordConfirmationWatcher = useWatch< CredentialsForm["passwordConfirmation"] >({ control, name: "passwordConfirmation", }); const isNotDisabled = passwordWatcher !== "" && passwordConfirmationWatcher !== ""; const resetIsNotDisabled = !isEmpty(resetActionWatcher); const toggleModal = () => { setOpen(!open); }; const toggleCredentialsResetModal = () => { setOpenCredentialReset(!openCredentialReset); }; const toggleConfirmSaveModal = () => { setOpenSaveConfirm(!openSaveConfirm); }; const saveUserPassword = async () => { if (!credentials) { return; } const passwordsMatch = credentials.password === credentials.passwordConfirmation; if (!passwordsMatch) { addAlert( isResetPassword ? t("resetPasswordNotMatchError") : t("savePasswordNotMatchError"), AlertVariant.danger ); } else { try { await adminClient.users.resetPassword({ id: user.id!, credential: { temporary: credentials.temporaryPassword, type: "password", value: credentials.password, }, }); refresh(); addAlert( isResetPassword ? t("resetCredentialsSuccess") : t("savePasswordSuccess"), AlertVariant.success ); setIsResetPassword(false); setOpenSaveConfirm(false); } catch (error) { addError( isResetPassword ? "users:resetPasswordError" : "users:savePasswordError", error ); } } }; const sendCredentialsResetEmail = async () => { if (isEmpty(credentialsReset.actions)) { return; } try { await adminClient.users.executeActionsEmail({ id: user.id!, actions: credentialsReset.actions, lifespan: credentialsReset.lifespan, }); refresh(); addAlert(t("credentialResetEmailSuccess"), AlertVariant.success); setOpenCredentialReset(false); } catch (error) { addError(t("credentialResetEmailError"), error); } }; const resetPassword = () => { setIsResetPassword(true); setOpen(true); }; const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: t("deleteCredentialsConfirmTitle"), messageKey: t("deleteCredentialsConfirm"), continueButtonLabel: t("common:delete"), continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { await adminClient.users.deleteCredential({ id: user.id!, credentialId: selectedCredential.id!, }); addAlert(t("deleteCredentialsSuccess"), AlertVariant.success); setKey((key) => key + 1); } catch (error) { addError("users:deleteCredentialsError", error); } }, }); const rows = useMemo(() => { if (!selectedCredential.credentialData) { return []; } const credentialData = JSON.parse(selectedCredential.credentialData); const locale = whoAmI.getLocale(); return Object.entries(credentialData) .sort(([a], [b]) => a.localeCompare(b, locale)) .map<[string, string]>(([key, value]) => { if (typeof value === "string") { return [key, value]; } return [key, JSON.stringify(value)]; }); }, [selectedCredential.credentialData]); const saveUserLabel = async () => { const credentialToEdit = userCredentials.find( (credential) => credential.id === editedUserCredential.id ); const userLabelFormValue = getValues1(); if (!credentialToEdit) { return; } try { await adminClient.users.updateCredentialLabel( { id: user.id!, credentialId: credentialToEdit.id!, }, userLabelFormValue.userLabel || "" ); refresh(); addAlert(t("updateCredentialUserLabelSuccess"), AlertVariant.success); setEditedUserCredential({}); } catch (error) { addError("users:updateCredentialUserLabelError", error); } setIsUserLabelEdit({ status: false, rowKey: credentialToEdit.id!, }); }; return ( <> {open && ( { setIsResetPassword(false); setOpen(false); }} actions={[ , , ]} >
} fieldId="kc-temporaryPassword" > ( onChange(value)} isChecked={value} label={t("common:on")} labelOff={t("common:off")} /> )} />
)} {openSaveConfirm && ( setOpenSaveConfirm(false)} actions={[ , , ]} > {isResetPassword ? `${t("resetPasswordConfirmText")} ${user.username} ${t( "questionMark" )}` : `${t("setPasswordConfirmText")} ${user.username} ${t( "questionMark" )}`} )} {openCredentialReset && ( { setOpenCredentialReset(false); }} data-testid="credential-reset-modal" actions={[ , , ]} >
)} {showData && Object.keys(selectedCredential).length !== 0 && ( { setShowData(false); setSelectedCredential({}); }} >
)} {userCredentials.length !== 0 && passwordTypeFinder === undefined && ( <> )} {groupedUserCredentials.length !== 0 ? ( <> {user.email && ( )} {t("type")} {t("userLabel")} {t("data")} {groupedUserCredentials.map((groupedCredential, rowIndex) => ( {groupedCredential.value.length > 1 ? ( { const rows = groupedUserCredentials.map( (credential, index) => index === rowIndex ? { ...credential, isExpanded: !credential.isExpanded, } : credential ); setGroupedUserCredentials(rows); }, }} /> ) : ( )} {groupedCredential.key.charAt(0).toUpperCase()! + groupedCredential.key.slice(1)} {groupedCredential.value.length <= 1 && groupedCredential.value.map((credential) => ( <>
{isUserLabelEdit?.status && isUserLabelEdit.rowKey === credential.id ? ( <>
) : ( <> {credential.userLabel ?? ""}
{credential.type === "password" ? ( ) : ( )} setKebabOpen({ status, rowKey: credential.id!, }) } /> } isOpen={ kebabOpen.status && kebabOpen.rowKey === credential.id } onSelect={() => { setSelectedCredential(credential); }} dropdownItems={[ { toggleDeleteDialog(); setKebabOpen({ status: false, rowKey: credential.id!, }); }} > {t("deleteBtn")} , ]} /> ))} {groupedCredential.isExpanded && groupedCredential.value.map((credential) => ( {credential.type!.charAt(0).toUpperCase()! + credential.type!.slice(1)}
{isUserLabelEdit?.status && isUserLabelEdit.rowKey === credential.id ? ( <>
) : ( <> {credential.userLabel ?? ""}
setKebabOpen({ status, rowKey: credential.id!, }) } /> } isOpen={ kebabOpen.status && kebabOpen.rowKey === credential.id } onSelect={() => { setSelectedCredential(credential); }} dropdownItems={[ { toggleDeleteDialog(); setKebabOpen({ status: false, rowKey: credential.id!, }); }} > {t("deleteBtn")} , ]} /> ))}
))}
) : ( )} ); };