Skip to content

Commit

Permalink
152 UI password reset screen (#162)
Browse files Browse the repository at this point in the history
* AC/UI password reset screen

* Email forgotten password reset link.

* AC/added reset password functionality

Co-authored-by: Vadim Zabolotniy <[email protected]>
  • Loading branch information
NastiaChooo and vadim-zabolotniy authored Nov 3, 2022
1 parent 2ddf02d commit 97d32c2
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 3 deletions.
2 changes: 1 addition & 1 deletion application/managers/invitations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion application/managers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/app/auth/services/jwtService/jwtService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/app/configs/routesConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +46,8 @@ if (userInfo?.user_role === 'admin') {
SignUpConfig,
UsersConfig,
TemplatesConfig,
ForgotPasswordConfig,
ResetPasswordConfig,
];
} else {
routeConfigs = [
Expand All @@ -56,6 +60,8 @@ if (userInfo?.user_role === 'admin') {
SignOutConfig,
SignInConfig,
SignUpConfig,
ForgotPasswordConfig,
ResetPasswordConfig,
];
}

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/constants/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
27 changes: 27 additions & 0 deletions frontend/src/app/main/forgot-password/ForgotPasswordConfig.js
Original file line number Diff line number Diff line change
@@ -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: <ForgotPasswordPage />,
},
],
};

export default ForgotPasswordConfig;
193 changes: 193 additions & 0 deletions frontend/src/app/main/forgot-password/ForgotPasswordPage.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className='flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-1 min-w-0'>
<Paper className='h-full sm:h-auto md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-16 sm:p-48 md:p-64 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none ltr:border-r-1 rtl:border-l-1'>
<div className='w-full max-w-320 sm:w-320 mx-auto sm:mx-0'>
<img className='w-[100px]' src='assets/images/logo.png' alt='logo' />

<Typography className='mt-32 text-4xl font-extrabold tracking-tight leading-tight'>
Forgot password?
</Typography>
<div className='flex items-baseline mt-2 font-medium'>
<Typography>Fill the form to reset your password</Typography>
</div>

<form
name='registerForm'
noValidate
className='flex flex-col justify-center w-full mt-32'
onSubmit={handleSubmit(onSubmit)}
>
<Controller
name='email'
control={control}
render={({ field }) => (
<TextField
{...field}
className='mb-24'
label='Email'
type='email'
error={!!errors.email}
helperText={errors?.email?.message}
variant='outlined'
required
fullWidth
/>
)}
/>

<Button
variant='contained'
color='secondary'
className=' w-full mt-4'
aria-label='Register'
disabled={_.isEmpty(dirtyFields) || !isValid}
type='submit'
size='large'
>
Send reset link
</Button>

<Typography className='mt-32 text-md font-medium' color='text.secondary'>
<span>Return to</span>
<Link className='ml-4' to='/sign-in'>
sign in
</Link>
</Typography>
</form>
</div>
</Paper>
<Box
className='relative hidden md:flex flex-auto items-center justify-center h-full p-64 lg:px-112 overflow-hidden'
sx={{ backgroundColor: 'primary.main' }}
>
<svg
className='absolute inset-0 pointer-events-none'
viewBox='0 0 960 540'
width='100%'
height='100%'
preserveAspectRatio='xMidYMax slice'
xmlns='http://www.w3.org/2000/svg'
>
<Box
component='g'
sx={{ color: 'primary.light' }}
className='opacity-20'
fill='none'
stroke='currentColor'
strokeWidth='100'
>
<circle r='234' cx='196' cy='23' />
<circle r='234' cx='790' cy='491' />
</Box>
</svg>
<Box
component='svg'
className='absolute -top-64 -right-64 opacity-20'
sx={{ color: 'primary.light' }}
viewBox='0 0 220 192'
width='220px'
height='192px'
fill='none'
>
<defs>
<pattern
id='837c3e70-6c3a-44e6-8854-cc48c737b659'
x='0'
y='0'
width='20'
height='20'
patternUnits='userSpaceOnUse'
>
<rect x='0' y='0' width='4' height='4' fill='currentColor' />
</pattern>
</defs>
<rect width='220' height='192' fill='url(#837c3e70-6c3a-44e6-8854-cc48c737b659)' />
</Box>

<div className='z-10 relative w-full max-w-2xl'>
<div className='text-7xl font-bold leading-none text-gray-100'>
<img src='assets/images/logo-white.png' width='130px' />
<div>Service Hub</div>
</div>
<div className='mt-24 text-lg tracking-tight leading-6 text-gray-400'>
Create on-demand services with Helm and Kubernetes.
</div>
<div className='flex items-center mt-32'>
<AvatarGroup sx={{ '& .MuiAvatar-root': { borderColor: 'primary.main' } }}>
<Avatar src='assets/images/avatars/male-16.jpg' />
<Avatar src='assets/images/avatars/female-11.jpg' />
<Avatar src='assets/images/avatars/male-09.jpg' />
<Avatar src='assets/images/avatars/female-18.jpg' />
</AvatarGroup>

<div className='ml-16 font-medium tracking-tight text-gray-400'>
More than 1k Platform engineers are using Service Hub, now it's your turn.
</div>
</div>
</div>
</Box>
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
open={open}
autoHideDuration={6000}
onClose={() => setOpen(false)}
message='A password reset link has been sent to your email'
/>
</div>
);
}

export default ForgotPasswordPage;
27 changes: 27 additions & 0 deletions frontend/src/app/main/reset-password/ResetPasswordConfig.js
Original file line number Diff line number Diff line change
@@ -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: <ResetPasswordPage />,
},
],
};

export default ResetPasswordConfig;
Loading

0 comments on commit 97d32c2

Please sign in to comment.