Commit 590b3a6a authored by Viacheslav Pavlov's avatar Viacheslav Pavlov Committed by Matija Obreza

Forgot password functionality

- Removed remember me functionality
- Added reCaptcha to reset cancellation
parent f136ed0f
......@@ -2097,6 +2097,10 @@
"fullName": "Your full name",
"register": "Register"
},
"resetPasswordForm": {
"key": "Verification key",
"resetPassword": "Reset password"
},
"userProfileCard": {
"accountExpires": "Account expires",
"lastLogin": "Last login",
......@@ -2108,11 +2112,25 @@
"p": {
"login": {
"title": "Welcome to Genesys",
"subTitle": "Log in to manage datasets"
"subTitle": "Log in to manage datasets",
"forgotPassword": "Forgot password"
},
"registration": {
"title": "Create your account",
"register": "Register a new account"
},
"resetPassword": {
"title": "Reset password"
},
"forgotPassword": {
"title": "Reset password",
"headerTitle": "To reset your password, you have to provide your registered email address.",
"subTitle": "In the unlikely case that the email address does not exist in our system, we will silently ignore your password reset request and pretend all was okay with your input."
},
"cancelReset": {
"title": "Canceling reset password request",
"cancelReset": "Confirm the cancellation of the password reset request",
"success": "Your request has been canceled successfully"
}
}
},
......
......@@ -6,5 +6,8 @@ const VERIFY_GOOGLE_TOKEN = 'VERIFY_GOOGLE_TOKEN';
const REGISTRATION_FORM = 'Form/Login/REGISTRATION_FORM';
const FORGOT_PASSWORD_FORM = 'Form/user/FORGOT_PASSWORD';
const RESET_PASSWORD_FORM = 'Form/user/RESET_PASSWORD';
const CANCEL_RESET_PASSWORD_FORM = 'Form/user/CANCEL_RESET_PASSWORD';
export {LOGIN_USER, LOGIN_APP, LOGOUT, CHECK_TOKEN, VERIFY_GOOGLE_TOKEN, REGISTRATION_FORM};
export {LOGIN_USER, LOGIN_APP, LOGOUT, CHECK_TOKEN, VERIFY_GOOGLE_TOKEN, REGISTRATION_FORM, FORGOT_PASSWORD_FORM, RESET_PASSWORD_FORM, CANCEL_RESET_PASSWORD_FORM};
import { axiosBackend } from 'utilities/requestUtils';
import * as UrlTemplate from 'url-template';
import * as QueryString from 'query-string';
// import FormData from 'form-data'; // it's in the browser
const LOGIN_URL = `/oauth/token`;
const URL_LOGOUT = `/api/v1/me/logout`;
const URL_RESET_PASSWORD = `/api/v1/me/password/reset`;
const URL_CANCEL_VALIDATION = UrlTemplate.parse(`/api/v1/me/{tokenUuid}/cancel`);
const URL_UPDATE_PASSWORD = UrlTemplate.parse(`/api/v1/me/{tokenUuid}/pwdreset`);
// const LOGOUT_URL = `${API_BASE_URL}/me/logout`;
export const CHECK_TOKEN_URL = `/oauth/check_token`;
export const VERIFY_GOOGLE_TOKEN_URL = `/api/google/verify-token`;
......@@ -74,4 +80,76 @@ export class LoginService {
return axiosBackend.post(VERIFY_GOOGLE_TOKEN_URL, form)
.then(({ data }) => data);
}
/**
* resetPassword at /api/v1/me/password/reset
*
* @param email email
* @param gRecaptchaResponse g-recaptcha-response
*/
public static resetPassword(email: string, gRecaptchaResponse?: string): Promise<boolean> {
const qs = QueryString.stringify({
'email': email || undefined,
'g-recaptcha-response': gRecaptchaResponse || undefined,
}, {});
const apiUrl = URL_RESET_PASSWORD + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return axiosBackend.request({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as boolean);
}
/**
* cancelValidation at /api/v1/me/{tokenUuid}/cancel
*
* @param tokenUuid tokenUuid
* @param gRecaptchaResponse g-recaptcha-response
*/
public static cancelValidation(tokenUuid: string, gRecaptchaResponse?: string): Promise<boolean> {
const qs = QueryString.stringify({
'g-recaptcha-response': gRecaptchaResponse || undefined,
}, {});
const apiUrl = URL_CANCEL_VALIDATION.expand({ tokenUuid }) + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return axiosBackend.request({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as boolean);
}
/**
* updatePassword at /api/v1/me/{tokenUuid}/pwdreset
*
* @param tokenUuid tokenUuid
* @param key key
* @param password password
* @param gRecaptchaResponse g-recaptcha-response
*/
public static updatePassword(tokenUuid: string, key: string, password: string, gRecaptchaResponse?: string): Promise<boolean> {
const qs = QueryString.stringify({
'key': key || undefined,
'password': password || undefined,
'g-recaptcha-response:': gRecaptchaResponse || undefined,
}, {});
const apiUrl = URL_UPDATE_PASSWORD.expand({ tokenUuid }) + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return axiosBackend.request({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as boolean);
}
}
......@@ -27,7 +27,7 @@ const styles = (theme) => ({
},
},
children: {
flexGrow: 1,
flex: '1',
width: '100%',
},
footer: {
......
......@@ -16,6 +16,27 @@ const publicRoutes = [
}),
exact: true,
},
{
path: '/forgot-password',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "user" */'user/ui/ForgotPasswordPage'),
}),
exact: true,
},
{
path: '/profile/:tokenUUID/pwdreset',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "user" */'user/ui/ResetPasswordPage'),
}),
exact: true,
},
{
path: '/profile/:tokenUUID/cancel',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "user" */'user/ui/CancelResetPassword'),
}),
exact: true,
},
];
const dashboardRoutes = [
......
......@@ -14,6 +14,10 @@
"fullName": "Your full name",
"register": "Register"
},
"resetPasswordForm": {
"key": "Verification key",
"resetPassword": "Reset password"
},
"userProfileCard": {
"accountExpires": "Account expires",
"lastLogin": "Last login",
......@@ -25,11 +29,25 @@
"p": {
"login": {
"title": "Welcome to Genesys",
"subTitle": "Log in to manage datasets"
"subTitle": "Log in to manage datasets",
"forgotPassword": "Forgot password"
},
"registration": {
"title": "Create your account",
"register": "Register a new account"
},
"resetPassword": {
"title": "Reset password"
},
"forgotPassword": {
"title": "Reset password",
"headerTitle": "To reset your password, you have to provide your registered email address.",
"subTitle": "In the unlikely case that the email address does not exist in our system, we will silently ignore your password reset request and pretend all was okay with your input."
},
"cancelReset": {
"title": "Canceling reset password request",
"cancelReset": "Confirm the cancellation of the password reset request",
"success": "Your request has been canceled successfully"
}
}
},
......
import * as React from 'react';
import {translate} from 'react-i18next';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
// actions
import navigateTo from 'actions/navigation';
import {showSnackbar} from 'actions/snackbar';
// service
import {LoginService} from 'service/LoginService';
// ui
import PageLayout from 'ui/layout/PageLayout';
import {CardContent, CardHeader} from 'ui/common/Card';
import PageTitle from 'ui/common/PageTitle';
import {Grid, Card} from '@material-ui/core';
import CancelResetPasswordForm from 'user/ui/c/CancelResetPasswordForm';
class CancelResetPassword extends React.Component<any> {
public state = {
errorMsg: null,
};
private handleSubmit = (values) => {
const {tokenUUID, navigateTo, showSnackbar} = this.props;
LoginService.cancelValidation(tokenUUID, values.captcha)
.then(() => {
showSnackbar('user.public.p.cancelReset.success');
return navigateTo('/login');
})
.catch((e) => {
const data = _.get(e, 'response.data');
if (data && data.error) {
this.setState({errorMsg: data.error});
}
});
}
public render() {
const {t, captchaClientKey} = this.props;
const {errorMsg} = this.state;
return (
<PageLayout withFooter>
<PageTitle title={ t('user.public.p.cancelReset.title') }/>
<Grid container spacing={ 0 } justify="center" className="back-gray p-20">
<Grid item xs={ 12 } md={ 5 } lg={ 4 } xl={ 3 }>
<Card>
<CardHeader title={ t('user.public.p.cancelReset.cancelReset') }/>
<CardContent>
<CancelResetPasswordForm captchaClientKey={ captchaClientKey } onSubmit={ this.handleSubmit } errorMsg={ errorMsg }/>
</CardContent>
</Card>
</Grid>
</Grid>
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
captchaClientKey: state.serverInfo.data.captchaSiteKey,
tokenUUID: ownProps.match.params.tokenUUID,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
showSnackbar,
navigateTo,
}, dispatch);
export default translate()(connect(mapStateToProps, mapDispatchToProps)(CancelResetPassword));
import * as React from 'react';
import {translate} from 'react-i18next';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as _ from 'lodash';
import navigateTo from 'actions/navigation';
// service
import {LoginService} from 'service/LoginService';
import PageLayout from 'ui/layout/PageLayout';
import ContentHeader from 'ui/common/heading/ContentHeader';
import PageTitle from 'ui/common/PageTitle';
import Card, {CardHeader, CardContent} from 'ui/common/Card';
import ForgotPasswordForm from './c/ForgotPasswordForm';
import Grid from '@material-ui/core/Grid';
interface ILoginContainerProps extends React.ClassAttributes<any> {
login: any;
captchaClientKey: string;
navigateTo: (location: string) => void;
t: any;
}
class LoginContainer extends React.Component<ILoginContainerProps> {
public state = {
errorMsg: '',
};
private handleSubmit = (values): any => {
const {navigateTo} = this.props;
return LoginService.resetPassword(values.email, values.captcha)
.then(() => {
return navigateTo('/content/user-password-reset-email-sent');
})
.catch((e) => {
const data = _.get(e, 'response.data');
if (data && data.error) {
this.setState({errorMsg: data.error});
}
});
}
public render() {
const {t, captchaClientKey} = this.props;
const {errorMsg} = this.state;
return (
<PageLayout withFooter>
<PageTitle title={ t('user.public.p.forgotPassword.title') }/>
<ContentHeader title={ t('user.public.p.forgotPassword.headerTitle') } subTitle="user.public.p.forgotPassword.subTitle"/>
<Grid container spacing={ 0 } justify="center" className="back-gray p-20">
<Grid item xs={ 12 } md={ 5 } lg={ 4 } xl={ 3 }>
<Card>
<CardHeader title={ t('user.public.p.forgotPassword.title') }/>
<CardContent>
<ForgotPasswordForm captchaClientKey={ captchaClientKey } onSubmit={ this.handleSubmit } errorMsg={ errorMsg }/>
</CardContent>
</Card>
</Grid>
</Grid>
</PageLayout>
);
}
}
const mapStateToProps = (state) => ({
captchaClientKey: state.serverInfo.data.captchaSiteKey,
lang: state.applicationConfig.lang,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
navigateTo,
}, dispatch);
export default translate()(connect(mapStateToProps, mapDispatchToProps)(LoginContainer));
import * as React from 'react';
import {translate} from 'react-i18next';
import { Link } from 'react-router-dom';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as _ from 'lodash';
......@@ -13,7 +14,7 @@ import ContentHeader from 'ui/common/heading/ContentHeader';
import PageLayout from 'ui/layout/PageLayout';
import LoginForm from './c/LoginForm';
import Grid from '@material-ui/core/Grid';
import Card, { CardHeader, CardContent } from 'ui/common/Card';
import Card, {CardHeader, CardContent, CardActions} from 'ui/common/Card';
import PageTitle from 'ui/common/PageTitle';
interface ILoginContainerProps extends React.ClassAttributes<any> {
......@@ -79,6 +80,9 @@ class LoginContainer extends React.Component<ILoginContainerProps, void> {
<CardContent>
<LoginForm onTryLogin={ this.onLogin } onTryGoogleLogin={ this.onGoogleLogin } googleClientId={ this.props.googleClientId } />
</CardContent>
<CardActions>
<Link to="/forgot-password">{ t('user.public.p.login.forgotPassword') }</Link>
</CardActions>
</Card>
</Grid>
</Grid>
......
import * as React from 'react';
import {translate} from 'react-i18next';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as _ from 'lodash';
import navigateTo from 'actions/navigation';
// service
import {LoginService} from 'service/LoginService';
import PageLayout from 'ui/layout/PageLayout';
import PageTitle from 'ui/common/PageTitle';
import Card, {CardHeader, CardContent} from 'ui/common/Card';
import ResetPasswordForm from './c/ResetPasswordForm';
import Grid from '@material-ui/core/Grid';
interface ILoginContainerProps extends React.ClassAttributes<any> {
login: any;
tokenUUID: string;
captchaClientKey: string;
navigateTo: (location: string) => void;
t: any;
}
class LoginContainer extends React.Component<ILoginContainerProps> {
public state = {
errorMsg: '',
};
private handleSubmit = (values): any => {
const {navigateTo, tokenUUID} = this.props;
return LoginService.updatePassword(tokenUUID, values.key, values.password, values.captha)
.then(() => {
return navigateTo('/content/user-password-reset');
})
.catch((e) => {
const data = _.get(e, 'response.data');
if (data && data.error) {
this.setState({errorMsg: data.error});
}
});
}
public render() {
const {t, captchaClientKey} = this.props;
const {errorMsg} = this.state;
return (
<PageLayout withFooter>
<PageTitle title={ t('user.public.p.resetPassword.title') }/>
<Grid container spacing={ 0 } justify="center" className="back-gray p-20">
<Grid item xs={ 12 } md={ 5 } lg={ 4 } xl={ 3 }>
<Card>
<CardHeader title={ t('user.public.p.resetPassword.title') }/>
<CardContent>
<ResetPasswordForm captchaClientKey={ captchaClientKey } onSubmit={ this.handleSubmit } errorMsg={ errorMsg }/>
</CardContent>
</Card>
</Grid>
</Grid>
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
captchaClientKey: state.serverInfo.data.captchaSiteKey,
tokenUUID: ownProps.match.params.tokenUUID,
lang: state.applicationConfig.lang,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
navigateTo,
}, dispatch);
export default translate()(connect(mapStateToProps, mapDispatchToProps)(LoginContainer));
import * as React from 'react';
import {translate} from 'react-i18next';
import ReCAPTCHA from 'react-google-recaptcha';
import {Field, reduxForm} from 'redux-form';
// Constants
import {CANCEL_RESET_PASSWORD_FORM} from 'constants/login';
// UI
import Button from '@material-ui/core/Button';
class CaptchaInput extends React.Component<any> {
public render() {
const {captchaClientKey, input} = this.props;
return !captchaClientKey ?
null :
<ReCAPTCHA sitekey={ captchaClientKey } onChange={ (value) => input.onChange({...input.value, captcha: value}) }/>;
}
}
interface ICancelResetPasswordFormProps extends React.ClassAttributes<any> {
handleSubmit: any;
errorMsg: string;
captchaClientKey: string;
t: any;
}
class CancelResetPasswordForm extends React.Component<ICancelResetPasswordFormProps, any> {
public state = {
captchaResponse: '',
};
public render() {
const {handleSubmit, captchaClientKey, errorMsg, t} = this.props;
return (
<form onSubmit={ handleSubmit }>
<Field
name="captcha"
component={ CaptchaInput }
captchaClientKey={ captchaClientKey }
fullWidth
/>
{ errorMsg && <div style={ {color: 'red'} }>{ errorMsg }</div> }
<Button variant="contained" color="secondary" type="submit" style={ {marginTop: '1rem'} }>
{ t('common:action.confirm') }
</Button>
</form>
);
}
}
export default translate()(reduxForm({
form: CANCEL_RESET_PASSWORD_FORM,
enableReinitialize: true,
})(CancelResetPasswordForm));
import * as React from 'react';
import {translate} from 'react-i18next';
import ReCAPTCHA from 'react-google-recaptcha';
import {Field, reduxForm} from 'redux-form';
// Constants
import {FORGOT_PASSWORD_FORM} from 'constants/login';
// Util
import Validators from 'utilities/Validators';
// UI
import {TextField} from 'ui/common/text-field/index';
import Button from '@material-ui/core/Button';
class CaptchaInput extends React.Component<any> {
public render() {
const {captchaClientKey, input} = this.props;
return !captchaClientKey ?
null :
<ReCAPTCHA sitekey={ captchaClientKey } onChange={ (value) => input.onChange({...input.value, captcha: value}) }/>;
}
}
interface IForgotPasswordFormProps extends React.ClassAttributes<any> {
handleSubmit: any;
errorMsg: string;
captchaClientKey: string;
t: any;
}
class ForgotPasswordForm extends React.Component<IForgotPasswordFormProps, any> {
public state = {
captchaResponse: '',
};
public render() {
const {handleSubmit, captchaClientKey, errorMsg, t} = this.props;
return (
<form onSubmit={ handleSubmit }>
<Field
name="email"
label={ t('user.common.email') }
type="text"
component={ TextField }
validate={ [Validators.required] }
fullWidth
/>
<Field
name="captcha"
component={ CaptchaInput }
captchaClientKey={ captchaClientKey }
fullWidth
/>
{ errorMsg && <div style={ {color: 'red'} }>{ errorMsg }</div> }
<Button variant="contained" color="secondary" type="submit" style={ {marginTop: '1rem'} }>
{ t('common:action.confirm') }
</Button>
</form>
);
}
}