#26: forgot/restore password frontend logic

This commit is contained in:
SleepWalker 2016-05-14 23:53:58 +03:00
parent a584ef0d95
commit 3472c7b29f
28 changed files with 621 additions and 255 deletions

View File

@ -28,7 +28,7 @@
"react-motion": "^0.4.0",
"react-redux": "^4.0.0",
"react-router": "^2.0.0",
"react-router-redux": "^2.1.0",
"react-router-redux": "^3.0.0",
"redux": "^3.0.4",
"redux-thunk": "^1.0.0",
"whatwg-fetch": "^0.11.0"

View File

@ -219,7 +219,8 @@ class PanelTransition extends Component {
activation: fromRight,
permissions: fromLeft,
changePassword: fromRight,
forgotPassword: fromRight
forgotPassword: [panelId, prevPanelId].includes('recoverPassword') ? fromLeft : fromRight,
recoverPassword: fromRight
};
const sign = map[key];
const transform = sign * 100;
@ -240,7 +241,8 @@ class PanelTransition extends Component {
activation: not('register') ? 'Y' : 'X',
permissions: 'Y',
changePassword: 'Y',
forgotPassword: not('password') && not('login') ? 'Y' : 'X'
forgotPassword: not('password') && not('login') ? 'Y' : 'X',
recoverPassword: not('password') && not('login') && not('forgotPassword') ? 'Y' : 'X'
};
return map[next];

View File

