mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-06-29 03:33:22 +05:30
#305: add mfa step during auth
This commit is contained in:
parent
d0a356050f
commit
5a16fe26ae
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
flow-typed
|
|
@ -213,7 +213,7 @@
|
|||
"react/no-direct-mutation-state": "warn",
|
||||
"react/require-render-return": "warn",
|
||||
"react/no-is-mounted": "warn",
|
||||
"react/no-multi-comp": "warn",
|
||||
"react/no-multi-comp": "off",
|
||||
"react/no-string-refs": "warn",
|
||||
"react/no-unknown-property": "warn",
|
||||
"react/prefer-es6-class": "warn",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
[include]
|
||||
|
||||
[libs]
|
||||
./flow-typed
|
||||
|
||||
[options]
|
||||
module.system.node.resolve_dirname=node_modules
|
||||
|
|
29
flow-typed/Promise.js
vendored
Normal file
29
flow-typed/Promise.js
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* This is a copypasted declaration from
|
||||
* https://github.com/facebook/flow/blob/master/lib/core.js
|
||||
* with addition of finally method
|
||||
*/
|
||||
declare class Promise<+R> {
|
||||
constructor(callback: (
|
||||
resolve: (result: Promise<R> | R) => void,
|
||||
reject: (error: any) => void
|
||||
) => mixed): void;
|
||||
|
||||
then<U>(
|
||||
onFulfill?: (value: R) => Promise<U> | U,
|
||||
onReject?: (error: any) => Promise<U> | U
|
||||
): Promise<U>;
|
||||
|
||||
catch<U>(
|
||||
onReject?: (error: any) => Promise<U> | U
|
||||
): Promise<R | U>;
|
||||
|
||||
static resolve<T>(object: Promise<T> | T): Promise<T>;
|
||||
static reject<T>(error?: any): Promise<T>;
|
||||
static all<Elem, T:Iterable<Elem>>(promises: T): Promise<$TupleMap<T, typeof $await>>;
|
||||
static race<T, Elem: Promise<T> | T>(promises: Array<Elem>): Promise<T>;
|
||||
|
||||
finally<T>(
|
||||
onSettled?: ?(value: any) => Promise<T> | T
|
||||
): Promise<T>;
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
import { Panel, PanelBody, PanelFooter, PanelHeader } from 'components/ui/Panel';
|
||||
import { getLogin } from 'components/auth/reducer';
|
||||
import { Form } from 'components/ui/form';
|
||||
import MeasureHeight from 'components/MeasureHeight';
|
||||
import { helpLinks as helpLinksStyles } from 'components/auth/helpLinks.scss';
|
||||
|
@ -30,7 +32,7 @@ const changeContextSpringConfig = {stiffness: 500, damping: 20, precision: 0.5};
|
|||
* (e.g. the panel with lower index will slide from left side, and with greater from right side)
|
||||
*/
|
||||
const contexts = [
|
||||
['login', 'password', 'forgotPassword', 'recoverPassword'],
|
||||
['login', 'password', 'mfa', 'forgotPassword', 'recoverPassword'],
|
||||
['register', 'activation', 'resendActivation'],
|
||||
['acceptRules'],
|
||||
['chooseAccount', 'permissions']
|
||||
|
@ -459,7 +461,7 @@ class PanelTransition extends Component {
|
|||
}
|
||||
|
||||
export default connect((state) => {
|
||||
const {login} = state.auth;
|
||||
const login = getLogin(state);
|
||||
let user = {
|
||||
...state.user
|
||||
};
|
||||
|
|
|
@ -4,14 +4,10 @@ To add new panel you need to:
|
|||
|
||||
* create panel component at `components/auth/[panelId]`
|
||||
* add new context in `components/auth/PanelTransition`
|
||||
* connect component to `routes`
|
||||
* connect component to router in `pages/auth/AuthPage`
|
||||
* add new state to `services/authFlow` and coresponding test to `tests/services/authFlow`
|
||||
* connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and `services/authFlow/AuthFlow.functional.test` (the last one for some complex flow)
|
||||
* add new actions to `components/auth/actions` and api endpoints to `services/api`
|
||||
* whatever else you need
|
||||
|
||||
Commit id with example: f4d315c
|
||||
|
||||
# TODO
|
||||
|
||||
This flow must be simplified
|
||||
Commit id with example implementation: f4d315c
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import { browserHistory } from 'services/history';
|
||||
|
||||
import logger from 'services/logger';
|
||||
|
@ -23,7 +24,7 @@ export { authenticate, logoutAll as logout } from 'components/accounts/actions';
|
|||
*
|
||||
* @return {object} - action definition
|
||||
*/
|
||||
export function goBack(fallbackUrl = null) {
|
||||
export function goBack(fallbackUrl?: ?string = null) {
|
||||
if (history.canGoBack()) {
|
||||
browserHistory.goBack();
|
||||
} else if (fallbackUrl) {
|
||||
|
@ -35,7 +36,7 @@ export function goBack(fallbackUrl = null) {
|
|||
};
|
||||
}
|
||||
|
||||
export function redirect(url) {
|
||||
export function redirect(url: string) {
|
||||
loader.show();
|
||||
|
||||
return () => new Promise(() => {
|
||||
|
@ -45,14 +46,25 @@ export function redirect(url) {
|
|||
});
|
||||
}
|
||||
|
||||
export function login({login = '', password = '', rememberMe = false}) {
|
||||
export function login({
|
||||
login = '',
|
||||
password = '',
|
||||
totp,
|
||||
rememberMe = false
|
||||
}: {
|
||||
login: string,
|
||||
password?: string,
|
||||
totp?: string,
|
||||
rememberMe?: bool
|
||||
}) {
|
||||
const PASSWORD_REQUIRED = 'error.password_required';
|
||||
const LOGIN_REQUIRED = 'error.login_required';
|
||||
const ACTIVATION_REQUIRED = 'error.account_not_activated';
|
||||
const TOTP_REQUIRED = 'error.totp_required';
|
||||
|
||||
return wrapInLoader((dispatch) =>
|
||||
authentication.login(
|
||||
{login, password, rememberMe}
|
||||
{login, password, totp, rememberMe}
|
||||
)
|
||||
.then(authHandler(dispatch))
|
||||
.catch((resp) => {
|
||||
|
@ -61,6 +73,12 @@ export function login({login = '', password = '', rememberMe = false}) {
|
|||
return dispatch(setLogin(login));
|
||||
} else if (resp.errors.login === ACTIVATION_REQUIRED) {
|
||||
return dispatch(needActivation());
|
||||
} else if (resp.errors.totp === TOTP_REQUIRED) {
|
||||
return dispatch(requestTotp({
|
||||
login,
|
||||
password,
|
||||
rememberMe
|
||||
}));
|
||||
} else if (resp.errors.login === LOGIN_REQUIRED && password) {
|
||||
logger.warn('No login on password panel');
|
||||
|
||||
|
@ -83,6 +101,9 @@ export function acceptRules() {
|
|||
export function forgotPassword({
|
||||
login = '',
|
||||
captcha = ''
|
||||
}: {
|
||||
login: string,
|
||||
captcha: string
|
||||
}) {
|
||||
return wrapInLoader((dispatch, getState) =>
|
||||
authentication.forgotPassword({login, captcha})
|
||||
|
@ -97,6 +118,10 @@ export function recoverPassword({
|
|||
key = '',
|
||||
newPassword = '',
|
||||
newRePassword = ''
|
||||
}: {
|
||||
key: string,
|
||||
newPassword: string,
|
||||
newRePassword: string
|
||||
}) {
|
||||
return wrapInLoader((dispatch) =>
|
||||
authentication.recoverPassword({key, newPassword, newRePassword})
|
||||
|
@ -112,6 +137,13 @@ export function register({
|
|||
rePassword = '',
|
||||
captcha = '',
|
||||
rulesAgreement = false
|
||||
}: {
|
||||
email: string,
|
||||
username: string,
|
||||
password: string,
|
||||
rePassword: string,
|
||||
captcha: string,
|
||||
rulesAgreement: bool
|
||||
}) {
|
||||
return wrapInLoader((dispatch, getState) =>
|
||||
signup.register({
|
||||
|
@ -134,7 +166,7 @@ export function register({
|
|||
);
|
||||
}
|
||||
|
||||
export function activate({key = ''}) {
|
||||
export function activate({key = ''}: {key: string}) {
|
||||
return wrapInLoader((dispatch) =>
|
||||
signup.activate({key})
|
||||
.then(authHandler(dispatch))
|
||||
|
@ -142,7 +174,13 @@ export function activate({key = ''}) {
|
|||
);
|
||||
}
|
||||
|
||||
export function resendActivation({email = '', captcha}) {
|
||||
export function resendActivation({
|
||||
email = '',
|
||||
captcha
|
||||
}: {
|
||||
email: string,
|
||||
captcha: string
|
||||
}) {
|
||||
return wrapInLoader((dispatch) =>
|
||||
signup.resendActivation({email, captcha})
|
||||
.then((resp) => {
|
||||
|
@ -160,16 +198,43 @@ export function contactUs() {
|
|||
return createPopup(ContactForm);
|
||||
}
|
||||
|
||||
export const SET_LOGIN = 'auth:setLogin';
|
||||
export function setLogin(login) {
|
||||
export const SET_CREDENTIALS = 'auth:setCredentials';
|
||||
/**
|
||||
* Sets login in credentials state
|
||||
*
|
||||
* Resets the state, when `null` is passed
|
||||
*
|
||||
* @param {string|null} login
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
export function setLogin(login: ?string) {
|
||||
return {
|
||||
type: SET_LOGIN,
|
||||
payload: login
|
||||
type: SET_CREDENTIALS,
|
||||
payload: login ? {
|
||||
login
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
function requestTotp({login, password, rememberMe}: {
|
||||
login: string,
|
||||
password: string,
|
||||
rememberMe: bool
|
||||
}) {
|
||||
return {
|
||||
type: SET_CREDENTIALS,
|
||||
payload: {
|
||||
login,
|
||||
password,
|
||||
rememberMe,
|
||||
isTotpRequired: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const SET_SWITCHER = 'auth:setAccountSwitcher';
|
||||
export function setAccountSwitcher(isOn) {
|
||||
export function setAccountSwitcher(isOn: bool) {
|
||||
return {
|
||||
type: SET_SWITCHER,
|
||||
payload: isOn
|
||||
|
@ -177,7 +242,7 @@ export function setAccountSwitcher(isOn) {
|
|||
}
|
||||
|
||||
export const ERROR = 'auth:error';
|
||||
export function setErrors(errors) {
|
||||
export function setErrors(errors: ?{[key: string]: string}) {
|
||||
return {
|
||||
type: ERROR,
|
||||
payload: errors,
|
||||
|
@ -213,7 +278,16 @@ const KNOWN_SCOPES = [
|
|||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
export function oAuthValidate(oauthData) {
|
||||
export function oAuthValidate(oauthData: {
|
||||
clientId: string,
|
||||
redirectUrl: string,
|
||||
responseType: string,
|
||||
description: string,
|
||||
scope: string,
|
||||
prompt: 'none'|'consent'|'select_account',
|
||||
loginHint?: string,
|
||||
state?: string
|
||||
}) {
|
||||
// TODO: move to oAuth actions?
|
||||
// test request: /oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session&description=foo
|
||||
return wrapInLoader((dispatch) =>
|
||||
|
@ -255,7 +329,7 @@ export function oAuthValidate(oauthData) {
|
|||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
export function oAuthComplete(params = {}) {
|
||||
export function oAuthComplete(params: {accept?: bool} = {}) {
|
||||
return wrapInLoader((dispatch, getState) =>
|
||||
oauth.complete(getState().auth.oauth, params)
|
||||
.then((resp) => {
|
||||
|
@ -297,7 +371,15 @@ function handleOauthParamsValidation(resp = {}) {
|
|||
}
|
||||
|
||||
export const SET_CLIENT = 'set_client';
|
||||
export function setClient({id, name, description}) {
|
||||
export function setClient({
|
||||
id,
|
||||
name,
|
||||
description
|
||||
}: {
|
||||
id: string,
|
||||
name: string,
|
||||
description: string
|
||||
}) {
|
||||
return {
|
||||
type: SET_CLIENT,
|
||||
payload: {id, name, description}
|
||||
|
@ -305,7 +387,7 @@ export function setClient({id, name, description}) {
|
|||
}
|
||||
|
||||
export function resetOAuth() {
|
||||
return (dispatch) => {
|
||||
return (dispatch: (Function|Object) => void) => {
|
||||
localStorage.removeItem('oauthData');
|
||||
dispatch(setOAuthRequest({}));
|
||||
};
|
||||
|
@ -317,14 +399,22 @@ export function resetOAuth() {
|
|||
* @return {function}
|
||||
*/
|
||||
export function resetAuth() {
|
||||
return (dispatch) => {
|
||||
return (dispatch: (Function|Object) => void) => {
|
||||
dispatch(setLogin(null));
|
||||
dispatch(resetOAuth({}));
|
||||
dispatch(resetOAuth());
|
||||
};
|
||||
}
|
||||
|
||||
export const SET_OAUTH = 'set_oauth';
|
||||
export function setOAuthRequest(oauth) {
|
||||
export function setOAuthRequest(oauth: {
|
||||
client_id?: string,
|
||||
redirect_uri?: string,
|
||||
response_type?: string,
|
||||
scope?: string,
|
||||
prompt?: string,
|
||||
loginHint?: string,
|
||||
state?: string
|
||||
}) {
|
||||
return {
|
||||
type: SET_OAUTH,
|
||||
payload: {
|
||||
|
@ -340,7 +430,11 @@ export function setOAuthRequest(oauth) {
|
|||
}
|
||||
|
||||
export const SET_OAUTH_RESULT = 'set_oauth_result';
|
||||
export function setOAuthCode(oauth) {
|
||||
export function setOAuthCode(oauth: {
|
||||
success: bool,
|
||||
code: string,
|
||||
displayCode: bool
|
||||
}) {
|
||||
return {
|
||||
type: SET_OAUTH_RESULT,
|
||||
payload: {
|
||||
|
@ -359,7 +453,7 @@ export function requirePermissionsAccept() {
|
|||
}
|
||||
|
||||
export const SET_SCOPES = 'set_scopes';
|
||||
export function setScopes(scopes) {
|
||||
export function setScopes(scopes: Array<string>) {
|
||||
if (!(scopes instanceof Array)) {
|
||||
throw new Error('Scopes must be array');
|
||||
}
|
||||
|
@ -372,7 +466,7 @@ export function setScopes(scopes) {
|
|||
|
||||
|
||||
export const SET_LOADING_STATE = 'set_loading_state';
|
||||
export function setLoadingState(isLoading) {
|
||||
export function setLoadingState(isLoading: bool) {
|
||||
return {
|
||||
type: SET_LOADING_STATE,
|
||||
payload: isLoading
|
||||
|
@ -380,7 +474,7 @@ export function setLoadingState(isLoading) {
|
|||
}
|
||||
|
||||
function wrapInLoader(fn) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch: (Function|Object) => void, getState: Object) => {
|
||||
dispatch(setLoadingState(true));
|
||||
const endLoading = () => dispatch(setLoadingState(false));
|
||||
|
||||
|
@ -414,14 +508,15 @@ function authHandler(dispatch) {
|
|||
});
|
||||
}
|
||||
|
||||
function validationErrorsHandler(dispatch, repeatUrl) {
|
||||
function validationErrorsHandler(dispatch: (Function|Object) => void, repeatUrl?: string) {
|
||||
return (resp) => {
|
||||
if (resp.errors) {
|
||||
const firstError = Object.keys(resp.errors)[0];
|
||||
const error = {
|
||||
type: resp.errors[firstError],
|
||||
payload: {
|
||||
isGuest: true
|
||||
isGuest: true,
|
||||
repeatUrl: ''
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -432,9 +527,7 @@ function validationErrorsHandler(dispatch, repeatUrl) {
|
|||
|
||||
if (['error.key_not_exists', 'error.key_expire'].includes(error.type) && repeatUrl) {
|
||||
// TODO: this should be formatted on backend
|
||||
Object.assign(error.payload, {
|
||||
repeatUrl
|
||||
});
|
||||
error.payload.repeatUrl = repeatUrl;
|
||||
}
|
||||
|
||||
resp.errors[firstError] = error;
|
||||
|
|
|
@ -3,7 +3,8 @@ import React from 'react';
|
|||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import { Input, Captcha } from 'components/ui/form';
|
||||
import icons from 'components/ui/icons.scss';
|
||||
import { getLogin } from 'components/auth/reducer';
|
||||
import { PanelIcon } from 'components/ui/Panel';
|
||||
import BaseAuthBody from 'components/auth/BaseAuthBody';
|
||||
|
||||
import styles from './forgotPassword.scss';
|
||||
|
@ -28,9 +29,7 @@ export default class ForgotPasswordBody extends BaseAuthBody {
|
|||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<div className={styles.bigIcon}>
|
||||
<span className={icons.lock} />
|
||||
</div>
|
||||
<PanelIcon icon="lock" />
|
||||
|
||||
{isLoginEditShown ? (
|
||||
<div>
|
||||
|
@ -73,9 +72,10 @@ export default class ForgotPasswordBody extends BaseAuthBody {
|
|||
}
|
||||
|
||||
getLogin() {
|
||||
const { user, auth } = this.context;
|
||||
const login = getLogin(this.context);
|
||||
const { user } = this.context;
|
||||
|
||||
return auth.login || user.username || user.email || '';
|
||||
return login || user.username || user.email || '';
|
||||
}
|
||||
|
||||
onClickEdit = () => {
|
||||
|
|
|
@ -7,14 +7,6 @@
|
|||
color: #aaa;
|
||||
}
|
||||
|
||||
// TODO: вынести иконки такого типа в какую-то внешнюю структуру?
|
||||
.bigIcon {
|
||||
color: #ccc;
|
||||
font-size: 100px;
|
||||
line-height: 1;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.login {
|
||||
composes: email from 'components/auth/password/password.scss';
|
||||
}
|
||||
|
|
4
src/components/auth/mfa/Mfa.intl.json
Normal file
4
src/components/auth/mfa/Mfa.intl.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"enterTotp": "Enter code",
|
||||
"description": "In order to sign in this account, you need to enter a one-time password from mobile application"
|
||||
}
|
15
src/components/auth/mfa/Mfa.js
Normal file
15
src/components/auth/mfa/Mfa.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
// @flow
|
||||
import factory from 'components/auth/factory';
|
||||
|
||||
import Body from './MfaBody';
|
||||
import messages from './Mfa.intl.json';
|
||||
import passwordMessages from '../password/Password.intl.json';
|
||||
|
||||
export default factory({
|
||||
title: messages.enterTotp,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'green',
|
||||
label: passwordMessages.signInButton
|
||||
}
|
||||
});
|
39
src/components/auth/mfa/MfaBody.js
Normal file
39
src/components/auth/mfa/MfaBody.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import { PanelIcon } from 'components/ui/Panel';
|
||||
import { Input } from 'components/ui/form';
|
||||
import BaseAuthBody from 'components/auth/BaseAuthBody';
|
||||
|
||||
import styles from './mfa.scss';
|
||||
import messages from './Mfa.intl.json';
|
||||
|
||||
export default class MfaBody extends BaseAuthBody {
|
||||
static panelId = 'mfa';
|
||||
static hasGoBack = true;
|
||||
|
||||
autoFocusField = 'totp';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<PanelIcon icon="lock" />
|
||||
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.description} />
|
||||
</p>
|
||||
|
||||
<Input {...this.bindField('totp')}
|
||||
icon="key"
|
||||
required
|
||||
placeholder={messages.enterTotp}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
6
src/components/auth/mfa/mfa.scss
Normal file
6
src/components/auth/mfa/mfa.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
.descriptionText {
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 8px;
|
||||
color: #aaa;
|
||||
}
|
|
@ -8,12 +8,12 @@ import {
|
|||
SET_SCOPES,
|
||||
SET_LOADING_STATE,
|
||||
REQUIRE_PERMISSIONS_ACCEPT,
|
||||
SET_LOGIN,
|
||||
SET_CREDENTIALS,
|
||||
SET_SWITCHER
|
||||
} from './actions';
|
||||
|
||||
export default combineReducers({
|
||||
login,
|
||||
credentials,
|
||||
error,
|
||||
isLoading,
|
||||
isSwitcherEnabled,
|
||||
|
@ -39,21 +39,31 @@ function error(
|
|||
}
|
||||
}
|
||||
|
||||
function login(
|
||||
state = null,
|
||||
{type, payload = null}
|
||||
) {
|
||||
switch (type) {
|
||||
case SET_LOGIN:
|
||||
if (payload !== null && typeof payload !== 'string') {
|
||||
throw new Error('Expected payload with login string or null');
|
||||
}
|
||||
|
||||
return payload;
|
||||
|
||||
default:
|
||||
return state;
|
||||
function credentials(
|
||||
state = {},
|
||||
{type, payload}: {
|
||||
type: string,
|
||||
payload: ?{
|
||||
login?: string,
|
||||
password?: string,
|
||||
rememberMe?: bool,
|
||||
isTotpRequired?: bool
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (type === SET_CREDENTIALS) {
|
||||
if (payload
|
||||
&& typeof payload === 'object'
|
||||
) {
|
||||
return {
|
||||
...payload
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function isSwitcherEnabled(
|
||||
|
@ -150,3 +160,16 @@ function scopes(
|
|||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLogin(state: Object): ?string {
|
||||
return state.auth.credentials.login || null;
|
||||
}
|
||||
|
||||
export function getCredentials(state: Object): {
|
||||
login?: string,
|
||||
password?: string,
|
||||
rememberMe?: bool,
|
||||
isTotpRequired?: bool
|
||||
} {
|
||||
return state.auth.credentials;
|
||||
}
|
||||
|
|
|
@ -2,16 +2,16 @@ import expect from 'unexpected';
|
|||
|
||||
import auth from 'components/auth/reducer';
|
||||
import {
|
||||
setLogin, SET_LOGIN,
|
||||
setLogin, SET_CREDENTIALS,
|
||||
setAccountSwitcher, SET_SWITCHER
|
||||
} from 'components/auth/actions';
|
||||
|
||||
describe('components/auth/reducer', () => {
|
||||
describe(SET_LOGIN, () => {
|
||||
describe(SET_CREDENTIALS, () => {
|
||||
it('should set login', () => {
|
||||
const expectedLogin = 'foo';
|
||||
|
||||
expect(auth(undefined, setLogin(expectedLogin)), 'to satisfy', {
|
||||
expect(auth(undefined, setLogin(expectedLogin)).credentials, 'to satisfy', {
|
||||
login: expectedLogin
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -7,8 +8,12 @@ import { omit } from 'functions';
|
|||
import styles from './panel.scss';
|
||||
import icons from './icons.scss';
|
||||
|
||||
export function Panel(props) {
|
||||
var { title, icon } = props;
|
||||
export function Panel(props: {
|
||||
title: string,
|
||||
icon: string,
|
||||
children: *
|
||||
}) {
|
||||
let { title, icon } = props;
|
||||
|
||||
if (icon) {
|
||||
icon = (
|
||||
|
@ -36,7 +41,9 @@ export function Panel(props) {
|
|||
);
|
||||
}
|
||||
|
||||
export function PanelHeader(props) {
|
||||
export function PanelHeader(props: {
|
||||
children: *
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.header} {...props}>
|
||||
{props.children}
|
||||
|
@ -44,7 +51,9 @@ export function PanelHeader(props) {
|
|||
);
|
||||
}
|
||||
|
||||
export function PanelBody(props) {
|
||||
export function PanelBody(props: {
|
||||
children: *
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.body} {...props}>
|
||||
{props.children}
|
||||
|
@ -52,7 +61,9 @@ export function PanelBody(props) {
|
|||
);
|
||||
}
|
||||
|
||||
export function PanelFooter(props) {
|
||||
export function PanelFooter(props: {
|
||||
children: *
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.footer} {...props}>
|
||||
{props.children}
|
||||
|
@ -61,11 +72,16 @@ export function PanelFooter(props) {
|
|||
}
|
||||
|
||||
export class PanelBodyHeader extends Component {
|
||||
static displayName = 'PanelBodyHeader';
|
||||
props: {
|
||||
type: 'default'|'error',
|
||||
onClose: Function,
|
||||
children: *
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
type: PropTypes.oneOf(['default', 'error']),
|
||||
onClose: PropTypes.func
|
||||
state: {
|
||||
isClosed: bool
|
||||
} = {
|
||||
isClosed: false
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -79,18 +95,23 @@ export class PanelBodyHeader extends Component {
|
|||
}
|
||||
|
||||
const className = classNames(styles[`${type}BodyHeader`], {
|
||||
[styles.isClosed]: this.state && this.state.isClosed
|
||||
[styles.isClosed]: this.state.isClosed
|
||||
});
|
||||
|
||||
const extraProps = omit(this.props, [
|
||||
'type',
|
||||
'onClose'
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={className} {...omit(this.props, Object.keys(PanelBodyHeader.propTypes))}>
|
||||
<div className={className} {...extraProps}>
|
||||
{close}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onClose = (event) => {
|
||||
onClose = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({isClosed: true});
|
||||
|
@ -98,3 +119,11 @@ export class PanelBodyHeader extends Component {
|
|||
this.props.onClose();
|
||||
};
|
||||
}
|
||||
|
||||
export function PanelIcon({icon}: {icon: string}) {
|
||||
return (
|
||||
<div className={styles.panelIcon}>
|
||||
<span className={icons[icon]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -129,3 +129,10 @@ $bodyTopBottomPadding: 15px;
|
|||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panelIcon {
|
||||
color: #ccc;
|
||||
font-size: 100px;
|
||||
line-height: 1;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import Password from 'components/auth/password/Password';
|
|||
import AcceptRules from 'components/auth/acceptRules/AcceptRules';
|
||||
import ForgotPassword from 'components/auth/forgotPassword/ForgotPassword';
|
||||
import RecoverPassword from 'components/auth/recoverPassword/RecoverPassword';
|
||||
import Mfa from 'components/auth/mfa/Mfa';
|
||||
import Finish from 'components/auth/finish/Finish';
|
||||
|
||||
import styles from './auth.scss';
|
||||
|
@ -46,6 +47,7 @@ class AuthPage extends Component {
|
|||
<div className={styles.content}>
|
||||
<Switch>
|
||||
<Route path="/login" render={renderPanelTransition(Login)} />
|
||||
<Route path="/mfa" render={renderPanelTransition(Mfa)} />
|
||||
<Route path="/password" render={renderPanelTransition(Password)} />
|
||||
<Route path="/register" render={renderPanelTransition(Register)} />
|
||||
<Route path="/activation/:key?" render={renderPanelTransition(Activation)} />
|
||||
|
@ -72,6 +74,7 @@ class AuthPage extends Component {
|
|||
|
||||
function renderPanelTransition(factory) {
|
||||
const {Title, Body, Footer, Links} = factory();
|
||||
|
||||
return (props) => (
|
||||
<PanelTransition
|
||||
key="panel-transition"
|
||||
|
|
|
@ -1,35 +1,54 @@
|
|||
// @flow
|
||||
import request from 'services/request';
|
||||
import accounts from 'services/api/accounts';
|
||||
|
||||
const authentication = {
|
||||
login({
|
||||
login = '',
|
||||
password = '',
|
||||
login,
|
||||
password,
|
||||
totp,
|
||||
rememberMe = false
|
||||
}: {
|
||||
login: string,
|
||||
password?: string,
|
||||
totp?: string,
|
||||
rememberMe: bool
|
||||
}) {
|
||||
return request.post(
|
||||
'/api/authentication/login',
|
||||
{login, password, rememberMe},
|
||||
{login, password, token: totp, rememberMe},
|
||||
{token: null}
|
||||
);
|
||||
).catch((resp) => {
|
||||
if (resp && resp.errors && resp.errors.token) {
|
||||
resp.errors.totp = resp.errors.token.replace('token', 'totp');
|
||||
delete resp.errors.token;
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {object} [options.token] - an optional token to overwrite headers
|
||||
* @param {string} [options.token] - an optional token to overwrite headers
|
||||
* in middleware and disable token auto-refresh
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
logout(options = {}) {
|
||||
logout(options: {
|
||||
token?: string
|
||||
} = {}) {
|
||||
return request.post('/api/authentication/logout', {}, {
|
||||
token: options.token
|
||||
});
|
||||
},
|
||||
|
||||
forgotPassword({
|
||||
login = '',
|
||||
captcha = ''
|
||||
login,
|
||||
captcha
|
||||
}: {
|
||||
login: string,
|
||||
captcha: string
|
||||
}) {
|
||||
return request.post(
|
||||
'/api/authentication/forgot-password',
|
||||
|
@ -39,9 +58,13 @@ const authentication = {
|
|||
},
|
||||
|
||||
recoverPassword({
|
||||
key = '',
|
||||
newPassword = '',
|
||||
newRePassword = ''
|
||||
key,
|
||||
newPassword,
|
||||
newRePassword
|
||||
}: {
|
||||
key: string,
|
||||
newPassword: string,
|
||||
newRePassword: string
|
||||
}) {
|
||||
return request.post(
|
||||
'/api/authentication/recover-password',
|
||||
|
@ -61,7 +84,10 @@ const authentication = {
|
|||
* if it was refreshed. As a side effect the response
|
||||
* will have a `user` field with current user data
|
||||
*/
|
||||
validateToken({token, refreshToken}) {
|
||||
validateToken({token, refreshToken}: {
|
||||
token: string,
|
||||
refreshToken: string
|
||||
}) {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof token !== 'string') {
|
||||
throw new Error('token must be a string');
|
||||
|
@ -91,12 +117,12 @@ const authentication = {
|
|||
*
|
||||
* @return {Promise} - resolves to {token}
|
||||
*/
|
||||
requestToken(refreshToken) {
|
||||
requestToken(refreshToken: string): Promise<{token: string}> {
|
||||
return request.post(
|
||||
'/api/authentication/refresh-token',
|
||||
{refresh_token: refreshToken}, // eslint-disable-line
|
||||
{token: null}
|
||||
).then((resp) => ({
|
||||
).then((resp: {access_token: string}) => ({
|
||||
token: resp.access_token
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ describe('authentication api', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
sinon.stub(request, 'post').named('request.post');
|
||||
|
||||
request.post.returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
import type { AuthContext } from 'services/authFlow';
|
||||
|
||||
export default class AbstractState {
|
||||
resolve(context: AuthContext, payload: Object) {}
|
||||
goBack(context: AuthContext) {
|
||||
resolve(context: AuthContext, payload: Object): void {}
|
||||
goBack(context: AuthContext): void {
|
||||
throw new Error('There is no way back');
|
||||
}
|
||||
reject(context: AuthContext, payload: Object) {}
|
||||
enter(context: AuthContext) {}
|
||||
leave(context: AuthContext) {}
|
||||
reject(context: AuthContext, payload: Object): void {}
|
||||
enter(context: AuthContext): void {}
|
||||
leave(context: AuthContext): void {}
|
||||
}
|
||||
|
|
|
@ -45,12 +45,16 @@ describe('AuthFlow.functional', () => {
|
|||
|
||||
describe('guest', () => {
|
||||
beforeEach(() => {
|
||||
state.user = {
|
||||
isGuest: true
|
||||
};
|
||||
state.auth = {
|
||||
login: null
|
||||
};
|
||||
Object.assign(state, {
|
||||
user: {
|
||||
isGuest: true,
|
||||
},
|
||||
auth: {
|
||||
credentials: {
|
||||
login: null
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect guest / -> /login', () => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import { browserHistory } from 'services/history';
|
||||
|
||||
import logger from 'services/logger';
|
||||
|
@ -13,18 +14,35 @@ import CompleteState from './CompleteState';
|
|||
import ResendActivationState from './ResendActivationState';
|
||||
import type AbstractState from './AbstractState';
|
||||
|
||||
export type AuthContext = {
|
||||
run: (actionId: string, payload: Object) => any,
|
||||
setState: (newState: AbstractState) => void,
|
||||
getRequest: () => {
|
||||
path: string,
|
||||
query: URLSearchParams,
|
||||
params: Object
|
||||
}
|
||||
type Request = {
|
||||
path: string,
|
||||
query: URLSearchParams,
|
||||
params: Object
|
||||
};
|
||||
export interface AuthContext {
|
||||
run(actionId: string, payload: *): *;
|
||||
setState(newState: AbstractState): Promise<*>|void;
|
||||
getState(): Object;
|
||||
navigate(route: string): void;
|
||||
getRequest(): Request;
|
||||
}
|
||||
|
||||
export default class AuthFlow {
|
||||
constructor(actions) {
|
||||
export default class AuthFlow implements AuthContext {
|
||||
actions: {[key: string]: Function};
|
||||
state: AbstractState;
|
||||
prevState: AbstractState;
|
||||
/**
|
||||
* A callback from router, that allows to replace (perform redirect) route
|
||||
* during route transition
|
||||
*/
|
||||
replace: ?(string) => void;
|
||||
onReady: Function;
|
||||
navigate: Function;
|
||||
currentRequest: Request;
|
||||
dispatch: (action: Object) => void;
|
||||
getState: () => Object;
|
||||
|
||||
constructor(actions: {[key: string]: Function}) {
|
||||
if (typeof actions !== 'object') {
|
||||
throw new Error('AuthFlow requires an actions object');
|
||||
}
|
||||
|
@ -36,16 +54,18 @@ export default class AuthFlow {
|
|||
}
|
||||
}
|
||||
|
||||
setStore(store) {
|
||||
setStore(store: *) {
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {object} options
|
||||
* @param {object} options.replace
|
||||
*/
|
||||
this.navigate = (route, options = {}) => {
|
||||
this.navigate = (route: string, options: {replace?: bool} = {}) => {
|
||||
if (this.getRequest().path !== route) {
|
||||
this.currentRequest = {
|
||||
path: route
|
||||
path: route,
|
||||
params: {},
|
||||
query: new URLSearchParams()
|
||||
};
|
||||
|
||||
if (this.replace) {
|
||||
|
@ -62,11 +82,11 @@ export default class AuthFlow {
|
|||
this.dispatch = store.dispatch.bind(store);
|
||||
}
|
||||
|
||||
resolve(payload = {}) {
|
||||
resolve(payload: Object = {}) {
|
||||
this.state.resolve(this, payload);
|
||||
}
|
||||
|
||||
reject(payload = {}) {
|
||||
reject(payload: Object = {}) {
|
||||
this.state.reject(this, payload);
|
||||
}
|
||||
|
||||
|
@ -74,15 +94,15 @@ export default class AuthFlow {
|
|||
this.state.goBack(this);
|
||||
}
|
||||
|
||||
run(actionId, payload) {
|
||||
run(actionId: string, payload: Object): Promise<*> {
|
||||
if (!this.actions[actionId]) {
|
||||
throw new Error(`Action ${actionId} does not exists`);
|
||||
}
|
||||
|
||||
return this.dispatch(this.actions[actionId](payload));
|
||||
return Promise.resolve(this.dispatch(this.actions[actionId](payload)));
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
setState(state: AbstractState) {
|
||||
if (!state) {
|
||||
throw new Error('State is required');
|
||||
}
|
||||
|
@ -128,7 +148,7 @@ export default class AuthFlow {
|
|||
* @param {function} [callback = function() {}] - an optional callback function to be called, when state will be stabilized
|
||||
* (state's enter function's promise resolved)
|
||||
*/
|
||||
handleRequest(request, replace, callback = function() {}) {
|
||||
handleRequest(request: Request, replace: Function, callback: Function = function() {}) {
|
||||
const {path} = request;
|
||||
this.replace = replace;
|
||||
this.onReady = callback;
|
||||
|
@ -165,6 +185,7 @@ export default class AuthFlow {
|
|||
case '/':
|
||||
case '/login':
|
||||
case '/password':
|
||||
case '/mfa':
|
||||
case '/accept-rules':
|
||||
case '/oauth/permissions':
|
||||
case '/oauth/finish':
|
||||
|
|
|
@ -202,11 +202,11 @@ describe('AuthFlow', () => {
|
|||
expect(actions.test, 'to have a call satisfying', ['arg']);
|
||||
});
|
||||
|
||||
it('should return action dispatch result', () => {
|
||||
it('should resolve to action dispatch result', () => {
|
||||
const expected = 'dispatch called';
|
||||
store.dispatch.returns(expected);
|
||||
|
||||
expect(flow.run('test'), 'to be', expected);
|
||||
expect(flow.run('test'), 'to be fulfilled with', expected);
|
||||
});
|
||||
|
||||
it('throws when running unexisted action', () => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logger from 'services/logger';
|
||||
import { getLogin } from 'components/auth/reducer';
|
||||
|
||||
import AbstractState from './AbstractState';
|
||||
import PasswordState from './PasswordState';
|
||||
|
@ -6,13 +7,14 @@ import RegisterState from './RegisterState';
|
|||
|
||||
export default class LoginState extends AbstractState {
|
||||
enter(context) {
|
||||
const {auth, user} = context.getState();
|
||||
const login = getLogin(context.getState());
|
||||
const {user} = context.getState();
|
||||
|
||||
const isUserAddsSecondAccount = !user.isGuest
|
||||
&& /login|password/.test(context.getRequest().path); // TODO: improve me
|
||||
|
||||
// TODO: it may not allow user to leave password state till he click back or enters password
|
||||
if (auth.login) {
|
||||
if (login) {
|
||||
context.setState(new PasswordState());
|
||||
} else if (user.isGuest || isUserAddsSecondAccount) {
|
||||
context.navigate('/login');
|
||||
|
|
|
@ -27,7 +27,9 @@ describe('LoginState', () => {
|
|||
it('should navigate to /login', () => {
|
||||
context.getState.returns({
|
||||
user: {isGuest: true},
|
||||
auth: {login: null}
|
||||
auth: {
|
||||
credentials: {login: null}
|
||||
}
|
||||
});
|
||||
|
||||
expectNavigate(mock, '/login');
|
||||
|
@ -38,7 +40,9 @@ describe('LoginState', () => {
|
|||
it('should transition to password if login was set', () => {
|
||||
context.getState.returns({
|
||||
user: {isGuest: true},
|
||||
auth: {login: 'foo'}
|
||||
auth: {
|
||||
credentials: {login: 'foo'}
|
||||
}
|
||||
});
|
||||
|
||||
expectState(mock, PasswordState);
|
||||
|
|
49
src/services/authFlow/MfaState.js
Normal file
49
src/services/authFlow/MfaState.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
// @flow
|
||||
import logger from 'services/logger';
|
||||
|
||||
import { getCredentials } from 'components/auth/reducer';
|
||||
|
||||
import AbstractState from './AbstractState';
|
||||
import CompleteState from './CompleteState';
|
||||
import PasswordState from './PasswordState';
|
||||
|
||||
import type { AuthContext } from './AuthFlow';
|
||||
|
||||
export default class MfaState extends AbstractState {
|
||||
enter(context: AuthContext) {
|
||||
const {
|
||||
login,
|
||||
password,
|
||||
isTotpRequired
|
||||
} = getCredentials(context.getState());
|
||||
|
||||
if (login && password && isTotpRequired) {
|
||||
context.navigate('/mfa');
|
||||
} else {
|
||||
context.setState(new CompleteState());
|
||||
}
|
||||
}
|
||||
|
||||
resolve(context: AuthContext, {totp}: {totp: string}) {
|
||||
const {
|
||||
login,
|
||||
password,
|
||||
rememberMe
|
||||
} = getCredentials(context.getState());
|
||||
|
||||
context.run('login', {
|
||||
totp,
|
||||
password,
|
||||
rememberMe,
|
||||
login
|
||||
})
|
||||
.then(() => context.setState(new CompleteState()))
|
||||
.catch((err = {}) =>
|
||||
err.errors || logger.warn('Error logging in', err)
|
||||
);
|
||||
}
|
||||
|
||||
goBack(context: AuthContext) {
|
||||
context.setState(new PasswordState());
|
||||
}
|
||||
}
|
102
src/services/authFlow/MfaState.test.js
Normal file
102
src/services/authFlow/MfaState.test.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
import expect from 'unexpected';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import MfaState from './MfaState';
|
||||
import CompleteState from 'services/authFlow/CompleteState';
|
||||
import PasswordState from 'services/authFlow/PasswordState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
|
||||
describe('MfaState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new MfaState();
|
||||
|
||||
const data = bootstrap();
|
||||
context = data.context;
|
||||
mock = data.mock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
describe('#enter', () => {
|
||||
it('should navigate to /mfa', () => {
|
||||
context.getState.returns({
|
||||
auth: {
|
||||
credentials: {
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
isTotpRequired: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expectNavigate(mock, '/mfa');
|
||||
|
||||
state.enter(context);
|
||||
});
|
||||
|
||||
it('should transition to complete if no totp required', () => {
|
||||
context.getState.returns({
|
||||
auth: {
|
||||
credentials: {
|
||||
login: 'foo',
|
||||
password: 'bar'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expectState(mock, CompleteState);
|
||||
|
||||
state.enter(context);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolve', () => {
|
||||
it('should call login with login and password', () => {
|
||||
const expectedLogin = 'foo';
|
||||
const expectedPassword = 'bar';
|
||||
const expectedTotp = '111222';
|
||||
const expectedRememberMe = true;
|
||||
|
||||
context.getState.returns({
|
||||
auth: {
|
||||
credentials: {
|
||||
login: expectedLogin,
|
||||
password: expectedPassword,
|
||||
rememberMe: expectedRememberMe
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expectRun(
|
||||
mock,
|
||||
'login',
|
||||
sinon.match({
|
||||
totp: expectedTotp,
|
||||
login: expectedLogin,
|
||||
password: expectedPassword,
|
||||
rememberMe: expectedRememberMe
|
||||
})
|
||||
).returns(Promise.resolve());
|
||||
expectState(mock, CompleteState);
|
||||
|
||||
const payload = {totp: expectedTotp};
|
||||
|
||||
return expect(state.resolve(context, payload), 'to be fulfilled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#goBack', () => {
|
||||
it('should transition to login state', () => {
|
||||
expectState(mock, PasswordState);
|
||||
|
||||
state.goBack(context);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,40 +1,62 @@
|
|||
// @flow
|
||||
import logger from 'services/logger';
|
||||
import { getCredentials } from 'components/auth/reducer';
|
||||
|
||||
import AbstractState from './AbstractState';
|
||||
import CompleteState from './CompleteState';
|
||||
import ForgotPasswordState from './ForgotPasswordState';
|
||||
import LoginState from './LoginState';
|
||||
import MfaState from './MfaState';
|
||||
|
||||
import type { AuthContext } from './AuthFlow';
|
||||
|
||||
export default class PasswordState extends AbstractState {
|
||||
enter(context) {
|
||||
const {auth} = context.getState();
|
||||
enter(context: AuthContext) {
|
||||
const {login} = getCredentials(context.getState());
|
||||
|
||||
if (auth.login) {
|
||||
if (login) {
|
||||
context.navigate('/password');
|
||||
} else {
|
||||
context.setState(new CompleteState());
|
||||
}
|
||||
}
|
||||
|
||||
resolve(context, {password, rememberMe}) {
|
||||
const {auth: {login}} = context.getState();
|
||||
resolve(
|
||||
context: AuthContext,
|
||||
{
|
||||
password,
|
||||
rememberMe
|
||||
}: {
|
||||
password: string,
|
||||
rememberMe: bool
|
||||
}
|
||||
) {
|
||||
const {login} = getCredentials(context.getState());
|
||||
|
||||
return context.run('login', {
|
||||
context.run('login', {
|
||||
password,
|
||||
rememberMe,
|
||||
login
|
||||
})
|
||||
.then(() => context.setState(new CompleteState()))
|
||||
.then(() => {
|
||||
const {isTotpRequired} = getCredentials(context.getState());
|
||||
|
||||
if (isTotpRequired) {
|
||||
return context.setState(new MfaState());
|
||||
}
|
||||
|
||||
return context.setState(new CompleteState());
|
||||
})
|
||||
.catch((err = {}) =>
|
||||
err.errors || logger.warn('Error logging in', err)
|
||||
);
|
||||
}
|
||||
|
||||
reject(context) {
|
||||
reject(context: AuthContext) {
|
||||
context.setState(new ForgotPasswordState());
|
||||
}
|
||||
|
||||
goBack(context) {
|
||||
goBack(context: AuthContext) {
|
||||
context.run('setLogin', null);
|
||||
context.setState(new LoginState());
|
||||
}
|
||||
|
|
|
@ -29,7 +29,9 @@ describe('PasswordState', () => {
|
|||
it('should navigate to /password', () => {
|
||||
context.getState.returns({
|
||||
user: {isGuest: true},
|
||||
auth: {login: 'foo'}
|
||||
auth: {
|
||||
credentials: {login: 'foo'}
|
||||
}
|
||||
});
|
||||
|
||||
expectNavigate(mock, '/password');
|
||||
|
@ -40,7 +42,9 @@ describe('PasswordState', () => {
|
|||
it('should transition to complete if not guest', () => {
|
||||
context.getState.returns({
|
||||
user: {isGuest: false},
|
||||
auth: {login: null}
|
||||
auth: {
|
||||
credentials: {login: null}
|
||||
}
|
||||
});
|
||||
|
||||
expectState(mock, CompleteState);
|
||||
|
@ -57,7 +61,9 @@ describe('PasswordState', () => {
|
|||
|
||||
context.getState.returns({
|
||||
auth: {
|
||||
login: expectedLogin
|
||||
credentials: {
|
||||
login: expectedLogin
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user