From d61462e764956896f87521ea89b6d98996ec9753 Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 26 Jul 2021 12:33:18 +0200 Subject: [PATCH 1/9] added UI for change password form in user profile --- .../ui/src/components/Profile.tsx | 309 +++++++++++++++++- 1 file changed, 307 insertions(+), 2 deletions(-) diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index 51cf6d87c..f75154d57 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -1,8 +1,26 @@ -import React from 'react'; -import { useAppSelector } from '../hooks/redux'; +import React, { useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { useAppSelector, useAppDispatch } from '../hooks/redux'; +import { selectPasswordReset } from '../redux/auth/selectors'; +import { toggleSnackbar } from '../redux/auth/slice'; +import { resetPassword } from '../redux/auth/thunk'; + import { selectUser } from '../redux/auth/selectors'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; import { Theme, makeStyles } from '@material-ui/core/styles'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; +import OutlinedInput from '@material-ui/core/OutlinedInput'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import IconButton from '@material-ui/core/IconButton'; +import Visibility from '@material-ui/icons/Visibility'; +import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; import { Chip, Tooltip } from '@material-ui/core'; const styles = makeStyles((theme: Theme) => ({ @@ -24,6 +42,292 @@ const styles = makeStyles((theme: Theme) => ({ }, })); +const useStyles = makeStyles((theme: Theme) => ({ + checkboxRoot: { + display: 'block', + }, + required: { + color: theme.palette.error.main, + }, + inpputField: { + display: 'block', + width: '240px', + marginBottom: '10px', + }, + signInButton: { + marginTop: '10px', + marginBottom: '10px', + }, + checkboxLabel: { + fontSize: '14px', + }, + link: { + fontWeight: 'bold', + color: theme.palette.primary.main, + cursor: 'pointer', + }, + forgotPassword: { + fontWeight: 'normal', + color: theme.palette.primary.main, + cursor: 'pointer', + fontSize: 'small', + marginTop: '-8px', + display: 'flex', + justifyContent: 'flex-end', + }, + labelRequired: { + color: theme.palette.error.main, + }, + title: { + margin: '10px 0', + fontWeight:700, + }, + googleButton: { + // margin: '35px 0 0 0', + fontWeight: 400, + }, + formFlexContainer: { + display: 'flex', + gap: '80px', + justifyContent:'center', + marginTop:'30px', + }, +})); + +interface FormValues { + oldpassword: string; + password: string; + passwordConfirmation: string; +} + +interface ChangePasswordFormInProfileProps { + disabled?: boolean; +} + +export function ChangePasswordFormInProfile({ + disabled, +}: ChangePasswordFormInProfileProps): JSX.Element { + const classes = useStyles(); + const dispatch = useAppDispatch(); + const history = useHistory(); + + const passwordReset = useAppSelector(selectPasswordReset); + const [passwordVisible, setPasswordVisible] = useState(false); + const [ + passwordConfirmationVisible, + setPasswordConfirmationVisible, + ] = useState(false); + + // After successful password reset redirect user to landing page and show snackbar alert + useEffect(() => { + if (!passwordReset) return; + + history.push('/'); + dispatch( + toggleSnackbar({ + isOpen: true, + message: + 'Your password was changed successfully. You can now sign in using the new password', + }), + ); + }, [dispatch, history, passwordReset]); + + const validationSchema = Yup.object().shape({ + oldpassword: Yup.string().required('This field is required'), + password: Yup.string().required('This field is required'), + passwordConfirmation: Yup.string().test( + 'passwords-match', + 'Passwords must match', + function (value) { + return this.parent.password === value; + }, + ), + }); + + const formik = useFormik({ + initialValues: { + oldpassword: '', + password: '', + passwordConfirmation: '', + }, + validationSchema, + onSubmit: (values) => { + console.log(values) + // dispatch( + // resetPassword({ + // newPassword: values.password, + // }), + // ); + }, + }); + + useEffect(() => { + return () => { + formik.resetForm(); + }; + // eslint-disable-next-line + }, []); + + return ( + <> +
+
+
+ + Change your password + + + Old Password + + + setPasswordVisible( + !passwordVisible, + ) + } + edge="end" + > + {passwordVisible ? ( + + ) : ( + + )} + + + } + label="Old password" + /> + + {formik.touched.oldpassword && + formik.errors.oldpassword} + + + + + Password + + + setPasswordVisible( + !passwordVisible, + ) + } + edge="end" + > + {passwordVisible ? ( + + ) : ( + + )} + + + } + label="New password" + /> + + {formik.touched.password && + formik.errors.password} + + + + + + Repeat password + + + + setPasswordConfirmationVisible( + !passwordConfirmationVisible, + ) + } + edge="end" + > + {passwordConfirmationVisible ? ( + + ) : ( + + )} + + + } + label="Repeat new password" + /> + + {formik.touched.passwordConfirmation && + formik.errors.passwordConfirmation} + + +
+
+ + +
+ + ); +} + export default function Profile(): JSX.Element { const classes = styles(); @@ -83,6 +387,7 @@ export default function Profile(): JSX.Element { throw Error(`Unknown role ${role}`); } })} + ) : ( <> From 3c31a404d266ad4abaaec9c01d284ad358d12beb Mon Sep 17 00:00:00 2001 From: Sergio Date: Tue, 27 Jul 2021 14:25:41 +0200 Subject: [PATCH 2/9] added password validation --- .../ui/src/components/Profile.tsx | 23 ++++++++++++++----- .../landing-page/ChangePasswordForm.tsx | 11 ++++++++- .../components/landing-page/SignUpForm.tsx | 17 +++++++++++--- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index f75154d57..5889323b9 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -80,7 +80,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, title: { margin: '10px 0', - fontWeight:700, + fontWeight: 700, }, googleButton: { // margin: '35px 0 0 0', @@ -89,8 +89,8 @@ const useStyles = makeStyles((theme: Theme) => ({ formFlexContainer: { display: 'flex', gap: '80px', - justifyContent:'center', - marginTop:'30px', + justifyContent: 'center', + marginTop: '30px', }, })); @@ -132,9 +132,18 @@ export function ChangePasswordFormInProfile({ ); }, [dispatch, history, passwordReset]); + const lowercaseRegex = /(?=.*[a-z])/; + const uppercaseRegex = /(?=.*[A-Z])/; + const numericRegex = /(?=.*[0-9])/; + const validationSchema = Yup.object().shape({ oldpassword: Yup.string().required('This field is required'), - password: Yup.string().required('This field is required'), + password: Yup.string() + .matches(lowercaseRegex, 'one lowercase required!') + .matches(uppercaseRegex, 'one uppercase required!') + .matches(numericRegex, 'one number required!') + .min(8, 'Minimum 8 characters required!') + .required('Required!'), passwordConfirmation: Yup.string().test( 'passwords-match', 'Passwords must match', @@ -152,7 +161,7 @@ export function ChangePasswordFormInProfile({ }, validationSchema, onSubmit: (values) => { - console.log(values) + console.log(values); // dispatch( // resetPassword({ // newPassword: values.password, @@ -185,7 +194,9 @@ export function ChangePasswordFormInProfile({ Boolean(formik.errors.oldpassword) } > - Old Password + + Old Password + Date: Tue, 27 Jul 2021 16:03:19 +0200 Subject: [PATCH 3/9] added password validation 2 --- verification/curator-service/ui/src/components/Profile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index 3a02286d5..3549f8919 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -400,7 +400,7 @@ export default function Profile(): JSX.Element { throw Error(`Unknown role ${role}`); } })} - {user.googleID && } + {user.googleID === "" && } ) : ( <> From 660f691ffaf1721d77777057821a998aa8df2c08 Mon Sep 17 00:00:00 2001 From: Sergio Date: Wed, 28 Jul 2021 11:55:49 +0200 Subject: [PATCH 4/9] fixed after review --- .../ui/src/components/Profile.tsx | 64 ++----------------- 1 file changed, 7 insertions(+), 57 deletions(-) diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index 3549f8919..f33822918 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -1,10 +1,8 @@ import React, { useState, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; -import { useAppSelector, useAppDispatch } from '../hooks/redux'; -import { selectPasswordReset } from '../redux/auth/selectors'; -import { toggleSnackbar } from '../redux/auth/slice'; -import { resetPassword } from '../redux/auth/thunk'; +import { useAppSelector } from '../hooks/redux'; + import { selectUser } from '../redux/auth/selectors'; import { useFormik } from 'formik'; @@ -43,12 +41,6 @@ const styles = makeStyles((theme: Theme) => ({ })); const useStyles = makeStyles((theme: Theme) => ({ - checkboxRoot: { - display: 'block', - }, - required: { - color: theme.palette.error.main, - }, inpputField: { display: 'block', width: '240px', @@ -58,34 +50,10 @@ const useStyles = makeStyles((theme: Theme) => ({ marginTop: '10px', marginBottom: '10px', }, - checkboxLabel: { - fontSize: '14px', - }, - link: { - fontWeight: 'bold', - color: theme.palette.primary.main, - cursor: 'pointer', - }, - forgotPassword: { - fontWeight: 'normal', - color: theme.palette.primary.main, - cursor: 'pointer', - fontSize: 'small', - marginTop: '-8px', - display: 'flex', - justifyContent: 'flex-end', - }, - labelRequired: { - color: theme.palette.error.main, - }, title: { margin: '10px 0', fontWeight: 700, }, - googleButton: { - // margin: '35px 0 0 0', - fontWeight: 400, - }, formFlexContainer: { display: 'flex', gap: '80px', @@ -108,30 +76,14 @@ export function ChangePasswordFormInProfile({ disabled, }: ChangePasswordFormInProfileProps): JSX.Element { const classes = useStyles(); - const dispatch = useAppDispatch(); - const history = useHistory(); - const passwordReset = useAppSelector(selectPasswordReset); + const [oldPasswordVisible, setOldPasswordVisible] = useState(false); const [passwordVisible, setPasswordVisible] = useState(false); const [ passwordConfirmationVisible, setPasswordConfirmationVisible, ] = useState(false); - // After successful password reset redirect user to landing page and show snackbar alert - useEffect(() => { - if (!passwordReset) return; - - history.push('/'); - dispatch( - toggleSnackbar({ - isOpen: true, - message: - 'Your password was changed successfully. You can now sign in using the new password', - }), - ); - }, [dispatch, history, passwordReset]); - const lowercaseRegex = /(?=.*[a-z])/; const uppercaseRegex = /(?=.*[A-Z])/; const numericRegex = /(?=.*[0-9])/; @@ -200,7 +152,7 @@ export function ChangePasswordFormInProfile({ - setPasswordVisible( - !passwordVisible, + setOldPasswordVisible( + !oldPasswordVisible, ) } edge="end" @@ -344,8 +296,6 @@ export default function Profile(): JSX.Element { const user = useAppSelector(selectUser); - console.log(user) - return ( <> {user ? ( @@ -400,7 +350,7 @@ export default function Profile(): JSX.Element { throw Error(`Unknown role ${role}`); } })} - {user.googleID === "" && } + {user.googleID && } ) : ( <> From 883b4b6317c3c6620c1124390a6133bab1beb616 Mon Sep 17 00:00:00 2001 From: Sergio Date: Wed, 28 Jul 2021 12:12:38 +0200 Subject: [PATCH 5/9] fixed googleID --- verification/curator-service/ui/src/components/Profile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index f33822918..80e125b14 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -350,7 +350,7 @@ export default function Profile(): JSX.Element { throw Error(`Unknown role ${role}`); } })} - {user.googleID && } + {!user.googleID && } ) : ( <> From 61e5df0d23ae3822eb3ebb25dc1feccb06d966ed Mon Sep 17 00:00:00 2001 From: Sergio Date: Wed, 28 Jul 2021 14:19:35 +0200 Subject: [PATCH 6/9] fixed failing jest tests --- .../curator-service/ui/src/components/Profile.tsx | 2 -- .../ui/src/components/landing-page/LandingPage.test.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index 80e125b14..cbd5b7897 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -1,6 +1,4 @@ import React, { useState, useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; - import { useAppSelector } from '../hooks/redux'; diff --git a/verification/curator-service/ui/src/components/landing-page/LandingPage.test.tsx b/verification/curator-service/ui/src/components/landing-page/LandingPage.test.tsx index 5de980831..75400b0af 100644 --- a/verification/curator-service/ui/src/components/landing-page/LandingPage.test.tsx +++ b/verification/curator-service/ui/src/components/landing-page/LandingPage.test.tsx @@ -264,7 +264,7 @@ describe('', () => { }); }); - test('displays verification errors when password is empty', async () => { + test('displays verification errors when password in SignUp form is empty', async () => { render( true} @@ -283,7 +283,7 @@ describe('', () => { await waitFor(() => { expect( - screen.getByText(/This field is required/i), + screen.getByText(/Required!/i), ).toBeInTheDocument(); expect( screen.getByText(/Passwords must match/i), @@ -365,7 +365,7 @@ describe('', () => { expect(screen.getByText('Choose a new password')).toBeInTheDocument(); }); - test('displays verification errors when password is empty', async () => { + test('displays verification errors when password in ChangePassword form is empty', async () => { render( @@ -377,7 +377,7 @@ describe('', () => { await waitFor(() => { expect( - screen.getByText('This field is required'), + screen.getByText('Required!'), ).toBeInTheDocument(); }); }); From 0432a2e1bf65255a44474e38640ef893efeb02cf Mon Sep 17 00:00:00 2001 From: Maciej Zarzeczny Date: Thu, 29 Jul 2021 12:26:39 +0200 Subject: [PATCH 7/9] Added API endpoint adn Redux logic for updating user password --- .../api/src/controllers/auth.ts | 47 ++- .../ui/src/components/LinelistTable.tsx | 2 +- .../ui/src/components/Profile.tsx | 372 ++++++++++-------- .../ui/src/components/SnackbarAlert/index.tsx | 6 +- .../components/landing-page/LandingPage.tsx | 2 +- .../ui/src/redux/auth/selectors.ts | 2 + .../ui/src/redux/auth/slice.ts | 37 +- .../ui/src/redux/auth/thunk.ts | 20 + 8 files changed, 308 insertions(+), 180 deletions(-) diff --git a/verification/curator-service/api/src/controllers/auth.ts b/verification/curator-service/api/src/controllers/auth.ts index cd82087cb..b39e0f7d4 100644 --- a/verification/curator-service/api/src/controllers/auth.ts +++ b/verification/curator-service/api/src/controllers/auth.ts @@ -208,6 +208,51 @@ export class AuthController { }, ); + /** + * Update user's password + */ + this.router.post( + '/change-password', + mustBeAuthenticated, + async (req: Request, res: Response) => { + const oldPassword = req.body.oldPassword as string; + const newPassword = req.body.newPassword as string; + const user = req.user as UserDocument; + + if (!user) { + return res.sendStatus(403); + } + + try { + const currentUser = await User.findById(user.id); + if (!currentUser) { + return res.sendStatus(403); + } + + const isValidPassword = await currentUser.isValidPassword( + oldPassword, + ); + + if (!isValidPassword) { + return res + .status(403) + .json({ message: 'Old password is incorrect' }); + } + + const hashedPassword = await bcrypt.hash(newPassword, 10); + await User.findByIdAndUpdate(user.id, { + password: hashedPassword, + }); + + return res + .status(200) + .json({ message: 'Password changed successfully' }); + } catch (error) { + return res.status(500).json(error); + } + }, + ); + /** * Generate reset password link */ @@ -414,7 +459,7 @@ export class AuthController { // Cf. https://github.com/jaredhanson/passport/issues/6#issuecomment-4857287 // This doesn't work however for now as per, if you hit this bug, you have to manually clear the cookies. // Cf https://github.com/jaredhanson/passport/issues/776 - done(null, user || undefined); + done(null, user?.publicFields() || undefined); return; }) .catch((e) => { diff --git a/verification/curator-service/ui/src/components/LinelistTable.tsx b/verification/curator-service/ui/src/components/LinelistTable.tsx index b44fb4baa..22992913b 100644 --- a/verification/curator-service/ui/src/components/LinelistTable.tsx +++ b/verification/curator-service/ui/src/components/LinelistTable.tsx @@ -649,7 +649,7 @@ export function DownloadButton({ <> diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index 80e125b14..24da63f9e 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -1,10 +1,16 @@ import React, { useState, useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; -import { useAppSelector } from '../hooks/redux'; +import { useAppSelector, useAppDispatch } from '../hooks/redux'; +import { changePassword } from '../redux/auth/thunk'; +import { + selectUser, + selectError, + selectChangePasswordResponse, + selectIsLoading, +} from '../redux/auth/selectors'; +import { resetError, resetChangePasswordResponse } from '../redux/auth/slice'; -import { selectUser } from '../redux/auth/selectors'; import { useFormik } from 'formik'; import * as Yup from 'yup'; @@ -20,6 +26,9 @@ import VisibilityOff from '@material-ui/icons/VisibilityOff'; import Button from '@material-ui/core/Button'; import Typography from '@material-ui/core/Typography'; import { Chip, Tooltip } from '@material-ui/core'; +import Alert from '@material-ui/lab/Alert'; +import LinearProgress from '@material-ui/core/LinearProgress'; +import { SnackbarAlert } from './SnackbarAlert'; const styles = makeStyles((theme: Theme) => ({ root: { @@ -48,7 +57,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, signInButton: { marginTop: '10px', - marginBottom: '10px', + marginBottom: '20px', }, title: { margin: '10px 0', @@ -56,45 +65,51 @@ const useStyles = makeStyles((theme: Theme) => ({ }, formFlexContainer: { display: 'flex', - gap: '80px', - justifyContent: 'center', + flexDirection: 'column', + alignItems: 'center', marginTop: '30px', }, + linearProgress: { + width: '240px', + }, })); interface FormValues { - oldpassword: string; + oldPassword: string; password: string; passwordConfirmation: string; } -interface ChangePasswordFormInProfileProps { - disabled?: boolean; -} - -export function ChangePasswordFormInProfile({ - disabled, -}: ChangePasswordFormInProfileProps): JSX.Element { +export function ChangePasswordFormInProfile(): JSX.Element { const classes = useStyles(); + const dispatch = useAppDispatch(); const [oldPasswordVisible, setOldPasswordVisible] = useState(false); const [passwordVisible, setPasswordVisible] = useState(false); - const [ - passwordConfirmationVisible, - setPasswordConfirmationVisible, - ] = useState(false); + const [passwordConfirmationVisible, setPasswordConfirmationVisible] = + useState(false); + const error = useAppSelector(selectError); + const changePasswordResponse = useAppSelector(selectChangePasswordResponse); + const isLoading = useAppSelector(selectIsLoading); const lowercaseRegex = /(?=.*[a-z])/; const uppercaseRegex = /(?=.*[A-Z])/; const numericRegex = /(?=.*[0-9])/; const validationSchema = Yup.object().shape({ - oldpassword: Yup.string().required('This field is required'), + oldPassword: Yup.string().required('This field is required'), password: Yup.string() .matches(lowercaseRegex, 'one lowercase required!') .matches(uppercaseRegex, 'one uppercase required!') .matches(numericRegex, 'one number required!') .min(8, 'Minimum 8 characters required!') + .test( + 'passwords-different', + "New password can't be the same as old password", + function (value) { + return this.parent.oldPassword !== value; + }, + ) .required('Required!'), passwordConfirmation: Yup.string().test( 'passwords-match', @@ -107,18 +122,14 @@ export function ChangePasswordFormInProfile({ const formik = useFormik({ initialValues: { - oldpassword: '', + oldPassword: '', password: '', passwordConfirmation: '', }, validationSchema, onSubmit: (values) => { - console.log(values); - // dispatch( - // resetPassword({ - // newPassword: values.password, - // }), - // ); + const { oldPassword, password } = values; + dispatch(changePassword({ oldPassword, newPassword: password })); }, }); @@ -129,155 +140,160 @@ export function ChangePasswordFormInProfile({ // eslint-disable-next-line }, []); + useEffect(() => { + if (!changePasswordResponse) return; + + formik.resetForm(); + //eslint-disable-next-line + }, [changePasswordResponse]); + return ( <> -
-
-
- - Change your password - - - - Old Password - - - - setOldPasswordVisible( - !oldPasswordVisible, - ) - } - edge="end" - > - {passwordVisible ? ( - - ) : ( - - )} - - - } - label="Old password" - /> - - {formik.touched.oldpassword && - formik.errors.oldpassword} - - + dispatch(resetChangePasswordResponse())} + type="success" + message={changePasswordResponse || ''} + /> - - Password - - - setPasswordVisible( - !passwordVisible, - ) - } - edge="end" - > - {passwordVisible ? ( - - ) : ( - - )} - - - } - label="New password" - /> - - {formik.touched.password && - formik.errors.password} - - + + + Change your password + + + Old Password + + + setOldPasswordVisible( + !oldPasswordVisible, + ) + } + edge="end" + > + {passwordVisible ? ( + + ) : ( + + )} + + + } + label="Old password" + /> + + {formik.touched.oldPassword && + formik.errors.oldPassword} + + - - - Repeat password - - - - setPasswordConfirmationVisible( - !passwordConfirmationVisible, - ) - } - edge="end" - > - {passwordConfirmationVisible ? ( - - ) : ( - - )} - - - } - label="Repeat new password" - /> - - {formik.touched.passwordConfirmation && - formik.errors.passwordConfirmation} - - -
-
+ + New password + + + setPasswordVisible(!passwordVisible) + } + edge="end" + > + {passwordVisible ? ( + + ) : ( + + )} + + + } + label="New password" + /> + + {formik.touched.password && formik.errors.password} + + + + + + Repeat new password + + + + setPasswordConfirmationVisible( + !passwordConfirmationVisible, + ) + } + edge="end" + > + {passwordConfirmationVisible ? ( + + ) : ( + + )} + + + } + label="Repeat new password" + /> + + {formik.touched.passwordConfirmation && + formik.errors.passwordConfirmation} + + + + {isLoading && ( + + )} + + {error && ( + dispatch(resetError())} + > + {error} + + )} ); @@ -350,7 +382,7 @@ export default function Profile(): JSX.Element { throw Error(`Unknown role ${role}`); } })} - {!user.googleID && } + {!user.googleID && } ) : ( <> diff --git a/verification/curator-service/ui/src/components/SnackbarAlert/index.tsx b/verification/curator-service/ui/src/components/SnackbarAlert/index.tsx index d80c70d3c..47ae3052e 100644 --- a/verification/curator-service/ui/src/components/SnackbarAlert/index.tsx +++ b/verification/curator-service/ui/src/components/SnackbarAlert/index.tsx @@ -4,7 +4,7 @@ import MuiAlert, { AlertProps } from '@material-ui/lab/Alert'; interface SnackbarAlertProps { isOpen: boolean; - setIsOpen: (value: boolean) => void; + onClose: (value: boolean) => void; message: string; type: 'success' | 'warning' | 'info' | 'error'; durationMs?: number; @@ -16,7 +16,7 @@ const Alert = (props: AlertProps) => { export const SnackbarAlert: React.FC = ({ isOpen, - setIsOpen, + onClose, message, type, durationMs = 5000, @@ -25,7 +25,7 @@ export const SnackbarAlert: React.FC = ({ setIsOpen(false)} + onClose={() => onClose(false)} > {message} diff --git a/verification/curator-service/ui/src/components/landing-page/LandingPage.tsx b/verification/curator-service/ui/src/components/landing-page/LandingPage.tsx index 18ee38888..f6a6af956 100644 --- a/verification/curator-service/ui/src/components/landing-page/LandingPage.tsx +++ b/verification/curator-service/ui/src/components/landing-page/LandingPage.tsx @@ -248,7 +248,7 @@ const LandingPage = (): JSX.Element => { type="success" message={message} durationMs={5000} - setIsOpen={(open: boolean) => + onClose={(open: boolean) => dispatch(toggleSnackbar({ isOpen: open, message: '' })) } /> diff --git a/verification/curator-service/ui/src/redux/auth/selectors.ts b/verification/curator-service/ui/src/redux/auth/selectors.ts index bdab5b7cb..b456b51e6 100644 --- a/verification/curator-service/ui/src/redux/auth/selectors.ts +++ b/verification/curator-service/ui/src/redux/auth/selectors.ts @@ -9,5 +9,7 @@ export const selectForgotPasswordPopupOpen = (state: RootState) => state.auth.forgotPasswordPopupOpen; export const selectPasswordReset = (state: RootState) => state.auth.passwordReset; +export const selectChangePasswordResponse = (state: RootState) => + state.auth.changePasswordResponse; export const selectSnackbar = (state: RootState) => state.auth.snackbar; diff --git a/verification/curator-service/ui/src/redux/auth/slice.ts b/verification/curator-service/ui/src/redux/auth/slice.ts index 88dcd8922..d9acab5c0 100644 --- a/verification/curator-service/ui/src/redux/auth/slice.ts +++ b/verification/curator-service/ui/src/redux/auth/slice.ts @@ -7,6 +7,7 @@ import { requestResetPasswordLink, resetPassword, logout, + changePassword, } from './thunk'; interface SnackbarProps { @@ -20,6 +21,7 @@ interface AuthState { error: string | undefined; resetPasswordEmailSent: boolean; passwordReset: boolean; + changePasswordResponse: string | undefined; forgotPasswordPopupOpen: boolean; snackbar: SnackbarProps; } @@ -33,6 +35,7 @@ const initialState: AuthState = { error: undefined, resetPasswordEmailSent: false, passwordReset: false, + changePasswordResponse: undefined, forgotPasswordPopupOpen: false, snackbar: { isOpen: false, @@ -56,7 +59,10 @@ const authSlice = createSlice({ }, setResetPasswordEmailSent: (state, action: PayloadAction) => { state.resetPasswordEmailSent = action.payload; - } + }, + resetChangePasswordResponse: (state) => { + state.changePasswordResponse = undefined; + }, }, extraReducers: (builder) => { // SIGN IN @@ -136,7 +142,7 @@ const authSlice = createSlice({ state.isLoading = true; state.error = undefined; }); - builder.addCase(resetPassword.fulfilled, (state, action) => { + builder.addCase(resetPassword.fulfilled, (state) => { state.isLoading = false; state.passwordReset = true; }); @@ -152,11 +158,34 @@ const authSlice = createSlice({ builder.addCase(logout.fulfilled, (state) => { state.user = undefined; }); + + // CHANGE PASSWORD + builder.addCase(changePassword.pending, (state) => { + state.isLoading = true; + state.changePasswordResponse = undefined; + state.error = undefined; + }); + builder.addCase(changePassword.fulfilled, (state, action) => { + state.isLoading = false; + state.changePasswordResponse = action.payload; + }); + builder.addCase(changePassword.rejected, (state, action) => { + state.isLoading = false; + state.changePasswordResponse = undefined; + state.error = action.payload + ? action.payload + : action.error.message; + }); }, }); // Actions -export const { resetError, setForgotPasswordPopupOpen, toggleSnackbar, setResetPasswordEmailSent } = - authSlice.actions; +export const { + resetError, + setForgotPasswordPopupOpen, + toggleSnackbar, + setResetPasswordEmailSent, + resetChangePasswordResponse, +} = authSlice.actions; export default authSlice.reducer; diff --git a/verification/curator-service/ui/src/redux/auth/thunk.ts b/verification/curator-service/ui/src/redux/auth/thunk.ts index ae776ab4c..f31d94b09 100644 --- a/verification/curator-service/ui/src/redux/auth/thunk.ts +++ b/verification/curator-service/ui/src/redux/auth/thunk.ts @@ -88,3 +88,23 @@ export const resetPassword = createAsyncThunk< export const logout = createAsyncThunk('auth/logout', async () => { await axios.get('/auth/logout'); }); + +export const changePassword = createAsyncThunk< + string, + { oldPassword: string; newPassword: string }, + { rejectValue: string } +>('auth/updatePassword', async (data, { rejectWithValue }) => { + try { + const response = await axios.post('/auth/change-password', data); + + if (response.status !== 200) { + throw new Error('Something went wrong, please try again'); + } + + return response.data.message; + } catch (error) { + if (!error.response.data.message) throw error; + + return rejectWithValue(error.response.data.message); + } +}); From 0ffb4dc40586bdb2ed67a1a46f9f76be502a20ab Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 29 Jul 2021 14:13:33 +0200 Subject: [PATCH 8/9] made cypress password validation tests --- .../api/src/controllers/auth.ts | 4 +- .../curator-service/api/src/model/user.ts | 2 +- .../components/LandingPage.spec.ts | 40 ++++++++++++++++- .../components/ProfileTest.spec.ts | 44 ++++++++++++++++++- .../ui/cypress/support/commands.ts | 3 ++ .../ui/src/components/Profile.tsx | 1 + 6 files changed, 89 insertions(+), 5 deletions(-) diff --git a/verification/curator-service/api/src/controllers/auth.ts b/verification/curator-service/api/src/controllers/auth.ts index cd82087cb..a5239f745 100644 --- a/verification/curator-service/api/src/controllers/auth.ts +++ b/verification/curator-service/api/src/controllers/auth.ts @@ -373,12 +373,12 @@ export class AuthController { this.router.post( '/register', async (req: Request, res: Response): Promise => { + const removeGoogleID = req.body.removeGoogleID as boolean; const user = await User.create({ name: req.body.name, email: req.body.email, - // Necessary to pass mongoose validation. - googleID: '42', roles: req.body.roles, + ...(removeGoogleID !== true && { googleID: '42' }), }); req.login(user, (err: Error) => { if (!err) { diff --git a/verification/curator-service/api/src/model/user.ts b/verification/curator-service/api/src/model/user.ts index 241e88fbc..f890312bf 100644 --- a/verification/curator-service/api/src/model/user.ts +++ b/verification/curator-service/api/src/model/user.ts @@ -4,7 +4,7 @@ import bcrypt from 'bcrypt'; export const userRoles = ['admin', 'curator']; export type UserDocument = Document & { - googleID?: string; + googleID?: string | undefined; name?: string; email: string; password?: string; diff --git a/verification/curator-service/ui/cypress/integration/components/LandingPage.spec.ts b/verification/curator-service/ui/cypress/integration/components/LandingPage.spec.ts index 489ed9ab6..631820c90 100644 --- a/verification/curator-service/ui/cypress/integration/components/LandingPage.spec.ts +++ b/verification/curator-service/ui/cypress/integration/components/LandingPage.spec.ts @@ -33,6 +33,29 @@ describe('LandingPage', function () { cy.get('input').should('have.length', 6); }); + it('Checks if the password validation works well in the SignUp page', function () { + cy.visit('/'); + cy.contains('Welcome to G.h Data.'); + cy.contains('Sign up!').click(); + cy.contains('SignUp form'); + + cy.get('#password').type('tsgasdgasd'); + cy.get('button[data-testid="sign-up-button"]').click(); + cy.contains('one uppercase required!'); + cy.contains('Passwords must match'); + + cy.get('#password').focus().clear(); + cy.get('#password').type('tsgasdgGasd'); + cy.get('button[data-testid="sign-up-button"]').click(); + cy.contains('one number required!'); + + cy.get('#password').focus().clear(); + cy.get('#password').type('tT$5'); + cy.get('button[data-testid="sign-up-button"]').click(); + cy.contains('Minimum 8 characters required!'); + + }); + it('Validates emails', function () { cy.visit('/'); cy.contains('Welcome to G.h Data.'); @@ -78,15 +101,30 @@ describe('LandingPage', function () { cy.get('button[data-testid="change-password-button"]'); }); - it('Validates passwords in the change password page', function () { + it.only('Validates passwords in the change password page', function () { cy.visit('/'); cy.visit('/reset-password/sampletoken/tokenid'); cy.get('#password').type('tsgasdgasd'); cy.get('#passwordConfirmation').type('uu'); cy.get('button[data-testid="change-password-button"]').click(); + cy.contains('Passwords must match'); + cy.get('#password').type('tsgasdgasd'); + cy.get('button[data-testid="change-password-button"]').click(); + cy.contains('one uppercase required!'); cy.contains('Passwords must match'); + + cy.get('#password').focus().clear(); + cy.get('#password').type('tsgasdgGasd'); + cy.get('button[data-testid="change-password-button"]').click(); + cy.contains('one number required!'); + + cy.get('#password').focus().clear(); + cy.get('#password').type('tT$5'); + cy.get('button[data-testid="change-password-button"]').click(); + cy.contains('Minimum 8 characters required!'); + }); it('Homepage with logged out user', function () { diff --git a/verification/curator-service/ui/cypress/integration/components/ProfileTest.spec.ts b/verification/curator-service/ui/cypress/integration/components/ProfileTest.spec.ts index 65693e155..bae046292 100644 --- a/verification/curator-service/ui/cypress/integration/components/ProfileTest.spec.ts +++ b/verification/curator-service/ui/cypress/integration/components/ProfileTest.spec.ts @@ -6,11 +6,53 @@ describe('Profile', function () { email: 'alice@test.com', roles: ['curator'], }); - cy.visit('/') + cy.visit('/'); cy.visit('/profile'); cy.contains('Alice Smith'); cy.contains('alice@test.com'); cy.contains('curator'); }); + + // it('Checks if the change pass form is visible only for non-Google users', function () { + // cy.login({ + // name: 'Alice Smith', + // email: 'alice@test.com', + // roles: ['curator'], + // }); + // cy.visit('/') + // cy.visit('/profile'); + + // cy.contains('Alice Smith'); + // cy.contains('alice@test.com'); + // cy.contains('curator'); + // }); + + it('Checks if the change pass form validation works well', function () { + cy.login({ + name: 'Alice Smith', + email: 'alice@test.com', + roles: ['curator'], + removeGoogleID: true, + }); + cy.visit('/'); + cy.visit('/profile'); + + cy.contains('Change your password'); + + cy.get('#password').type('tsgasdgasd'); + cy.get('button[data-testid="change-password-button"]').click(); + cy.contains('one uppercase required!'); + cy.contains('Passwords must match'); + + cy.get('#password').focus().clear(); + cy.get('#password').type('tsgasdgGasd'); + cy.get('button[data-testid="change-password-button"]').click(); + cy.contains('one number required!'); + + cy.get('#password').focus().clear(); + cy.get('#password').type('tT$5'); + cy.get('button[data-testid="change-password-button"]').click(); + cy.contains('Minimum 8 characters required!'); + }); }); diff --git a/verification/curator-service/ui/cypress/support/commands.ts b/verification/curator-service/ui/cypress/support/commands.ts index 433f7d3e2..54f91575b 100644 --- a/verification/curator-service/ui/cypress/support/commands.ts +++ b/verification/curator-service/ui/cypress/support/commands.ts @@ -24,6 +24,7 @@ declare global { name: string; email: string; roles: string[]; + removeGoogleID?: boolean; }) => void; addSource: (name: string, url: string, uploads?: []) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -99,6 +100,7 @@ export function login(opts?: { name: string; email: string; roles: string[]; + removeGoogleID: boolean; }): void { cy.request({ method: 'POST', @@ -107,6 +109,7 @@ export function login(opts?: { name: opts?.name ?? 'superuser', email: opts?.email ?? 'superuser@test.com', roles: opts?.roles ?? ['admin', 'curator'], + removeGoogleID: opts?.removeGoogleID ?? undefined, }, }); } diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index cbd5b7897..8ecc448d9 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -353,6 +353,7 @@ export default function Profile(): JSX.Element { ) : ( <> )} +

{user?.googleID}

); } From 5b999bd6d43685890b57f3672e26921f6eee889c Mon Sep 17 00:00:00 2001 From: Sergio Date: Fri, 30 Jul 2021 13:56:35 +0200 Subject: [PATCH 9/9] added cypress test and api tests --- .../components/ProfileTest.spec.ts | 43 ++++++++---- .../ui/src/components/Profile.test.tsx | 69 ++++++++++++++++++- .../ui/src/components/Profile.tsx | 5 +- 3 files changed, 100 insertions(+), 17 deletions(-) diff --git a/verification/curator-service/ui/cypress/integration/components/ProfileTest.spec.ts b/verification/curator-service/ui/cypress/integration/components/ProfileTest.spec.ts index bae046292..83019af85 100644 --- a/verification/curator-service/ui/cypress/integration/components/ProfileTest.spec.ts +++ b/verification/curator-service/ui/cypress/integration/components/ProfileTest.spec.ts @@ -14,19 +14,17 @@ describe('Profile', function () { cy.contains('curator'); }); - // it('Checks if the change pass form is visible only for non-Google users', function () { - // cy.login({ - // name: 'Alice Smith', - // email: 'alice@test.com', - // roles: ['curator'], - // }); - // cy.visit('/') - // cy.visit('/profile'); - - // cy.contains('Alice Smith'); - // cy.contains('alice@test.com'); - // cy.contains('curator'); - // }); + it('Checks if the change pass form is visible only for non-Google users', function () { + cy.login({ + name: 'Alice Smith', + email: 'alice@test.com', + roles: ['curator'], + }); + cy.visit('/') + cy.visit('/profile'); + + cy.get('[data-testid="change-your-password-title"]').should('not.exist'); + }); it('Checks if the change pass form validation works well', function () { cy.login({ @@ -55,4 +53,23 @@ describe('Profile', function () { cy.get('button[data-testid="change-password-button"]').click(); cy.contains('Minimum 8 characters required!'); }); + + it.only('Checks if the validates the repeated password', function () { + cy.login({ + name: 'Alice Smith', + email: 'alice@test.com', + roles: ['curator'], + removeGoogleID: true, + }); + cy.visit('/'); + cy.visit('/profile'); + + cy.contains('Change your password'); + + cy.get('#password').type('tsgas%dFg9asd'); + cy.get('#passwordConfirmation').type('ts'); + cy.get('button[data-testid="change-password-button"]').click(); + cy.contains('This field is required'); + cy.contains('Passwords must match'); + }); }); diff --git a/verification/curator-service/ui/src/components/Profile.test.tsx b/verification/curator-service/ui/src/components/Profile.test.tsx index 512ee599e..31bbc8863 100644 --- a/verification/curator-service/ui/src/components/Profile.test.tsx +++ b/verification/curator-service/ui/src/components/Profile.test.tsx @@ -1,7 +1,16 @@ import React from 'react'; -import { render, screen } from './util/test-utils'; +import { render, screen, waitFor } from './util/test-utils'; +import userEvent from '@testing-library/user-event'; import Profile from './Profile'; import { RootState } from '../redux/store'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +const server = setupServer(); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); const initialLoggedInState: RootState = { app: { @@ -70,4 +79,62 @@ describe('', () => { screen.getByText(/Login required to view this page/i), ).toBeInTheDocument(); }); + + it('checks if the old password is right', async () => { + server.use( + rest.post('/auth/change-password', (req, res, ctx) => { + return res( + ctx.status(403), + ctx.json({ message: 'Old password is incorrect' }), + ); + }), + ); + render(, { initialState: noUserInfoState }); + + + userEvent.type(screen.getByLabelText('Old Password'), '1234567'); + userEvent.type(screen.getByLabelText('New password'), 'asdD?234'); + userEvent.type(screen.getByLabelText('Repeat new password'), 'asdD?234'); + + userEvent.click(screen.getByRole('button', { name: 'Change password' })); + + await waitFor( + () => { + expect( + screen.getByText(/Old password is incorrect/i), + ).toBeInTheDocument(); + }, + { timeout: 15000 }, + ); + }); + + + it('checks if the password was changed successfully', async () => { + server.use( + rest.post('/auth/change-password', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ message: 'Password changed successfull' }), + ); + }), + ); + render(, { initialState: noUserInfoState }); + + + userEvent.type(screen.getByLabelText('Old Password'), '1234567'); + userEvent.type(screen.getByLabelText('New password'), 'asdD?234'); + userEvent.type(screen.getByLabelText('Repeat new password'), 'asdD?234'); + + userEvent.click(screen.getByRole('button', { name: 'Change password' })); + + await waitFor( + () => { + expect( + screen.getByText(/Password changed successfull/i), + ).toBeInTheDocument(); + }, + { timeout: 15000 }, + ); + }); + }); diff --git a/verification/curator-service/ui/src/components/Profile.tsx b/verification/curator-service/ui/src/components/Profile.tsx index abf9b5506..4b0edaca3 100644 --- a/verification/curator-service/ui/src/components/Profile.tsx +++ b/verification/curator-service/ui/src/components/Profile.tsx @@ -144,7 +144,7 @@ export function ChangePasswordFormInProfile(): JSX.Element { formik.resetForm(); //eslint-disable-next-line - }, [changePasswordResponse]); + }, [changePasswordResponse]); return ( <> @@ -159,7 +159,7 @@ export function ChangePasswordFormInProfile(): JSX.Element { onSubmit={formik.handleSubmit} className={classes.formFlexContainer} > - + Change your password )} -

{user?.googleID}

); }