@ -33,12 +33,9 @@ export function login({login = '', password = '', rememberMe = false}) {
if (resp.errors.login === LOGIN_REQUIRED && password) {
dispatch(logout());
}
const errorMessage = resp.errors[Object.keys(resp.errors)[0]];
dispatch(setError(errorMessage));
return Promise.reject(errorMessage);
}
// TODO: log unexpected errors
return validationErrorsHandler(dispatch)(resp);
})
);
}
@ -50,15 +47,44 @@ export function changePassword({
}) {
return wrapInLoader((dispatch) =>
dispatch(changeUserPassword({password, newPassword, newRePassword, logoutAll : false}))
.catch((resp) => {
if (resp.errors) {
const errorMessage = resp.errors[Object.keys(resp.errors)[0]];
dispatch(setError(errorMessage));
return Promise.reject(errorMessage);
}
.catch(validationErrorsHandler(dispatch))
);
}
// TODO: log unexpected errors
})
export function forgotPassword({
login = ''
}) {
return wrapInLoader((dispatch, getState) =>
request.post(
'/api/authentication/forgot-password',
{login}
)
.then(({data = {}}) => dispatch(updateUser({
maskedEmail: data.emailMask || getState().user.email
})))
.catch(validationErrorsHandler(dispatch))
);
}
export function recoverPassword({
key = '',
newPassword = '',
newRePassword = ''
}) {
return wrapInLoader((dispatch) =>
request.post(
'/api/authentication/recover-password',
{key, newPassword, newRePassword}
)
.then((resp) => {
dispatch(updateUser({
isGuest: false,
isActive: true
}));
return dispatch(authenticate(resp.jwt));
})
.catch(validationErrorsHandler(dispatch))
);
}
@ -82,18 +108,7 @@ export function register({
dispatch(needActivation());
dispatch(routeActions.push('/activation'));
})
.catch((resp) => {
if (resp.errors) {
const errorMessage = resp.errors[Object.keys(resp.errors)[0]];
dispatch(setError(errorMessage));
return Promise.reject(errorMessage);
}
// TODO: log unexpected errors
// We can get here something like:
// code: 500
// {"name":"Invalid Configuration","message":"","code":0,"type":"yii\\base\\InvalidConfigException","file":"/home/sleepwalker/www/account/api/components/ReCaptcha/Component.php","line":12,"stack-trace":["#0 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Object.php(107): api\\components\\ReCaptcha\\Component->init()","#1 [internal function]: yii\\base\\Object->__construct(Array)","#2 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/Container.php(368): ReflectionClass->newInstanceArgs(Array)","#3 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/Container.php(153): yii\\di\\Container->build('api\\\\components\\\\...', Array, Array)","#4 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/BaseYii.php(344): yii\\di\\Container->get('api\\\\components\\\\...', Array, Array)","#5 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/ServiceLocator.php(133): yii\\BaseYii::createObject(Array)","#6 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/ServiceLocator.php(71): yii\\di\\ServiceLocator->get('reCaptcha')","#7 /home/sleepwalker/www/account/api/components/ReCaptcha/Validator.php(20): yii\\di\\ServiceLocator->__get('reCaptcha')","#8 /home/sleepwalker/www/account/api/components/ReCaptcha/Validator.php(25): api\\components\\ReCaptcha\\Validator->getComponent()","#9 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Object.php(107): api\\components\\ReCaptcha\\Validator->init()","#10 [internal function]: yii\\base\\Object->__construct(Array)","#11 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/Container.php(374): ReflectionClass->newInstanceArgs(Array)","#12 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/Container.php(153): yii\\di\\Container->build('api\\\\components\\\\...', Array, Array)","#13 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/BaseYii.php(344): yii\\di\\Container->get('api\\\\components\\\\...', Array, Array)","#14 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/validators/Validator.php(209): yii\\BaseYii::createObject(Array)","#15 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(445): yii\\validators\\Validator::createValidator('api\\\\components\\\\...', Object(api\\models\\RegistrationForm), Array, Array)","#16 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(409): yii\\base\\Model->createValidators()","#17 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(185): yii\\base\\Model->getValidators()","#18 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(751): yii\\base\\Model->scenarios()","#19 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(695): yii\\base\\Model->safeAttributes()","#20 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(823): yii\\base\\Model->setAttributes(Array)","#21 /home/sleepwalker/www/account/api/controllers/SignupController.php(41): yii\\base\\Model->load(Array)","#22 [internal function]: api\\controllers\\SignupController->actionIndex()","#23 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/InlineAction.php(55): call_user_func_array(Array, Array)","#24 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Controller.php(154): yii\\base\\InlineAction->runWithParams(Array)","#25 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Module.php(454): yii\\base\\Controller->runAction('', Array)","#26 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/web/Application.php(84): yii\\base\\Module->runAction('signup', Array)","#27 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Application.php(375): yii\\web\\Application->handleRequest(Object(yii\\web\\Request))","#28 /home/sleepwalker/www/account/api/web/index.php(18): yii\\base\\Application->run()","#29 {main}"]}
})
.catch(validationErrorsHandler(dispatch))
);
}
@ -111,13 +126,7 @@ export function activate({key = ''}) {
return dispatch(authenticate(resp.jwt));
})
.catch((resp) => {
const errorMessage = resp.errors[Object.keys(resp.errors)[0]];
dispatch(setError(errorMessage));
return Promise.reject(errorMessage);
// TODO: log unexpected errors
})
.catch(validationErrorsHandler(dispatch))
);
}
@ -322,3 +331,21 @@ function needActivation() {
isGuest: false
});
}
function validationErrorsHandler(dispatch) {
return (resp) => {
if (resp.errors) {
const errorMessage = resp.errors[Object.keys(resp.errors)[0]];
dispatch(setError(errorMessage));
return Promise.reject(errorMessage);
}
return Promise.reject(resp);
// TODO: log unexpected errors
// We can get here something like:
// code: 500
// {"name":"Invalid Configuration","message":"","code":0,"type":"yii\\base\\InvalidConfigException","file":"/home/sleepwalker/www/account/api/components/ReCaptcha/Component.php","line":12,"stack-trace":["#0 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Object.php(107): api\\components\\ReCaptcha\\Component->init()","#1 [internal function]: yii\\base\\Object->__construct(Array)","#2 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/Container.php(368): ReflectionClass->newInstanceArgs(Array)","#3 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/Container.php(153): yii\\di\\Container->build('api\\\\components\\\\...', Array, Array)","#4 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/BaseYii.php(344): yii\\di\\Container->get('api\\\\components\\\\...', Array, Array)","#5 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/ServiceLocator.php(133): yii\\BaseYii::createObject(Array)","#6 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/ServiceLocator.php(71): yii\\di\\ServiceLocator->get('reCaptcha')","#7 /home/sleepwalker/www/account/api/components/ReCaptcha/Validator.php(20): yii\\di\\ServiceLocator->__get('reCaptcha')","#8 /home/sleepwalker/www/account/api/components/ReCaptcha/Validator.php(25): api\\components\\ReCaptcha\\Validator->getComponent()","#9 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Object.php(107): api\\components\\ReCaptcha\\Validator->init()","#10 [internal function]: yii\\base\\Object->__construct(Array)","#11 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/Container.php(374): ReflectionClass->newInstanceArgs(Array)","#12 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/di/Container.php(153): yii\\di\\Container->build('api\\\\components\\\\...', Array, Array)","#13 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/BaseYii.php(344): yii\\di\\Container->get('api\\\\components\\\\...', Array, Array)","#14 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/validators/Validator.php(209): yii\\BaseYii::createObject(Array)","#15 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(445): yii\\validators\\Validator::createValidator('api\\\\components\\\\...', Object(api\\models\\RegistrationForm), Array, Array)","#16 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(409): yii\\base\\Model->createValidators()","#17 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(185): yii\\base\\Model->getValidators()","#18 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(751): yii\\base\\Model->scenarios()","#19 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(695): yii\\base\\Model->safeAttributes()","#20 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Model.php(823): yii\\base\\Model->setAttributes(Array)","#21 /home/sleepwalker/www/account/api/controllers/SignupController.php(41): yii\\base\\Model->load(Array)","#22 [internal function]: api\\controllers\\SignupController->actionIndex()","#23 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/InlineAction.php(55): call_user_func_array(Array, Array)","#24 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Controller.php(154): yii\\base\\InlineAction->runWithParams(Array)","#25 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Module.php(454): yii\\base\\Controller->runAction('', Array)","#26 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/web/Application.php(84): yii\\base\\Module->runAction('signup', Array)","#27 /home/sleepwalker/www/ely/vendor/yiisoft/yii2/base/Application.php(375): yii\\web\\Application->handleRequest(Object(yii\\web\\Request))","#28 /home/sleepwalker/www/account/api/web/index.php(18): yii\\base\\Application->run()","#29 {main}"]}
// We need here status code. Probably `request` module should add _request field en each resp
};
}

View File

