From 97d32c253cc65438c6c706746f99ac27088c865b Mon Sep 17 00:00:00 2001 From: Anastasiya Chuprey <82256209+NastiaChooo@users.noreply.github.com> Date: Thu, 3 Nov 2022 16:44:55 +0200 Subject: [PATCH] 152 UI password reset screen (#162) * AC/UI password reset screen * Email forgotten password reset link. * AC/added reset password functionality Co-authored-by: Vadim Zabolotniy --- application/managers/invitations.py | 2 +- application/managers/users.py | 8 +- .../auth/services/jwtService/jwtService.js | 8 + frontend/src/app/configs/routesConfig.js | 6 + frontend/src/app/constants/paths.js | 2 + .../forgot-password/ForgotPasswordConfig.js | 27 +++ .../forgot-password/ForgotPasswordPage.js | 193 ++++++++++++++++ .../reset-password/ResetPasswordConfig.js | 27 +++ .../main/reset-password/ResetPasswordPage.js | 212 ++++++++++++++++++ frontend/src/app/main/sign-in/SignInPage.js | 6 +- frontend/src/app/main/sign-up/SignUpPage.js | 5 + 11 files changed, 493 insertions(+), 3 deletions(-) create mode 100644 frontend/src/app/main/forgot-password/ForgotPasswordConfig.js create mode 100644 frontend/src/app/main/forgot-password/ForgotPasswordPage.js create mode 100644 frontend/src/app/main/reset-password/ResetPasswordConfig.js create mode 100644 frontend/src/app/main/reset-password/ResetPasswordPage.js diff --git a/application/managers/invitations.py b/application/managers/invitations.py index f2df6c94..f9be2a2b 100644 --- a/application/managers/invitations.py +++ b/application/managers/invitations.py @@ -121,7 +121,7 @@ async def send_email(self, invitation: UserInvitation) -> None: await send_email( invitation.email, f'You have been invited to join "{invitation.organization.title}" organization!', - f'To to join organization\'s team click following link: {link}' + f'To join organization\'s team click following link: {link}' ) invitation.email_sent_at = datetime.now() await self.db.save(invitation) diff --git a/application/managers/users.py b/application/managers/users.py index 8711a5f8..0e5ae2f4 100644 --- a/application/managers/users.py +++ b/application/managers/users.py @@ -23,6 +23,7 @@ from models.organization import Organization from models.user import User from schemas.users import UserCreate +from utils.email import send_email logger = logging.getLogger(__name__) @@ -178,7 +179,12 @@ async def on_after_register(self, user: User, request: Request | None = None): await self.invitation_manager.use(self.invitation_record, user) async def on_after_forgot_password(self, user: User, token: str, request: Request | None = None): - pass + link = f'{settings.UI_HOST}/reset-password?token={token}' + await send_email( + user.email, + f'Password reset request', + f'To reset forgotten password click following link: {link}' + ) async def on_after_request_verify(self, user: User, token: str, request: Request | None = None): pass diff --git a/frontend/src/app/auth/services/jwtService/jwtService.js b/frontend/src/app/auth/services/jwtService/jwtService.js index b17d8d27..9e21e0de 100644 --- a/frontend/src/app/auth/services/jwtService/jwtService.js +++ b/frontend/src/app/auth/services/jwtService/jwtService.js @@ -134,6 +134,14 @@ class JwtService extends FuseUtils.EventEmitter { getAccessToken = () => { return localStorage.getItem('jwt_access_token'); }; + + forgotPassword = async (email) => { + await axios.post(jwtServiceConfig.forgotPassword, { email }); + }; + + resetPassword = async (password, token) => { + await axios.post(jwtServiceConfig.resetPassword, { password, token }); + }; } const instance = new JwtService(); diff --git a/frontend/src/app/configs/routesConfig.js b/frontend/src/app/configs/routesConfig.js index 9ce39c5e..68805488 100644 --- a/frontend/src/app/configs/routesConfig.js +++ b/frontend/src/app/configs/routesConfig.js @@ -11,9 +11,11 @@ import ChartsConfig from '../main/charts/ChartsConfig'; import ClustersConfig from '../main/clusters/ClustersConfig'; import DashboardConfig from '../main/dashboard/DashboardConfig'; import ExampleConfig from '../main/example/ExampleConfig'; +import ForgotPasswordConfig from '../main/forgot-password/ForgotPasswordConfig'; import ReleaseDetailsConfig from '../main/releases/ReleaseDetails/ReleaseDetailsConfig'; import ReleasesConfig from '../main/releases/ReleasesConfig'; import RepositoriesConfig from '../main/repositories/RepositoriesConfig'; +import ResetPasswordConfig from '../main/reset-password/ResetPasswordConfig'; import ServicesConfig from '../main/services/ServicesConfig'; import SignInConfig from '../main/sign-in/SignInConfig'; import SignOutConfig from '../main/sign-out/SignOutConfig'; @@ -44,6 +46,8 @@ if (userInfo?.user_role === 'admin') { SignUpConfig, UsersConfig, TemplatesConfig, + ForgotPasswordConfig, + ResetPasswordConfig, ]; } else { routeConfigs = [ @@ -56,6 +60,8 @@ if (userInfo?.user_role === 'admin') { SignOutConfig, SignInConfig, SignUpConfig, + ForgotPasswordConfig, + ResetPasswordConfig, ]; } diff --git a/frontend/src/app/constants/paths.js b/frontend/src/app/constants/paths.js index e8627390..02e14315 100644 --- a/frontend/src/app/constants/paths.js +++ b/frontend/src/app/constants/paths.js @@ -12,4 +12,6 @@ export const PATHS = { SIGN_IN: 'sign-in', SIGN_UP: 'sign-up', SIGN_OUT: 'sign-out', + FORGOT_PASSWORD: 'forgot-password', + RESET_PASSWORD: 'reset-password', }; diff --git a/frontend/src/app/main/forgot-password/ForgotPasswordConfig.js b/frontend/src/app/main/forgot-password/ForgotPasswordConfig.js new file mode 100644 index 00000000..b554635e --- /dev/null +++ b/frontend/src/app/main/forgot-password/ForgotPasswordConfig.js @@ -0,0 +1,27 @@ +import authRoles from '../../auth/authRoles'; +import { PATHS } from '../../constants/paths'; + +import ForgotPasswordPage from './ForgotPasswordPage'; + +const ForgotPasswordConfig = { + settings: { + layout: { + config: { + navbar: { display: false }, + toolbar: { display: false }, + footer: { display: false }, + leftSidePanel: { display: false }, + rightSidePanel: { display: false }, + }, + }, + }, + auth: authRoles.onlyGuest, + routes: [ + { + path: PATHS.FORGOT_PASSWORD, + element: , + }, + ], +}; + +export default ForgotPasswordConfig; diff --git a/frontend/src/app/main/forgot-password/ForgotPasswordPage.js b/frontend/src/app/main/forgot-password/ForgotPasswordPage.js new file mode 100644 index 00000000..f4e241e5 --- /dev/null +++ b/frontend/src/app/main/forgot-password/ForgotPasswordPage.js @@ -0,0 +1,193 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import Avatar from '@mui/material/Avatar'; +import AvatarGroup from '@mui/material/AvatarGroup'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Snackbar from '@mui/material/Snackbar'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { Link } from 'react-router-dom'; +import * as yup from 'yup'; + +import _ from '@lodash'; + +import jwtService from '../../auth/services/jwtService'; +import { getErrorMessage } from '../sign-in/utils'; + +/** + * Form Validation Schema + */ +const schema = yup.object().shape({ + email: yup.string().email('You must enter a valid email').required('You must enter a email'), +}); + +const defaultValues = { + email: '', +}; + +function ForgotPasswordPage() { + const [open, setOpen] = useState(false); + const { control, formState, handleSubmit, reset, setError } = useForm({ + mode: 'onChange', + defaultValues, + resolver: yupResolver(schema), + }); + + const { isValid, dirtyFields, errors } = formState; + + const onSubmit = async ({ email }) => { + try { + await jwtService.forgotPassword(email); + setOpen(true); + } catch (error) { + setError('email', { + type: 'manual', + message: getErrorMessage(error), + }); + } + reset(defaultValues); + }; + + return ( +
+ +
+ logo + + + Forgot password? + +
+ Fill the form to reset your password +
+ +
+ ( + + )} + /> + + + + + Return to + + sign in + + + +
+
+ + + + + + + + + + + + + + + + +
+
+ +
Service Hub
+
+
+ Create on-demand services with Helm and Kubernetes. +
+
+ + + + + + + +
+ More than 1k Platform engineers are using Service Hub, now it's your turn. +
+
+
+
+ setOpen(false)} + message='A password reset link has been sent to your email' + /> +
+ ); +} + +export default ForgotPasswordPage; diff --git a/frontend/src/app/main/reset-password/ResetPasswordConfig.js b/frontend/src/app/main/reset-password/ResetPasswordConfig.js new file mode 100644 index 00000000..955cd591 --- /dev/null +++ b/frontend/src/app/main/reset-password/ResetPasswordConfig.js @@ -0,0 +1,27 @@ +import authRoles from '../../auth/authRoles'; +import { PATHS } from '../../constants/paths'; + +import ResetPasswordPage from './ResetPasswordPage'; + +const ResetPasswordConfig = { + settings: { + layout: { + config: { + navbar: { display: false }, + toolbar: { display: false }, + footer: { display: false }, + leftSidePanel: { display: false }, + rightSidePanel: { display: false }, + }, + }, + }, + auth: authRoles.onlyGuest, + routes: [ + { + path: PATHS.RESET_PASSWORD, + element: , + }, + ], +}; + +export default ResetPasswordConfig; diff --git a/frontend/src/app/main/reset-password/ResetPasswordPage.js b/frontend/src/app/main/reset-password/ResetPasswordPage.js new file mode 100644 index 00000000..3e3e3240 --- /dev/null +++ b/frontend/src/app/main/reset-password/ResetPasswordPage.js @@ -0,0 +1,212 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import Avatar from '@mui/material/Avatar'; +import AvatarGroup from '@mui/material/AvatarGroup'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { Controller, useForm } from 'react-hook-form'; +import { Link, Navigate, useNavigate, useSearchParams } from 'react-router-dom'; +import * as yup from 'yup'; + +import _ from '@lodash'; + +import jwtService from '../../auth/services/jwtService'; +import { getErrorMessage } from '../sign-in/utils'; + +/** + * Form Validation Schema + */ +const schema = yup.object().shape({ + password: yup + .string() + .required('Please enter your password.') + .min(8, 'Password is too short - should be 8 chars minimum.'), + passwordConfirm: yup.string().oneOf([yup.ref('password'), null], 'Passwords must match'), +}); + +const defaultValues = { + password: '', + passwordConfirm: '', +}; + +function ResetPasswordPage() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const token = searchParams.get('token'); + + const { control, formState, handleSubmit, reset, setError } = useForm({ + mode: 'onChange', + defaultValues, + resolver: yupResolver(schema), + }); + + const { isValid, dirtyFields, errors } = formState; + + const onSubmit = async ({ password }) => { + try { + await jwtService.resetPassword(password, token); + navigate('/sign-in'); + } catch (error) { + setError('email', { + type: 'manual', + message: getErrorMessage(error), + }); + } + reset(defaultValues); + }; + + if (!token) { + return ; + } + + return ( +
+ +
+ logo + + + Reset your password + + Create a new password for your account + +
+ ( + + )} + /> + + ( + + )} + /> + + + + + Return to + + sign in + + + +
+
+ + + + + + + + + + + + + + + + +
+
+ +
Service Hub
+
+
+ Create on-demand services with Helm and Kubernetes. +
+
+ + + + + + + +
+ More than 1k Platform engineers are using Service Hub, now it's your turn. +
+
+
+
+
+ ); +} + +export default ResetPasswordPage; diff --git a/frontend/src/app/main/sign-in/SignInPage.js b/frontend/src/app/main/sign-in/SignInPage.js index f6cd1ae7..1ec13859 100644 --- a/frontend/src/app/main/sign-in/SignInPage.js +++ b/frontend/src/app/main/sign-in/SignInPage.js @@ -189,7 +189,6 @@ const SignInPage = () => {
-
+
+ + Forgot my password + +
diff --git a/frontend/src/app/main/sign-up/SignUpPage.js b/frontend/src/app/main/sign-up/SignUpPage.js index 0c314c7c..db373e9d 100644 --- a/frontend/src/app/main/sign-up/SignUpPage.js +++ b/frontend/src/app/main/sign-up/SignUpPage.js @@ -201,6 +201,11 @@ function SignUpPage() { +
+ + Forgot my password + +