Added password reset functionality (#2058)
* added reset password functionality * updated changelog and versions of cvat-core, cvat-ui * fixed comments * Update cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-form.tsx * Fix CHANGELOG * fixed comments Co-authored-by: Nikita Manovich <nikita.manovich@intel.com>main
parent
a6884427d4
commit
510191f64b
@ -0,0 +1,156 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import Form, { FormComponentProps } from 'antd/lib/form/Form';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Input from 'antd/lib/input';
|
||||
|
||||
import patterns from 'utils/validation-patterns';
|
||||
|
||||
export interface ResetPasswordConfirmData {
|
||||
newPassword1: string;
|
||||
newPassword2: string;
|
||||
uid: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
type ResetPasswordConfirmFormProps = {
|
||||
fetching: boolean;
|
||||
onSubmit(resetPasswordConfirmData: ResetPasswordConfirmData): void;
|
||||
} & FormComponentProps & RouteComponentProps;
|
||||
|
||||
class ResetPasswordConfirmFormComponent extends React.PureComponent<ResetPasswordConfirmFormProps> {
|
||||
private validateConfirmation = (_: any, value: string, callback: Function): void => {
|
||||
const { form } = this.props;
|
||||
if (value && value !== form.getFieldValue('newPassword1')) {
|
||||
callback('Passwords do not match!');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
private validatePassword = (_: any, value: string, callback: Function): void => {
|
||||
const { form } = this.props;
|
||||
if (!patterns.validatePasswordLength.pattern.test(value)) {
|
||||
callback(patterns.validatePasswordLength.message);
|
||||
}
|
||||
|
||||
if (!patterns.passwordContainsNumericCharacters.pattern.test(value)) {
|
||||
callback(patterns.passwordContainsNumericCharacters.message);
|
||||
}
|
||||
|
||||
if (!patterns.passwordContainsUpperCaseCharacter.pattern.test(value)) {
|
||||
callback(patterns.passwordContainsUpperCaseCharacter.message);
|
||||
}
|
||||
|
||||
if (!patterns.passwordContainsLowerCaseCharacter.pattern.test(value)) {
|
||||
callback(patterns.passwordContainsLowerCaseCharacter.message);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
form.validateFields(['newPassword2'], { force: true });
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
private handleSubmit = (e: React.FormEvent): void => {
|
||||
e.preventDefault();
|
||||
const {
|
||||
form,
|
||||
onSubmit,
|
||||
location,
|
||||
} = this.props;
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const uid = params.get('uid');
|
||||
const token = params.get('token');
|
||||
|
||||
form.validateFields((error, values): void => {
|
||||
if (!error) {
|
||||
const validatedFields = {
|
||||
...values,
|
||||
uid,
|
||||
token,
|
||||
};
|
||||
|
||||
onSubmit(validatedFields);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private renderNewPasswordField(): JSX.Element {
|
||||
const { form } = this.props;
|
||||
|
||||
return (
|
||||
<Form.Item hasFeedback>
|
||||
{form.getFieldDecorator('newPassword1', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: 'Please input new password!',
|
||||
}, {
|
||||
validator: this.validatePassword,
|
||||
}],
|
||||
})(<Input.Password
|
||||
autoComplete='new-password'
|
||||
prefix={<Icon type='lock' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
|
||||
placeholder='New password'
|
||||
/>)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderNewPasswordConfirmationField(): JSX.Element {
|
||||
const { form } = this.props;
|
||||
|
||||
return (
|
||||
<Form.Item hasFeedback>
|
||||
{form.getFieldDecorator('newPassword2', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: 'Please confirm your new password!',
|
||||
}, {
|
||||
validator: this.validateConfirmation,
|
||||
}],
|
||||
})(<Input.Password
|
||||
autoComplete='new-password'
|
||||
prefix={<Icon type='lock' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
|
||||
placeholder='Confirm new password'
|
||||
/>)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { fetching } = this.props;
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={this.handleSubmit}
|
||||
className='cvat-reset-password-confirm-form'
|
||||
>
|
||||
{this.renderNewPasswordField()}
|
||||
{this.renderNewPasswordConfirmationField()}
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='cvat-reset-password-confirm-form-button'
|
||||
loading={fetching}
|
||||
disabled={fetching}
|
||||
>
|
||||
Change password
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
Form.create<ResetPasswordConfirmFormProps>()(ResetPasswordConfirmFormComponent),
|
||||
);
|
||||
@ -0,0 +1,83 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import { resetPasswordAsync } from 'actions/auth-actions';
|
||||
|
||||
import ResetPasswordConfirmForm, { ResetPasswordConfirmData } from './reset-password-confirm-form';
|
||||
|
||||
interface StateToProps {
|
||||
fetching: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
onResetPasswordConfirm: typeof resetPasswordAsync;
|
||||
}
|
||||
|
||||
interface ResetPasswordConfirmPageComponentProps {
|
||||
fetching: boolean;
|
||||
onResetPasswordConfirm: (
|
||||
newPassword1: string,
|
||||
newPassword2: string,
|
||||
uid: string,
|
||||
token: string) => void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
return {
|
||||
fetching: state.auth.fetching,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps: DispatchToProps = {
|
||||
onResetPasswordConfirm: resetPasswordAsync,
|
||||
};
|
||||
|
||||
function ResetPasswordPagePageComponent(
|
||||
props: ResetPasswordConfirmPageComponentProps,
|
||||
): JSX.Element {
|
||||
const sizes = {
|
||||
xs: { span: 14 },
|
||||
sm: { span: 14 },
|
||||
md: { span: 10 },
|
||||
lg: { span: 4 },
|
||||
xl: { span: 4 },
|
||||
};
|
||||
|
||||
const {
|
||||
fetching,
|
||||
onResetPasswordConfirm,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col {...sizes}>
|
||||
<Title level={2}> Change password </Title>
|
||||
<ResetPasswordConfirmForm
|
||||
fetching={fetching}
|
||||
onSubmit={(resetPasswordConfirmData: ResetPasswordConfirmData): void => {
|
||||
onResetPasswordConfirm(
|
||||
resetPasswordConfirmData.newPassword1,
|
||||
resetPasswordConfirmData.newPassword2,
|
||||
resetPasswordConfirmData.uid,
|
||||
resetPasswordConfirmData.token,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ResetPasswordPagePageComponent);
|
||||
@ -0,0 +1,81 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import Form, { FormComponentProps } from 'antd/lib/form/Form';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Input from 'antd/lib/input';
|
||||
|
||||
export interface ResetPasswordData {
|
||||
email: string;
|
||||
}
|
||||
|
||||
type ResetPasswordFormProps = {
|
||||
fetching: boolean;
|
||||
onSubmit(resetPasswordData: ResetPasswordData): void;
|
||||
} & FormComponentProps;
|
||||
|
||||
class ResetPasswordFormComponent extends React.PureComponent<ResetPasswordFormProps> {
|
||||
private handleSubmit = (e: React.FormEvent): void => {
|
||||
e.preventDefault();
|
||||
const {
|
||||
form,
|
||||
onSubmit,
|
||||
} = this.props;
|
||||
|
||||
form.validateFields((error, values): void => {
|
||||
if (!error) {
|
||||
onSubmit(values);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private renderEmailField(): JSX.Element {
|
||||
const { form } = this.props;
|
||||
|
||||
return (
|
||||
<Form.Item hasFeedback>
|
||||
{form.getFieldDecorator('email', {
|
||||
rules: [{
|
||||
type: 'email',
|
||||
message: 'The input is not valid E-mail!',
|
||||
}, {
|
||||
required: true,
|
||||
message: 'Please specify an email address',
|
||||
}],
|
||||
})(
|
||||
<Input
|
||||
autoComplete='email'
|
||||
prefix={<Icon type='mail' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
|
||||
placeholder='Email address'
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { fetching } = this.props;
|
||||
return (
|
||||
<Form onSubmit={this.handleSubmit} className='cvat-reset-password-form'>
|
||||
{this.renderEmailField()}
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type='primary'
|
||||
loading={fetching}
|
||||
disabled={fetching}
|
||||
htmlType='submit'
|
||||
className='cvat-reset-password-form-button'
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create<ResetPasswordFormProps>()(ResetPasswordFormComponent);
|
||||
@ -0,0 +1,79 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
|
||||
import { requestPasswordResetAsync } from 'actions/auth-actions';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import ResetPasswordForm, { ResetPasswordData } from './reset-password-form';
|
||||
|
||||
interface StateToProps {
|
||||
fetching: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
onResetPassword: typeof requestPasswordResetAsync;
|
||||
}
|
||||
|
||||
interface ResetPasswordPageComponentProps {
|
||||
fetching: boolean;
|
||||
onResetPassword: (email: string) => void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
return {
|
||||
fetching: state.auth.fetching,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps: DispatchToProps = {
|
||||
onResetPassword: requestPasswordResetAsync,
|
||||
};
|
||||
|
||||
function ResetPasswordPagePageComponent(props: ResetPasswordPageComponentProps): JSX.Element {
|
||||
const sizes = {
|
||||
xs: { span: 14 },
|
||||
sm: { span: 14 },
|
||||
md: { span: 10 },
|
||||
lg: { span: 4 },
|
||||
xl: { span: 4 },
|
||||
};
|
||||
|
||||
const {
|
||||
fetching,
|
||||
onResetPassword,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col {...sizes}>
|
||||
<Title level={2}> Reset password </Title>
|
||||
<ResetPasswordForm
|
||||
fetching={fetching}
|
||||
onSubmit={(resetPasswordData: ResetPasswordData): void => {
|
||||
onResetPassword(resetPasswordData.email);
|
||||
}}
|
||||
/>
|
||||
<Row type='flex' justify='start' align='top'>
|
||||
<Col>
|
||||
<Text strong>
|
||||
Go to
|
||||
<Link to='/auth/login'> login page </Link>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ResetPasswordPagePageComponent);
|
||||
@ -1,16 +1,31 @@
|
||||
from rest_auth.registration.serializers import RegisterSerializer
|
||||
from rest_auth.serializers import PasswordResetSerializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class RegisterSerializerEx(RegisterSerializer):
|
||||
first_name = serializers.CharField(required=False)
|
||||
last_name = serializers.CharField(required=False)
|
||||
first_name = serializers.CharField(required=False)
|
||||
last_name = serializers.CharField(required=False)
|
||||
|
||||
def get_cleaned_data(self):
|
||||
data = super().get_cleaned_data()
|
||||
data.update({
|
||||
'first_name': self.validated_data.get('first_name', ''),
|
||||
'last_name': self.validated_data.get('last_name', ''),
|
||||
})
|
||||
|
||||
def get_cleaned_data(self):
|
||||
data = super().get_cleaned_data()
|
||||
data.update({
|
||||
'first_name': self.validated_data.get('first_name', ''),
|
||||
'last_name': self.validated_data.get('last_name', ''),
|
||||
})
|
||||
return data
|
||||
|
||||
return data
|
||||
class PasswordResetSerializerEx(PasswordResetSerializer):
|
||||
def get_email_options(self):
|
||||
domain = None
|
||||
if hasattr(settings, 'UI_HOST') and settings.UI_HOST:
|
||||
domain = settings.UI_HOST
|
||||
if hasattr(settings, 'UI_PORT') and settings.UI_PORT:
|
||||
domain += ':{}'.format(settings.UI_PORT)
|
||||
return {
|
||||
'email_template_name': 'authentication/password_reset_email.html',
|
||||
'domain_override': domain
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
{% load i18n %}{% autoescape off %}
|
||||
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
|
||||
|
||||
{% trans "Please go to the following page and choose a new password:" %}
|
||||
{% block reset_link %}
|
||||
{{ protocol }}://{{ domain }}/auth/password/reset/confirm?uid={{ uid }}&token={{ token }}
|
||||
{% endblock %}
|
||||
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
|
||||
|
||||
{% trans "Thanks for using our site!" %}
|
||||
|
||||
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||
|
||||
{% endautoescape %}
|
||||
Loading…
Reference in New Issue