@ -31,7 +31,7 @@ export default class ActivationBody extends BaseAuthBody {
<div className={styles.formRow}>
<Input {...this.bindField('key')}
color="blue"
className={styles.activationCodeInput}
style={{textAlign: 'center'}}
required
placeholder={messages.enterTheCode}
/>

View File

@ -17,10 +17,3 @@
line-height: 1.4;
font-size: 16px;
}
.activationCodeInput {
composes: darkTextField from 'components/ui/form/form.scss';
composes: blueTextField from 'components/ui/form/form.scss';
text-align: center;
}

View File

@ -1,7 +1,7 @@
{
"forgotPasswordTitle": "Forgot password",
"contactSupport": "Contact support",
"title": "Forgot password",
"sendMail": "Send mail",
"forgotPasswordMessage": "Specify the registration E-mail address for your account and we will send an email with instructions for further password recovery.",
"accountEmail": "Enter account E-mail"
"specifyEmail": "Specify the registration E-mail address for your account and we will send an email with instructions for further password recovery.",
"pleasePressButton": "Please press the button bellow to get an email with password recovery code.",
"alreadyHaveCode": "Already have a code"
}

View File

@ -9,9 +9,9 @@ import Body from './ForgotPasswordBody';
export default function ForgotPassword() {
return {
Title: () => <AuthTitle title={messages.forgotPasswordTitle} />,
Title: () => <AuthTitle title={messages.title} />,
Body,
Footer: () => <Button color="lightViolet" label={messages.sendMail} />,
Links: () => <RejectionLink label={messages.contactSupport} />
Footer: () => <Button color="lightViolet" autoFocus label={messages.sendMail} />,
Links: () => <RejectionLink label={messages.alreadyHaveCode} />
};
}

View File

@ -13,27 +13,29 @@ export default class ForgotPasswordBody extends BaseAuthBody {
static panelId = 'forgotPassword';
static hasGoBack = true;
autoFocusField = 'email';
// Если юзер вводил своё мыло во время попытки авторизации, то почему бы его сюда автоматически не подставить?
render() {
const {user} = this.context;
const hasIdentity = user.email || user.username;
const message = hasIdentity ? messages.pleasePressButton : messages.specifyEmail;
return (
<div>
{this.renderErrors()}
<p className={styles.descriptionText}>
<Message {...messages.forgotPasswordMessage} />
<Message {...message} />
</p>
<Input {...this.bindField('email')}
icon="envelope"
color="lightViolet"
required
placeholder={messages.accountEmail}
defaultValue={user.email || user.username || ''}
/>
{hasIdentity ? null : (
<Input {...this.bindField('email')}
icon="envelope"
color="lightViolet"
required
placeholder={messages.accountEmail}
defaultValue={user.email || user.username}
/>
)}
</div>
);
}

View File

@ -1,7 +1,5 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import icons from 'components/ui/icons.scss';
import { Input, Checkbox } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';

View File

@ -0,0 +1,8 @@
{
"title": "Restore password",
"contactSupport": "Contact support",
"messageWasSentTo": "The recovery code was sent to your email {email}.",
"enterCodeBelow": "Please enter the code received into the field below:",
"enterNewPasswordBelow": "Enter and repeat new password below:",
"enterTheCode": "Enter confirmation code"
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Button } from 'components/ui/form';
import RejectionLink from 'components/auth/RejectionLink';
import AuthTitle from 'components/auth/AuthTitle';
import changePassword from 'components/auth/changePassword/ChangePassword.intl.json';
import messages from './RecoverPassword.intl.json';
import Body from './RecoverPasswordBody';
export default function RecoverPassword() {
return {
Title: () => <AuthTitle title={messages.title} />,
Body,
Footer: () => <Button color="lightViolet" label={changePassword.change} />,
Links: () => <RejectionLink label={messages.contactSupport} />
};
}

View File

@ -0,0 +1,78 @@
import React, { PropTypes } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Input } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';
import changePassword from 'components/auth/changePassword/ChangePassword.intl.json';
import styles from './recoverPassword.scss';
import messages from './RecoverPassword.intl.json';
// TODO: activation code field may be decoupled into common component and reused here and in activation panel
// TODO: new password fields may be decoupled into common component and reused here and in changePassword panel
export default class RecoverPasswordBody extends BaseAuthBody {
static displayName = 'RecoverPasswordBody';
static panelId = 'recoverPassword';
static hasGoBack = true;
static propTypes = {
params: PropTypes.shape({
key: PropTypes.string
})
};
autoFocusField = this.props.params && this.props.params.key ? 'newPassword' : 'key';
// Если юзер вводил своё мыло во время попытки авторизации, то почему бы его сюда автоматически не подставить?
render() {
const {user} = this.context;
const {key} = this.props.params;
return (
<div>
{this.renderErrors()}
<p className={styles.descriptionText}>
{user.maskedEmail ? (
<Message {...messages.messageWasSentTo} values={{
email: <b>{user.maskedEmail}</b>
}} />
) : null}
{' '}
<Message {...messages.enterCodeBelow} />
</p>
<Input {...this.bindField('key')}
color="lightViolet"
style={{textAlign: 'center'}}
required
value={key}
readOnly={!!key}
placeholder={messages.enterTheCode}
/>
<p className={styles.descriptionText}>
<Message {...messages.enterNewPasswordBelow} />
</p>
<Input {...this.bindField('newPassword')}
icon="key"
color="lightViolet"
type="password"
required
placeholder={changePassword.newPassword}
/>
<Input {...this.bindField('newRePassword')}
icon="key"
color="lightViolet"
type="password"
required
placeholder={changePassword.newRePassword}
/>
</div>
);
}
}

