From f963b3651c657070dbb289d02c1995ab8668abfb Mon Sep 17 00:00:00 2001 From: Mart van Santen <mart@greenhost.nl> Date: Wed, 22 Feb 2023 00:12:47 +0800 Subject: [PATCH] Added password reset link function --- frontend/Dockerfile | 2 +- .../components/Modal/InfoModal/InfoModal.tsx | 86 +++++++++++++++++++ .../src/components/Modal/InfoModal/index.ts | 1 + frontend/src/components/Modal/Modal/Modal.tsx | 6 +- frontend/src/components/Modal/Modal/types.ts | 3 +- frontend/src/components/Modal/index.ts | 1 + .../src/components/UserModal/UserModal.tsx | 44 +++++++++- frontend/src/components/index.ts | 2 +- .../src/services/users/hooks/use-users.ts | 9 +- frontend/src/services/users/redux/actions.ts | 18 ++++ frontend/src/services/users/redux/reducers.ts | 5 ++ .../src/services/users/redux/selectors.ts | 1 + frontend/src/services/users/redux/types.ts | 1 + .../src/services/users/transformations.ts | 4 + 14 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/Modal/InfoModal/InfoModal.tsx create mode 100644 frontend/src/components/Modal/InfoModal/index.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6a7e2195..47b5c6e1 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /home/node/app # First copy only files necessary for installing dependencies, so that we can # cache that step even when our own source code changes. -COPY package.json yarn.lock . +COPY package.json yarn.lock ./ RUN yarn install diff --git a/frontend/src/components/Modal/InfoModal/InfoModal.tsx b/frontend/src/components/Modal/InfoModal/InfoModal.tsx new file mode 100644 index 00000000..2e69dba8 --- /dev/null +++ b/frontend/src/components/Modal/InfoModal/InfoModal.tsx @@ -0,0 +1,86 @@ +import React, { Fragment, useRef } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; +import { InformationCircleIcon } from '@heroicons/react/outline'; + +type InfoModalProps = { + open: boolean; + onClose: () => void; + title: string; + body: string; + dynamicData: string; +}; + +export const InfoModal = ({ open, onClose, title, body, dynamicData }: InfoModalProps) => { + const cancelButtonRef = useRef(null); + + return ( + <Transition.Root show={open} as={Fragment}> + <Dialog + as="div" + auto-reopen="true" + className="fixed z-10 inset-0 overflow-y-auto" + initialFocus={cancelButtonRef} + onClose={onClose} + > + <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> + </Transition.Child> + + {/* This element is to trick the browser into centering the modal contents. */} + <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> + ​ + </span> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" + enterTo="opacity-100 translate-y-0 sm:scale-100" + leave="ease-in duration-200" + leaveFrom="opacity-100 translate-y-0 sm:scale-100" + leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" + > + <div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> + <div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> + <div className="sm:flex sm:items-start"> + <div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"> + <InformationCircleIcon className="h-6 w-6 text-blue-600" aria-hidden="true" /> + </div> + <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> + <Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900"> + {title} + </Dialog.Title> + <div className="mt-2"> + <p className="text-sm text-gray-500">{body}</p> + </div> + <div className="mt-2"> + <p className="text-sm text-gray-500">{dynamicData}</p> + </div> + </div> + </div> + </div> + <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> + <button + type="button" + className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" + onClick={onClose} + ref={cancelButtonRef} + > + Close + </button> + </div> + </div> + </Transition.Child> + </div> + </Dialog> + </Transition.Root> + ); +}; diff --git a/frontend/src/components/Modal/InfoModal/index.ts b/frontend/src/components/Modal/InfoModal/index.ts new file mode 100644 index 00000000..3e5e1e45 --- /dev/null +++ b/frontend/src/components/Modal/InfoModal/index.ts @@ -0,0 +1 @@ +export { InfoModal } from './InfoModal'; diff --git a/frontend/src/components/Modal/Modal/Modal.tsx b/frontend/src/components/Modal/Modal/Modal.tsx index d64acfec..ee4fc603 100644 --- a/frontend/src/components/Modal/Modal/Modal.tsx +++ b/frontend/src/components/Modal/Modal/Modal.tsx @@ -13,7 +13,8 @@ export const Modal: React.FC<ModalProps> = ({ useCancelButton = false, cancelButtonTitle = 'Cancel', isLoading = false, - leftActions = <></>, + leftActionA = <></>, + leftActionB = <></>, saveButtonDisabled = false, }) => { const cancelButtonRef = useRef(null); @@ -85,7 +86,8 @@ export const Modal: React.FC<ModalProps> = ({ {onSave && ( <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex"> - {leftActions} + {leftActionA} + {leftActionB} <div className="ml-auto sm:flex sm:flex-row-reverse"> <button type="button" diff --git a/frontend/src/components/Modal/Modal/types.ts b/frontend/src/components/Modal/Modal/types.ts index 52cf784b..bd790e0c 100644 --- a/frontend/src/components/Modal/Modal/types.ts +++ b/frontend/src/components/Modal/Modal/types.ts @@ -9,6 +9,7 @@ export type ModalProps = { useCancelButton?: boolean; cancelButtonTitle?: string; isLoading?: boolean; - leftActions?: React.ReactNode; + leftActionA?: React.ReactNode; + leftActionB?: React.ReactNode; saveButtonDisabled?: boolean; }; diff --git a/frontend/src/components/Modal/index.ts b/frontend/src/components/Modal/index.ts index e5800df0..32c47ffa 100644 --- a/frontend/src/components/Modal/index.ts +++ b/frontend/src/components/Modal/index.ts @@ -1,3 +1,4 @@ export { ConfirmationModal } from './ConfirmationModal'; +export { InfoModal } from './InfoModal'; export { Modal } from './Modal'; export { StepsModal } from './StepsModal'; diff --git a/frontend/src/components/UserModal/UserModal.tsx b/frontend/src/components/UserModal/UserModal.tsx index 29e9738d..3a8dda58 100644 --- a/frontend/src/components/UserModal/UserModal.tsx +++ b/frontend/src/components/UserModal/UserModal.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import _ from 'lodash'; import { TrashIcon } from '@heroicons/react/outline'; import { useFieldArray, useForm, useWatch } from 'react-hook-form'; -import { Banner, Modal, ConfirmationModal } from 'src/components'; +import { Banner, Modal, ConfirmationModal, InfoModal } from 'src/components'; import { Input, Select } from 'src/components/Form'; import { User, UserRole, useUsers } from 'src/services/users'; import { useAuth } from 'src/services/auth'; @@ -12,10 +12,15 @@ import { UserModalProps } from './types'; export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) => { const [deleteModal, setDeleteModal] = useState(false); + const [passwordLinkModal, setPasswordLinkModal] = useState(false); + // const [passwordLinkBody, setPasswordLinkBody] = useState({ + // value: 'start', + // }); const [isAdminRoleSelected, setAdminRoleSelected] = useState(true); const [isPersonalModal, setPersonalModal] = useState(false); const { user, + recoveryLink, loadUser, loadPersonalInfo, editUserById, @@ -23,6 +28,7 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) createNewUser, userModalLoading, deleteUserById, + getRecoveryLinkUserById, clearSelectedUser, } = useUsers(); const { currentUser, isAdmin } = useAuth(); @@ -108,6 +114,18 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) const deleteModalOpen = () => setDeleteModal(true); const deleteModalClose = () => setDeleteModal(false); + const passwordLinkModalOpen = () => { + if (userId) { + getRecoveryLinkUserById(userId); + // setPasswordLinkBody({ + // value: userId, + // }); + } + setPasswordLinkModal(true); + }; + + const passwordLinkModalClose = () => setPasswordLinkModal(false); + const handleDelete = () => { if (userId) { deleteUserById(userId); @@ -126,7 +144,7 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) open={open} onSave={handleSave} isLoading={userModalLoading} - leftActions={ + leftActionA={ userId && user.email !== currentUser?.email && ( <button @@ -139,6 +157,19 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) </button> ) } + leftActionB={ + userId && + isAdmin && ( + <button + onClick={passwordLinkModalOpen} + type="button" + className="mb-4 sm:mb-0 inline-flex items-center px-4 py-2 text-sm +font-medium rounded-md text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" + > + Password Link + </button> + ) + } useCancelButton > <div className="bg-white px-4"> @@ -201,7 +232,6 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) )} </div> </div> - {isAdmin && !userModalLoading && ( <div> <div className="mt-8"> @@ -265,7 +295,6 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) </div> </div> </Modal> - <ConfirmationModal onDeleteAction={handleDelete} open={deleteModal} @@ -273,6 +302,13 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) title="Delete user" body="Are you sure you want to delete this user? All of the user data will be permanently removed. This action cannot be undone." /> + <InfoModal + open={passwordLinkModal} + onClose={passwordLinkModalClose} + title="Password reset link" + body="Below is a password reset link. Copy this link and send to the user to reset the password without using e-mail" + dynamicData={recoveryLink} + /> </> ); }; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 9a2f607c..64e430d5 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -3,6 +3,6 @@ export { Header } from './Header'; export { Table } from './Table'; export { Banner } from './Banner'; export { Tabs } from './Tabs'; -export { Modal, ConfirmationModal, StepsModal } from './Modal'; +export { Modal, ConfirmationModal, InfoModal, StepsModal } from './Modal'; export { UserModal } from './UserModal'; export { ProgressSteps } from './ProgressSteps'; diff --git a/frontend/src/services/users/hooks/use-users.ts b/frontend/src/services/users/hooks/use-users.ts index 2d0381de..447bcce9 100644 --- a/frontend/src/services/users/hooks/use-users.ts +++ b/frontend/src/services/users/hooks/use-users.ts @@ -10,13 +10,15 @@ import { deleteUser, clearCurrentUser, createBatchUsers, + fetchRecoveryLink, } from '../redux'; -import { getUserById, getUserModalLoading, getUserslLoading } from '../redux/selectors'; +import { getUserById, getRecoveryLink, getUserModalLoading, getUserslLoading } from '../redux/selectors'; export function useUsers() { const dispatch = useDispatch(); const users = useSelector(getUsers); const user = useSelector(getUserById); + const recoveryLink = useSelector(getRecoveryLink); const userModalLoading = useSelector(getUserModalLoading); const userTableLoading = useSelector(getUserslLoading); @@ -55,10 +57,14 @@ export function useUsers() { function deleteUserById(id: string) { return dispatch(deleteUser(id)); } + function getRecoveryLinkUserById(id: string) { + return dispatch(fetchRecoveryLink(id)); + } return { users, user, + recoveryLink, loadUser, loadUsers, loadPersonalInfo, @@ -68,6 +74,7 @@ export function useUsers() { userTableLoading, createNewUser, deleteUserById, + getRecoveryLinkUserById, clearSelectedUser, createUsers, }; diff --git a/frontend/src/services/users/redux/actions.ts b/frontend/src/services/users/redux/actions.ts index 7643b41e..e038ce88 100644 --- a/frontend/src/services/users/redux/actions.ts +++ b/frontend/src/services/users/redux/actions.ts @@ -9,6 +9,7 @@ import { transformRequestMultipleUsers, transformRequestUser, transformUser, + transformRecoveryLink, } from '../transformations'; export enum UserActionTypes { @@ -17,6 +18,7 @@ export enum UserActionTypes { UPDATE_USER = 'users/update_user', CREATE_USER = 'users/create_user', DELETE_USER = 'users/delete_user', + FETCH_RECOVERY_LINK = 'users/recoverylink_user', SET_USER_MODAL_LOADING = 'users/user_modal_loading', SET_USERS_LOADING = 'users/users_loading', CREATE_BATCH_USERS = 'users/create_batch_users', @@ -185,6 +187,22 @@ export const createUser = (user: any) => async (dispatch: Dispatch<any>) => { dispatch(setUserModalLoading(false)); }; +export const fetchRecoveryLink = (id: string) => async (dispatch: Dispatch<any>) => { + try { + const { data } = await performApiCall({ + path: `/users/${id}/recovery`, + method: 'GET', + }); + + dispatch({ + type: UserActionTypes.FETCH_RECOVERY_LINK, + payload: transformRecoveryLink(data), + }); + } catch (err) { + console.error(err); + } +}; + export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => { dispatch(setUserModalLoading(true)); diff --git a/frontend/src/services/users/redux/reducers.ts b/frontend/src/services/users/redux/reducers.ts index 2d02771d..d2372f58 100644 --- a/frontend/src/services/users/redux/reducers.ts +++ b/frontend/src/services/users/redux/reducers.ts @@ -19,6 +19,11 @@ const usersReducer = (state: any = initialUsersState, action: any) => { ...state, userModalLoading: action.payload, }; + case UserActionTypes.FETCH_RECOVERY_LINK: + return { + ...state, + recoveryLink: action.payload, + }; case UserActionTypes.SET_USERS_LOADING: return { ...state, diff --git a/frontend/src/services/users/redux/selectors.ts b/frontend/src/services/users/redux/selectors.ts index 44dea21a..af00389f 100644 --- a/frontend/src/services/users/redux/selectors.ts +++ b/frontend/src/services/users/redux/selectors.ts @@ -4,3 +4,4 @@ export const getUsers = (state: State) => state.users.users; export const getUserById = (state: State) => state.users.user; export const getUserModalLoading = (state: State) => state.users.userModalLoading; export const getUserslLoading = (state: State) => state.users.usersLoading; +export const getRecoveryLink = (state: State) => state.users.recoveryLink; diff --git a/frontend/src/services/users/redux/types.ts b/frontend/src/services/users/redux/types.ts index c1954ead..4fd9f0be 100644 --- a/frontend/src/services/users/redux/types.ts +++ b/frontend/src/services/users/redux/types.ts @@ -10,6 +10,7 @@ export interface UsersState { currentUser: CurrentUserState; users: User[]; user: User; + recoveryLink: string; userModalLoading: boolean; usersLoading: boolean; } diff --git a/frontend/src/services/users/transformations.ts b/frontend/src/services/users/transformations.ts index 21e7db46..545c9475 100644 --- a/frontend/src/services/users/transformations.ts +++ b/frontend/src/services/users/transformations.ts @@ -91,3 +91,7 @@ export const transformBatchResponse = (response: any): any => { failed: response.failed, }; }; + +export const transformRecoveryLink = (response: any): string => { + return response.recovery_link; +}; -- GitLab