Skip to content

Commit

Permalink
Merge pull request #264 from tnc-ca-geo/feature/234-reset_password
Browse files Browse the repository at this point in the history
Adding reset password option for administrators
  • Loading branch information
nathanielrindlaub authored Jan 10, 2025
2 parents 12d31d7 + d002b49 commit 9def193
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 40 deletions.
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', {
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>
<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

0 comments on commit 9def193

Please sign in to comment.