View File

@ -0,0 +1,8 @@
@import '~components/ui/colors.scss';
.descriptionText {
font-size: 15px;
line-height: 1.4;
padding-bottom: 8px;
color: #aaa;
}

View File

@ -37,7 +37,7 @@ export default class FormModel {
focus(fieldId) {
if (!this.fields[fieldId]) {
throw new Error(`The field with an id ${fieldId} does not exists`);
throw new Error(`Can not focus. The field with an id ${fieldId} does not exists`);
}
this.fields[fieldId].focus();
@ -47,7 +47,7 @@ export default class FormModel {
const field = this.fields[fieldId];
if (!field) {
throw new Error(`The field with an id ${fieldId} does not exists`);
throw new Error(`Can not get value. The field with an id ${fieldId} does not exists`);
}
if (!field.getValue) {

View File

@ -21,6 +21,9 @@ export default class User {
token: '',
username: '',
email: '',
// will contain user's email or masked email
// (e.g. ex**ple@em*il.c**) depending on what information user have already provided
maskedEmail: '',
avatar: '',
lang: '',
goal: null, // the goal with wich user entered site

View File

@ -26,11 +26,11 @@
"components.auth.finish.copy": "Copy",
"components.auth.finish.passCodeToApp": "To complete authorization process, please, provide the following code to {appName}",
"components.auth.finish.waitAppReaction": "Please, wait till your application response",
"components.auth.forgotPassword.accountEmail": "Enter account E-mail",
"components.auth.forgotPassword.contactSupport": "Contact support",
"components.auth.forgotPassword.forgotPasswordMessage": "Specify the registration E-mail address for your account and we will send an email with instructions for further password recovery.",
"components.auth.forgotPassword.forgotPasswordTitle": "Forgot password",
"components.auth.forgotPassword.alreadyHaveCode": "Already have a code",
"components.auth.forgotPassword.pleasePressButton": "Please press the button bellow to get an email with password recovery code.",
"components.auth.forgotPassword.sendMail": "Send mail",
"components.auth.forgotPassword.specifyEmail": "Specify the registration E-mail address for your account and we will send an email with instructions for further password recovery.",
"components.auth.forgotPassword.title": "Forgot password",
"components.auth.login.emailOrUsername": "E-mail or username",
"components.auth.login.loginTitle": "Sign in",
"components.auth.login.next": "Next",
@ -50,6 +50,12 @@
"components.auth.permissions.theAppNeedsAccess1": "This application needs access",
"components.auth.permissions.theAppNeedsAccess2": "to your data",
"components.auth.permissions.youAuthorizedAs": "You authorized as:",
"components.auth.recoverPassword.contactSupport": "Contact support",
"components.auth.recoverPassword.enterCodeBelow": "Please enter the code received into the field below:",
"components.auth.recoverPassword.enterNewPasswordBelow": "Enter and repeat new password below:",
"components.auth.recoverPassword.enterTheCode": "Enter confirmation code",
"components.auth.recoverPassword.messageWasSentTo": "The recovery code was sent to your email {email}.",
"components.auth.recoverPassword.title": "Restore password",
"components.auth.register.acceptRules": "I agree with {link}",
"components.auth.register.accountPassword": "Account password",
"components.auth.register.registerTitle": "Sign Up",
@ -78,38 +84,40 @@
"components.profile.preferencesDescription": "Here you can change the key preferences of your account. Please note that all actions must be confirmed by entering a password.",
"components.profile.twoFactorAuth": "Two factor auth",
"currentAccountEmail": "Current account E-mail address:",
"emailInvalid": "Email is invalid",
"emailIsTempmail": "Tempmail E-mail addresses is not allowed",
"emailNotAvailable": "This email is already registered.",
"emailRequired": "Email is required",
"emailToLong": "Email is too long",
"enterFinalizationCode": "The E-mail change confirmation code was sent to {email}. Please enter the code received into the field below:",
"enterInitializationCode": "The E-mail with an initialization code for E-mail change procedure was sent to {email}. Please enter the code into the field below:",
"enterNewEmail": "Then provide your new E-mail address, that you want to use with this account. You will be mailed with confirmation code.",
"forgotYourPassword": "forgot your password",
"invalidPassword": "You have entered wrong account password.",
"keyNotExists": "The key is incorrect",
"keyRequired": "Please, enter an activation key",
"loginNotExist": "Sorry, Ely doesn't recognise your login.",
"loginRequired": "Please enter email or username",
"logout": "Logout",
"newEmailPlaceholder": "Enter new E-mail",
"newPasswordRequired": "Please enter new password",
"newRePasswordRequired": "Please repeat new password",
"passwordRequired": "Please enter password",
"passwordTooShort": "Your password is too short",
"passwordsDoesNotMatch": "The passwords does not match",
"pleaseEnterPassword": "Please, enter your current password",
"pressButtonToStart": "Press the button below to send a message with the code for E-mail change initialization.",
"rePasswordRequired": "Please retype your password",
"register": "Join",
"rulesAgreementRequired": "You must accept rules in order to create an account",
"sendEmailButton": "Send E-mail",
"suggestResetPassword": "Are you have {link}?",
"title": "Confirm your action",
"usernameInvalid": "Username is invalid",
"usernameRequired": "Username is required",
"usernameTooLong": "Username is too long",
"usernameTooShort": "Username is too short",
"usernameUnavailable": "This username is already taken"
"services.accountNotActivated": "The account is not activated",
"services.emailFrequency": "Please cool down, you are requesting emails too often",
"services.emailInvalid": "Email is invalid",
"services.emailIsTempmail": "Tempmail E-mail addresses is not allowed",
"services.emailNotAvailable": "This email is already registered.",
"services.emailRequired": "Email is required",
"services.emailToLong": "Email is too long",
"services.forgotYourPassword": "forgot your password",
"services.invalidPassword": "You have entered wrong account password.",
"services.keyNotExists": "The key is incorrect",
"services.keyRequired": "Please, enter an activation key",
"services.loginNotExist": "Sorry, Ely doesn't recognise your login.",
"services.loginRequired": "Please enter email or username",
"services.newPasswordRequired": "Please enter new password",
"services.newRePasswordRequired": "Please repeat new password",
"services.passwordRequired": "Please enter password",
"services.passwordTooShort": "Your password is too short",
"services.passwordsDoesNotMatch": "The passwords does not match",
"services.rePasswordRequired": "Please retype your password",
"services.rulesAgreementRequired": "You must accept rules in order to create an account",
"services.suggestResetPassword": "Are you have {link}?",
"services.usernameInvalid": "Username is invalid",
"services.usernameRequired": "Username is required",
"services.usernameTooLong": "Username is too long",
"services.usernameTooShort": "Username is too short",
"services.usernameUnavailable": "This username is already taken",
"title": "Confirm your action"
}

View File

@ -26,11 +26,11 @@
"components.auth.finish.copy": "Скопировать",
"components.auth.finish.passCodeToApp": "Чтобы завершить процесс авторизации, пожалуйста, передай {appName} этот код",
"components.auth.finish.waitAppReaction": "Пожалуйста, дождитесь реакции вашего приложения",
"components.auth.forgotPassword.accountEmail": "Enter account E-mail",
"components.auth.forgotPassword.contactSupport": "Contact support",
"components.auth.forgotPassword.forgotPasswordMessage": "Specify the registration E-mail address for your account and we will send an email with instructions for further password recovery.",
"components.auth.forgotPassword.forgotPasswordTitle": "Forgot password",
"components.auth.forgotPassword.alreadyHaveCode": "Already have a code",
"components.auth.forgotPassword.pleasePressButton": "Please press the button bellow to get an email with password recovery code.",
"components.auth.forgotPassword.sendMail": "Send mail",
"components.auth.forgotPassword.specifyEmail": "Specify the registration E-mail address for your account and we will send an email with instructions for further password recovery.",
"components.auth.forgotPassword.title": "Forgot password",
"components.auth.login.emailOrUsername": "E-mail or username",
"components.auth.login.loginTitle": "Sign in",
"components.auth.login.next": "Next",
@ -50,6 +50,12 @@
"components.auth.permissions.theAppNeedsAccess1": "This application needs access",
"components.auth.permissions.theAppNeedsAccess2": "to your data",
"components.auth.permissions.youAuthorizedAs": "You authorized as:",
"components.auth.recoverPassword.contactSupport": "Contact support",
"components.auth.recoverPassword.enterCodeBelow": "Please enter the code received into the field below:",
"components.auth.recoverPassword.enterNewPasswordBelow": "Enter and repeat new password below:",
"components.auth.recoverPassword.enterTheCode": "Enter confirmation code",
"components.auth.recoverPassword.messageWasSentTo": "The recovery code was sent to your email {email}.",
"components.auth.recoverPassword.title": "Restore password",
"components.auth.register.acceptRules": "I agree with {link}",
"components.auth.register.accountPassword": "Account password",
"components.auth.register.registerTitle": "Sign Up",
@ -78,38 +84,40 @@
"components.profile.preferencesDescription": "Здесь вы можете сменить ключевые параметры вашего аккаунта. Обратите внимание, что для всех действий необходимо подтверждение при помощи ввода пароля.",
"components.profile.twoFactorAuth": "Двухфакторная аутентификация",
"currentAccountEmail": "Current account E-mail address:",
"emailInvalid": "Email is invalid",
"emailIsTempmail": "Tempmail E-mail addresses is not allowed",
"emailNotAvailable": "This email is already registered.",
"emailRequired": "Email is required",
"emailToLong": "Email is too long",
"enterFinalizationCode": "The E-mail change confirmation code was sent to {email}. Please enter the code received into the field below:",
"enterInitializationCode": "The E-mail with an initialization code for E-mail change procedure was sent to {email}. Please enter the code into the field below:",
"enterNewEmail": "Then provide your new E-mail address, that you want to use with this account. You will be mailed with confirmation code.",
"forgotYourPassword": "forgot your password",
"invalidPassword": "You have entered wrong account password.",
"keyNotExists": "The key is incorrect",
"keyRequired": "Please, enter an activation key",
"loginNotExist": "Sorry, Ely doesn't recognise your login.",
"loginRequired": "Please enter email or username",
"logout": "Logout",
"newEmailPlaceholder": "Enter new E-mail",
"newPasswordRequired": "Please enter new password",
"newRePasswordRequired": "Please repeat new password",
"passwordRequired": "Please enter password",
"passwordTooShort": "Your password is too short",
"passwordsDoesNotMatch": "The passwords does not match",
"pleaseEnterPassword": "Please, enter your current password",
"pressButtonToStart": "Press the button below to send a message with the code for E-mail change initialization.",
"rePasswordRequired": "Please retype your password",
"register": "Join",
"rulesAgreementRequired": "You must accept rules in order to create an account",
"sendEmailButton": "Send E-mail",
"suggestResetPassword": "Are you have {link}?",
"title": "Confirm your action",
"usernameInvalid": "Username is invalid",
"usernameRequired": "Username is required",
"usernameTooLong": "Username is too long",
"usernameTooShort": "Username is too short",
"usernameUnavailable": "This username is already taken"
"services.accountNotActivated": "The account is not activated",
"services.emailFrequency": "Please cool down, you are requesting emails too often",
"services.emailInvalid": "Email is invalid",
"services.emailIsTempmail": "Tempmail E-mail addresses is not allowed",
"services.emailNotAvailable": "This email is already registered.",
"services.emailRequired": "Email is required",
"services.emailToLong": "Email is too long",
"services.forgotYourPassword": "forgot your password",
"services.invalidPassword": "You have entered wrong account password.",
"services.keyNotExists": "The key is incorrect",
"services.keyRequired": "Please, enter an activation key",
"services.loginNotExist": "Sorry, Ely doesn't recognise your login.",
"services.loginRequired": "Please enter email or username",
"services.newPasswordRequired": "Please enter new password",
"services.newRePasswordRequired": "Please repeat new password",
"services.passwordRequired": "Please enter password",
"services.passwordTooShort": "Your password is too short",
"services.passwordsDoesNotMatch": "The passwords does not match",
"services.rePasswordRequired": "Please retype your password",
"services.rulesAgreementRequired": "You must accept rules in order to create an account",
"services.suggestResetPassword": "Are you have {link}?",
"services.usernameInvalid": "Username is invalid",
"services.usernameRequired": "Username is required",
"services.usernameTooLong": "Username is too long",
"services.usernameTooShort": "Username is too short",
"services.usernameUnavailable": "This username is already taken",
"title": "Confirm your action"
}

View File

@ -18,6 +18,7 @@ import Activation from 'components/auth/activation/Activation';
import Password from 'components/auth/password/Password';
import ChangePassword from 'components/auth/changePassword/ChangePassword';
import ForgotPassword from 'components/auth/forgotPassword/ForgotPassword';
import RecoverPassword from 'components/auth/recoverPassword/RecoverPassword';
import Finish from 'components/auth/finish/Finish';
import authFlow from 'services/authFlow';
@ -54,6 +55,7 @@ export default function routesFactory(store) {
<Route path="/oauth/finish" component={Finish} {...startAuthFlow} />
<Route path="/change-password" components={new ChangePassword()} {...startAuthFlow} />
<Route path="/forgot-password" components={new ForgotPassword()} {...startAuthFlow} />
<Route path="/recover-password(/:key)" components={new RecoverPassword()} {...startAuthFlow} />
</Route>
<Route path="profile" component={ProfilePage} {...userOnly}>

View File

@ -4,6 +4,7 @@ import RegisterState from './RegisterState';
import LoginState from './LoginState';
import OAuthState from './OAuthState';
import ForgotPasswordState from './ForgotPasswordState';
import RecoverPasswordState from './RecoverPasswordState';
// TODO: a way to unload service (when we are on account page)
@ -81,7 +82,7 @@ export default class AuthFlow {
this.run('setOAuthRequest', {});
}
switch (path) {
switch (path.replace(/(.)\/.+/, '$1')) { // use only first part of an url
case '/oauth':
this.setState(new OAuthState());
break;
@ -90,6 +91,10 @@ export default class AuthFlow {
this.setState(new RegisterState());
break;
case '/recover-password':
this.setState(new RecoverPasswordState());
break;
case '/forgot-password':
this.setState(new ForgotPasswordState());
break;

View File

@ -1,9 +1,26 @@
import AbstractState from './AbstractState';
import LoginState from './LoginState';
import CompleteState from './CompleteState';
import RecoverPasswordState from './RecoverPasswordState';
export default class ForgotPasswordState extends AbstractState {
enter(context) {
context.navigate('/forgot-password');
const {user} = context.getState();
if (user.isGuest) {
if (this.getLogin(context)) {
context.navigate('/forgot-password');
} else {
context.setState(new LoginState());
}
} else {
context.setState(new CompleteState());
}
}
resolve(context) {
context.run('forgotPassword', {login: this.getLogin(context)})
.then(() => context.setState(new RecoverPasswordState()));
}
goBack(context) {
@ -11,6 +28,12 @@ export default class ForgotPasswordState extends AbstractState {
}
reject(context) {
context.navigate('/send-message');
context.setState(new RecoverPasswordState());
}
getLogin(context) {
const {user} = context.getState();
return user.email || user.username;
}
}

View File

@ -7,10 +7,10 @@ export default class PasswordState extends AbstractState {
enter(context) {
const {user} = context.getState();
if (!user.isGuest) {
context.setState(new CompleteState());
} else {
if (user.isGuest) {
context.navigate('/password');
} else {
context.setState(new CompleteState());
}
}

View File

@ -0,0 +1,31 @@
import AbstractState from './AbstractState';
import LoginState from './LoginState';
import CompleteState from './CompleteState';
export default class RecoverPasswordState extends AbstractState {
enter(context) {
const {user, routing} = context.getState();
if (user.isGuest) {
const url = routing.location.pathname.indexOf('/recover-password') === 0
? routing.location.pathname
: '/recover-password';
context.navigate(url);
} else {
context.setState(new CompleteState());
}
}
resolve(context, payload) {
context.run('recoverPassword', payload)
.then(() => context.setState(new CompleteState()));
}
goBack(context) {
context.setState(new LoginState());
}
reject(context) {
context.navigate('/send-message');
}
}

View File

@ -0,0 +1,28 @@
{
"invalidPassword": "You have entered wrong account password.",
"suggestResetPassword": "Are you have {link}?",
"forgotYourPassword": "forgot your password",
"loginRequired": "Please enter email or username",
"loginNotExist": "Sorry, Ely doesn't recognise your login.",
"passwordRequired": "Please enter password",
"newPasswordRequired": "Please enter new password",
"newRePasswordRequired": "Please repeat new password",
"usernameRequired": "Username is required",
"usernameInvalid": "Username is invalid",
"usernameTooShort": "Username is too short",
"usernameTooLong": "Username is too long",
"usernameUnavailable": "This username is already taken",
"emailRequired": "Email is required",
"emailInvalid": "Email is invalid",
"emailToLong": "Email is too long",
"emailIsTempmail": "Tempmail E-mail addresses is not allowed",
"emailNotAvailable": "This email is already registered.",
"rePasswordRequired": "Please retype your password",
"passwordTooShort": "Your password is too short",
"passwordsDoesNotMatch": "The passwords does not match",
"rulesAgreementRequired": "You must accept rules in order to create an account",
"keyRequired": "Please, enter an activation key",
"keyNotExists": "The key is incorrect",
"emailFrequency": "Please cool down, you are requesting emails too often",
"accountNotActivated": "The account is not activated"
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import messages from './errorsDict.messages';
import messages from './errorsDict.intl.json';
export default {
resolve(error) {
@ -65,5 +65,9 @@ const errorsMap = {
'error.newPassword_required': () => <Message {...messages.newPasswordRequired} />,
'error.newRePassword_required': () => <Message {...messages.newRePasswordRequired} />,
'error.newRePassword_does_not_match': () => <Message {...messages.passwordsDoesNotMatch} />
'error.newRePassword_does_not_match': () => <Message {...messages.passwordsDoesNotMatch} />,
'error.account_not_activated': () => <Message {...messages.accountNotActivated} />,
'error.email_frequency': () => <Message {...messages.emailFrequency} />
};

View File

@ -1,123 +0,0 @@
import { defineMessages } from 'react-intl';
export default defineMessages({
invalidPassword: {
id: 'invalidPassword',
defaultMessage: 'You have entered wrong account password.'
},
suggestResetPassword: {
id: 'suggestResetPassword',
defaultMessage: 'Are you have {link}?'
},
forgotYourPassword: {
id: 'forgotYourPassword',
defaultMessage: 'forgot your password'
},
loginRequired: {
id: 'loginRequired',
defaultMessage: 'Please enter email or username'
},
loginNotExist: {
id: 'loginNotExist',
defaultMessage: 'Sorry, Ely doesn\'t recognise your login.'
},
passwordRequired: {
id: 'passwordRequired',
defaultMessage: 'Please enter password'
},
newPasswordRequired: {
id: 'newPasswordRequired',
defaultMessage: 'Please enter new password'
},
newRePasswordRequired: {
id: 'newRePasswordRequired',
defaultMessage: 'Please repeat new password'
},
usernameRequired: {
id: 'usernameRequired',
defaultMessage: 'Username is required'
},
usernameInvalid: {
id: 'usernameInvalid',
defaultMessage: 'Username is invalid'
},
usernameTooShort: {
id: 'usernameTooShort',
defaultMessage: 'Username is too short'
},
usernameTooLong: {
id: 'usernameTooLong',
defaultMessage: 'Username is too long'
},
usernameUnavailable: {
id: 'usernameUnavailable',
defaultMessage: 'This username is already taken'
},
emailRequired: {
id: 'emailRequired',
defaultMessage: 'Email is required'
},
emailInvalid: {
id: 'emailInvalid',
defaultMessage: 'Email is invalid'
},
emailToLong: {
id: 'emailToLong',
defaultMessage: 'Email is too long'
},
emailIsTempmail: {
id: 'emailIsTempmail',
defaultMessage: 'Tempmail E-mail addresses is not allowed'
},
emailNotAvailable: {
id: 'emailNotAvailable',
defaultMessage: 'This email is already registered.'
},
rePasswordRequired: {
id: 'rePasswordRequired',
defaultMessage: 'Please retype your password'
},
passwordTooShort: {
id: 'passwordTooShort',
defaultMessage: 'Your password is too short'
},
passwordsDoesNotMatch: {
id: 'passwordsDoesNotMatch',
defaultMessage: 'The passwords does not match'
},
rulesAgreementRequired: {
id: 'rulesAgreementRequired',
defaultMessage: 'You must accept rules in order to create an account'
},
keyRequired: {
id: 'keyRequired',
defaultMessage: 'Please, enter an activation key'
},
keyNotExists: {
id: 'keyNotExists',
defaultMessage: 'The key is incorrect'
}
});

View File

@ -0,0 +1,139 @@
import ForgotPasswordState from 'services/authFlow/ForgotPasswordState';
import RecoverPasswordState from 'services/authFlow/RecoverPasswordState';
import CompleteState from 'services/authFlow/CompleteState';
import LoginState from 'services/authFlow/LoginState';
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
describe('ForgotPasswordState', () => {
let state;
let context;
let mock;
beforeEach(() => {
state = new ForgotPasswordState();
const data = bootstrap();
context = data.context;
mock = data.mock;
});
afterEach(() => {
mock.verify();
});
describe('#enter', () => {
it('should navigate to /forgot-password if email set', () => {
context.getState.returns({
user: {isGuest: true, email: 'foo@bar.com'}
});
expectNavigate(mock, '/forgot-password');
state.enter(context);
});
it('should navigate to /forgot-password if username set', () => {
context.getState.returns({
user: {isGuest: true, username: 'foobar'}
});
expectNavigate(mock, '/forgot-password');
state.enter(context);
});
it('should transition to complete if not guest', () => {
context.getState.returns({
user: {isGuest: false}
});
expectState(mock, CompleteState);
state.enter(context);
});
it('should transition to login if no identity', () => {
context.getState.returns({
user: {isGuest: true}
});
expectState(mock, LoginState);
state.enter(context);
});
});
describe('#resolve', () => {
it('should call forgotPassword with email', () => {
const expectedLogin = 'foo@bar.com';
context.getState.returns({
user: {
email: expectedLogin
}
});
expectRun(
mock,
'forgotPassword',
sinon.match({
login: expectedLogin
})
).returns({then() {}});
state.resolve(context, {});
});
it('should call forgotPassword with username', () => {
const expectedLogin = 'foobar';
context.getState.returns({
user: {
username: expectedLogin
}
});
expectRun(
mock,
'forgotPassword',
sinon.match({
login: expectedLogin
})
).returns({then() {}});
state.resolve(context, {});
});
it('should transition to recoverPassword state on success', () => {
const promise = Promise.resolve();
const expectedLogin = 'foo@bar.com';
context.getState.returns({
user: {
email: expectedLogin
}
});
mock.expects('run').returns(promise);
expectState(mock, RecoverPasswordState);
state.resolve(context, {});
return promise;
});
});
describe('#reject', () => {
it('should navigate to /send-message', () => {
expectState(mock, RecoverPasswordState);
state.reject(context);
});
});
describe('#goBack', () => {
it('should transition to login state', () => {
expectState(mock, LoginState);
state.goBack(context);
});
});
});

View File

@ -45,7 +45,7 @@ describe('PasswordState', () => {
});
describe('#resolve', () => {
(() => {
(function() {
const expectedLogin = 'login';
const expectedPassword = 'password';
@ -78,7 +78,7 @@ describe('PasswordState', () => {
email: expectedLogin,
username: expectedLogin
});
});
}());
it('should transition to complete state on successfull login', () => {
const promise = Promise.resolve();

View File

@ -0,0 +1,104 @@
import RecoverPasswordState from 'services/authFlow/RecoverPasswordState';
import CompleteState from 'services/authFlow/CompleteState';
import LoginState from 'services/authFlow/LoginState';
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
describe('RecoverPasswordState', () => {
let state;
let context;
let mock;
beforeEach(() => {
state = new RecoverPasswordState();
const data = bootstrap();
context = data.context;
mock = data.mock;
});
afterEach(() => {
mock.verify();
});
describe('#enter', () => {
it('should navigate to /recover-password', () => {
const expectedPath = '/recover-password';
context.getState.returns({
user: {isGuest: true},
routing: {
location: {pathname: expectedPath}
}
});
expectNavigate(mock, expectedPath);
state.enter(context);
});
it('should navigate to /recover-password/key', () => {
const expectedPath = '/recover-password/sasx5AS4d61';
context.getState.returns({
user: {isGuest: true},
routing: {
location: {pathname: expectedPath}
}
});
expectNavigate(mock, expectedPath);
state.enter(context);
});
it('should transition to complete if not guest', () => {
context.getState.returns({
user: {isGuest: false}
});
expectState(mock, CompleteState);
state.enter(context);
});
});
describe('#resolve', () => {
it('should call recoverPassword with provided payload', () => {
const expectedPayload = {key: 123, newPassword: '123', newRePassword: '123'};
expectRun(
mock,
'recoverPassword',
sinon.match(expectedPayload)
).returns({then() {}});
state.resolve(context, expectedPayload);
});
it('should transition to complete state on success', () => {
const promise = Promise.resolve();
mock.expects('run').returns(promise);
expectState(mock, CompleteState);
state.resolve(context, {});
return promise;
});
});
describe('#reject', () => {
it('should navigate to /send-message', () => {
expectNavigate(mock, '/send-message');
state.reject(context);
});
});
describe('#goBack', () => {
it('should transition to login state', () => {
expectState(mock, LoginState);
state.goBack(context);
});
});
});