Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding reset password option for administrators #264

Merged
merged 13 commits into from
Jan 10, 2025
15 changes: 13 additions & 2 deletions src/api/buildQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ const queries = {
`,
variables: {
input: {
filters: filters
filters: filters,
},
},
};
Expand Down Expand Up @@ -668,7 +668,7 @@ const queries = {
`,
variables: { input: input },
}),

createImageTag: (input) => ({
template: `
mutation CreateImageTag($input: CreateImageTagInput!) {
Expand Down Expand Up @@ -923,6 +923,17 @@ const queries = {
`,
variables: { input },
}),

resendTempPassword: (input) => ({
template: `
mutation resendTempPassword($input: ResendTempPasswordInput!){
resendTempPassword(input: $input) {
isOk
}
}
`,
variables: { input },
}),
};

export default queries;
48 changes: 38 additions & 10 deletions src/features/auth/LoginForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -31,6 +32,8 @@ const Subheader = styled('div', {
paddingTop: '$3',
maxWidth: 700,
margin: '0 auto',
paddingRight: '$4',
paddingLeft: '$4',
a: {
textDecoration: 'none',
color: '$textDark',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -135,6 +137,13 @@ const StyledAuthenticator = styled(Authenticator, {
},
});

const StyledLoginCallout = styled('div', {
nathanielrindlaub marked this conversation as resolved.
Show resolved Hide resolved
maxWidth: 700,
margin: '0 auto',
paddingRight: '$4',
paddingLeft: '$4',
});

const StyledButton = styled(Button, {
fontSize: '$3',
fontWeight: '$2',
Expand Down Expand Up @@ -164,11 +173,30 @@ const LoginForm = () => {
<LoginScreen>
<Header css={{ '@bp3': { fontSize: '64px' } }}>Welcome back</Header>
<Subheader>{helperText[route] || userName || ''}</Subheader>
<StyledAuthenticator
loginMechanisms={['email']}
hideDefault={true}
hideSignUp={true}
/>
<StyledAuthenticator loginMechanisms={['email']} hideDefault={true} hideSignUp={true} />
{route === 'resetPassword' && (
<StyledLoginCallout>
<Callout type="info" title="Note about temporary passwords">
<p>
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.
</p>
<p>
{' '}
Instead, you must reach out to one of your Project Managers and have them{' '}
<a
href="https://docs.animl.camera/fundamentals/user-management#re-sending-users-temporary-passwords"
target="_blank"
rel="noreferrer"
>
send you a new temporary password
</a>
.
</p>
</Callout>
</StyledLoginCallout>
)}
{route === 'confirmResetPassword' && (
<StyledButton onClick={toSignIn}>Return to Sign In</StyledButton>
)}
Expand Down
90 changes: 71 additions & 19 deletions src/features/projects/ManageUsersTable.jsx
Original file line number Diff line number Diff line change
@@ -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());
Expand All @@ -24,6 +43,11 @@ const ManageUsersTable = () => {
[users],
);

const handleResendTempPassword = (email) => {
dispatch(resendTempPassword({ username: email }));
setUsersClicked([...usersClicked, email]);
};

return (
<Content>
{isLoading && (
Expand All @@ -41,23 +65,51 @@ const ManageUsersTable = () => {
</tr>
</thead>
<tbody>
{userSorted.map(({ email, roles }) => (
{userSorted.map(({ email, roles, status }) => (
<TableRow key={email}>
<TableCell>{email}</TableCell>
<TableCell>{roles.join(', ')}</TableCell>
<TableCell>
<Tooltip>
<TooltipTrigger asChild>
<IconButton variant="ghost" size="large" onClick={() => dispatch(editUser(email))}>
<Pencil1Icon />
</IconButton>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
Edit user roles
<TooltipArrow />
</TooltipContent>
</Tooltip>
</TableCell>
{hasRole(currentUserRoles, MANAGE_USERS_ROLES) && (
<TableCell>
nathanielrindlaub marked this conversation as resolved.
Show resolved Hide resolved
<Tooltip>
<TooltipTrigger asChild>
<IconButton
variant="ghost"
size="med"
onClick={() => dispatch(editUser(email))}
>
<Pencil1Icon />
</IconButton>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
Edit user roles
<TooltipArrow />
</TooltipContent>
</Tooltip>
{status === 'FORCE_CHANGE_PASSWORD' && (
<Tooltip>
<TooltipTrigger asChild>
<IconButton
variant="ghost"
size="med"
onClick={() => handleResendTempPassword(email)}
disabled={usersClicked.includes(email) && !hasErrors}
>
{usersClicked.includes(email) && !hasErrors ? (
<CheckIcon />
) : (
<ResetIcon />
)}
</IconButton>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
Resend Temporary Password
<TooltipArrow />
</TooltipContent>
</Tooltip>
)}
</TableCell>
)}
</TableRow>
))}
</tbody>
Expand Down
61 changes: 52 additions & 9 deletions src/features/projects/usersSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -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;
},
Expand All @@ -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,
Expand All @@ -150,7 +168,7 @@ export const fetchUsers = () => {
dispatch(fetchUsersSuccess({ users: res.users.users }));
}
} catch (err) {
dispatch(fetchUsersError(err));
dispatch(fetchUsersFailure(err));
}
};
};
Expand All @@ -175,7 +193,7 @@ export const updateUser = (values) => {
dispatch(updateUserSuccess(values));
}
} catch (err) {
dispatch(updateUserError(err));
dispatch(updateUserFailure(err));
}
};
};
Expand All @@ -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));
}
};
};
Expand Down
Loading