diff --git a/src/api/buildQuery.js b/src/api/buildQuery.js index 8c7f48f..68cc9ce 100644 --- a/src/api/buildQuery.js +++ b/src/api/buildQuery.js @@ -273,7 +273,7 @@ const queries = { `, variables: { input: { - filters: filters + filters: filters, }, }, }; @@ -668,7 +668,7 @@ const queries = { `, variables: { input: input }, }), - + createImageTag: (input) => ({ template: ` mutation CreateImageTag($input: CreateImageTagInput!) { @@ -923,6 +923,17 @@ const queries = { `, variables: { input }, }), + + resendTempPassword: (input) => ({ + template: ` + mutation resendTempPassword($input: ResendTempPasswordInput!){ + resendTempPassword(input: $input) { + isOk + } + } + `, + variables: { input }, + }), }; export default queries; diff --git a/src/features/auth/LoginForm.jsx b/src/features/auth/LoginForm.jsx index 54d4281..98c966c 100644 --- a/src/features/auth/LoginForm.jsx +++ b/src/features/auth/LoginForm.jsx @@ -7,6 +7,7 @@ import Button from '../../components/Button.jsx'; import '@aws-amplify/ui-react/styles.css'; import { useSelector } from 'react-redux'; import { selectUserUsername } from './authSlice.js'; +import Callout from '../../components/Callout.jsx'; const LoginScreen = styled('div', { display: 'flex', @@ -31,6 +32,8 @@ const Subheader = styled('div', { paddingTop: '$3', maxWidth: 700, margin: '0 auto', + paddingRight: '$4', + paddingLeft: '$4', a: { textDecoration: 'none', color: '$textDark', @@ -95,12 +98,11 @@ const StyledAuthenticator = styled(Authenticator, { }, }, - '&[data-amplify-authenticator] [data-amplify-authenticator-confirmresetpassword]': - { - '.amplify-heading': { - display: 'none', - }, + '&[data-amplify-authenticator] [data-amplify-authenticator-confirmresetpassword]': { + '.amplify-heading': { + display: 'none', }, + }, '.amplify-input': { display: 'inherit', @@ -135,6 +137,13 @@ const StyledAuthenticator = styled(Authenticator, { }, }); +const StyledLoginCallout = styled('div', { + maxWidth: 700, + margin: '0 auto', + paddingRight: '$4', + paddingLeft: '$4', +}); + const StyledButton = styled(Button, { fontSize: '$3', fontWeight: '$2', @@ -164,11 +173,30 @@ const LoginForm = () => {
Welcome back
{helperText[route] || userName || ''} - + + {route === 'resetPassword' && ( + + +

+ If you never logged into Animl and didn{"'"}t reset the temporary password that was + sent in your invitation email before it expired, we are unable to deliver password + reset emails via the form above. +

+

+ {' '} + Instead, you must reach out to one of your Project Managers and have them{' '} + + send you a new temporary password + + . +

+
+
+ )} {route === 'confirmResetPassword' && ( Return to Sign In )} diff --git a/src/features/projects/ManageUsersTable.jsx b/src/features/projects/ManageUsersTable.jsx index 7ddddfa..8c7303f 100644 --- a/src/features/projects/ManageUsersTable.jsx +++ b/src/features/projects/ManageUsersTable.jsx @@ -1,19 +1,38 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { styled } from '@stitches/react'; -import { Pencil1Icon } from '@radix-ui/react-icons'; - +import { CheckIcon, Pencil1Icon, ResetIcon } from '@radix-ui/react-icons'; import Button from '../../components/Button'; import IconButton from '../../components/IconButton.jsx'; -import { Tooltip, TooltipContent, TooltipArrow, TooltipTrigger } from '../../components/Tooltip.jsx'; +import { + Tooltip, + TooltipContent, + TooltipArrow, + TooltipTrigger, +} from '../../components/Tooltip.jsx'; import { ButtonRow } from '../../components/Form'; import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; -import { addUser, editUser, fetchUsers, selectUsers, selectUsersLoading } from './usersSlice.js'; +import { selectUserCurrentRoles } from '../auth/authSlice'; +import { + addUser, + editUser, + fetchUsers, + selectUsers, + selectUsersLoading, + selectManageUserErrors, + resendTempPassword, +} from './usersSlice.js'; +import { hasRole, MANAGE_USERS_ROLES } from '../auth/roles'; const ManageUsersTable = () => { const dispatch = useDispatch(); + const currentUserRoles = useSelector(selectUserCurrentRoles); const users = useSelector(selectUsers); const isLoading = useSelector(selectUsersLoading); + const errors = useSelector(selectManageUserErrors); + const hasErrors = !isLoading && errors; + + const [usersClicked, setUsersClicked] = useState([]); useEffect(() => { dispatch(fetchUsers()); @@ -24,6 +43,11 @@ const ManageUsersTable = () => { [users], ); + const handleResendTempPassword = (email) => { + dispatch(resendTempPassword({ username: email })); + setUsersClicked([...usersClicked, email]); + }; + return ( {isLoading && ( @@ -41,23 +65,51 @@ const ManageUsersTable = () => { - {userSorted.map(({ email, roles }) => ( + {userSorted.map(({ email, roles, status }) => ( {email} {roles.join(', ')} - - - - dispatch(editUser(email))}> - - - - - Edit user roles - - - - + {hasRole(currentUserRoles, MANAGE_USERS_ROLES) && ( + + + + dispatch(editUser(email))} + > + + + + + Edit user roles + + + + {status === 'FORCE_CHANGE_PASSWORD' && ( + + + handleResendTempPassword(email)} + disabled={usersClicked.includes(email) && !hasErrors} + > + {usersClicked.includes(email) && !hasErrors ? ( + + ) : ( + + )} + + + + Resend Temporary Password + + + + )} + + )} ))} diff --git a/src/features/projects/usersSlice.js b/src/features/projects/usersSlice.js index 3f995a9..451f027 100644 --- a/src/features/projects/usersSlice.js +++ b/src/features/projects/usersSlice.js @@ -30,7 +30,7 @@ export const usersSlice = createSlice({ state.users = payload.users; }, - fetchUsersError: (state, { payload }) => { + fetchUsersFailure: (state, { payload }) => { const ls = { isLoading: false, operation: null, errors: payload }; state.loadingStates.users = ls; }, @@ -62,7 +62,7 @@ export const usersSlice = createSlice({ state.users = updatedUsers; }, - updateUserError: (state, { payload }) => { + updateUserFailure: (state, { payload }) => { const ls = { isLoading: false, operation: null, errors: payload }; state.loadingStates.users = ls; }, @@ -90,7 +90,22 @@ export const usersSlice = createSlice({ ]; }, - addUserError: (state, { payload }) => { + addUserFailure: (state, { payload }) => { + const ls = { isLoading: false, operation: null, errors: payload }; + state.loadingStates.users = ls; + }, + + resendTempPasswordStart: (state) => { + const ls = { isLoading: true, operation: 'resendTempPassword', errors: null }; + state.loadingStates.users = ls; + }, + + resendTempPasswordSuccess: (state) => { + const ls = { isLoading: false, operation: null, errors: null }; + state.loadingStates.users = ls; + }, + + resendTempPasswordFailure: (state, { payload }) => { const ls = { isLoading: false, operation: null, errors: payload }; state.loadingStates.users = ls; }, @@ -117,15 +132,18 @@ export const usersSlice = createSlice({ export const { fetchUsersStart, fetchUsersSuccess, - fetchUsersError, + fetchUsersFailure, editUser, updateUserSuccess, updateUserStart, - updateUserError, + updateUserFailure, addUser, addUserSuccess, addUserStart, - addUserError, + addUserFailure, + resendTempPasswordStart, + resendTempPasswordSuccess, + resendTempPasswordFailure, cancel, clearUsers, dismissManageUsersError, @@ -150,7 +168,7 @@ export const fetchUsers = () => { dispatch(fetchUsersSuccess({ users: res.users.users })); } } catch (err) { - dispatch(fetchUsersError(err)); + dispatch(fetchUsersFailure(err)); } }; }; @@ -175,7 +193,7 @@ export const updateUser = (values) => { dispatch(updateUserSuccess(values)); } } catch (err) { - dispatch(updateUserError(err)); + dispatch(updateUserFailure(err)); } }; }; @@ -200,7 +218,32 @@ export const createUser = (values) => { dispatch(addUserSuccess(values)); } } catch (err) { - dispatch(addUserError(err)); + dispatch(addUserFailure(err)); + } + }; +}; + +export const resendTempPassword = (values) => { + return async (dispatch, getState) => { + try { + dispatch(resendTempPasswordStart()); + + const currentUser = await Auth.currentAuthenticatedUser(); + const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); + const projects = getState().projects.projects; + const selectedProj = projects.find((proj) => proj.selected); + const projId = selectedProj._id; + + if (token && selectedProj) { + await call({ + projId, + request: 'resendTempPassword', + input: values, + }); + dispatch(resendTempPasswordSuccess()); + } + } catch (err) { + dispatch(resendTempPasswordFailure(err)); } }; };