#389: fix logout in case, when all the accounts have invalid tokens

This commit is contained in:
SleepWalker 2018-02-27 23:17:31 +02:00
parent 206627be17
commit f1d33bf7ec
8 changed files with 95 additions and 45 deletions

View File

@ -1,14 +1,14 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import loader from 'services/loader'; import loader from 'services/loader';
import { skins, SKIN_DARK, COLOR_WHITE } from 'components/ui'; import { skins, SKIN_DARK, COLOR_WHITE } from 'components/ui';
import { Button } from 'components/ui/form'; import { Button } from 'components/ui/form';
import { userShape } from 'components/user/User'; import { authenticate, revoke } from 'components/accounts/actions';
import { getActiveAccount } from 'components/accounts/reducer';
import styles from './accountSwitcher.scss'; import styles from './accountSwitcher.scss';
import messages from './AccountSwitcher.intl.json'; import messages from './AccountSwitcher.intl.json';
@ -22,7 +22,6 @@ export class AccountSwitcher extends Component {
onAfterAction: PropTypes.func, // called after each action performed onAfterAction: PropTypes.func, // called after each action performed
onSwitch: PropTypes.func, // called after switching an account. The active account will be passed as arg onSwitch: PropTypes.func, // called after switching an account. The active account will be passed as arg
accounts: PropTypes.object, // eslint-disable-line accounts: PropTypes.object, // eslint-disable-line
user: userShape, // TODO: remove me, when we will be sure, that accounts.active is always set for user (event after register)
skin: PropTypes.oneOf(skins), skin: PropTypes.oneOf(skins),
highlightActiveAccount: PropTypes.bool, // whether active account should be expanded and shown on the top highlightActiveAccount: PropTypes.bool, // whether active account should be expanded and shown on the top
allowLogout: PropTypes.bool, // whether to show logout icon near each account allowLogout: PropTypes.bool, // whether to show logout icon near each account
@ -40,8 +39,7 @@ export class AccountSwitcher extends Component {
render() { render() {
const { accounts, skin, allowAdd, allowLogout, highlightActiveAccount } = this.props; const { accounts, skin, allowAdd, allowLogout, highlightActiveAccount } = this.props;
// const activeAccount = accounts.active || this.props.user; const activeAccount = getActiveAccount({ accounts });
const activeAccount = this.props.user;
let {available} = accounts; let {available} = accounts;
@ -83,14 +81,14 @@ export class AccountSwitcher extends Component {
</div> </div>
</div> </div>
) : null} ) : null}
{available.map((account, id) => ( {available.map((account, index) => (
<div className={classNames(styles.item, styles.accountSwitchItem)} <div className={classNames(styles.item, styles.accountSwitchItem)}
key={account.id} key={account.id}
onClick={this.onSwitch(account)} onClick={this.onSwitch(account)}
> >
<div className={classNames( <div className={classNames(
styles.accountIcon, styles.accountIcon,
styles[`accountIcon${id % 7 + (highlightActiveAccount ? 2 : 1)}`] styles[`accountIcon${index % 7 + (highlightActiveAccount ? 2 : 1)}`]
)} /> )} />
{allowLogout ? ( {allowLogout ? (
@ -156,12 +154,8 @@ export class AccountSwitcher extends Component {
}; };
} }
import { connect } from 'react-redux'; export default connect(({accounts}) => ({
import { authenticate, revoke } from 'components/accounts/actions';
export default connect(({accounts, user}) => ({
accounts, accounts,
user
}), { }), {
switchAccount: authenticate, switchAccount: authenticate,
removeAccount: revoke removeAccount: revoke

View File

@ -9,6 +9,7 @@ import { setAccountSwitcher } from 'components/auth/actions';
import { getActiveAccount } from 'components/accounts/reducer'; import { getActiveAccount } from 'components/accounts/reducer';
import logger from 'services/logger'; import logger from 'services/logger';
import type { Account, State as AccountsState } from './reducer';
import { import {
add, add,
remove, remove,
@ -16,7 +17,6 @@ import {
reset, reset,
updateToken updateToken
} from './actions/pure-actions'; } from './actions/pure-actions';
import type { Account, State as AccountsState } from './reducer';
type Dispatch = (action: Object) => Promise<*>; type Dispatch = (action: Object) => Promise<*>;
@ -45,8 +45,19 @@ export function authenticate(account: Account | {
const {token, refreshToken} = account; const {token, refreshToken} = account;
const email = account.email || null; const email = account.email || null;
return (dispatch: Dispatch, getState: () => State): Promise<Account> => return (dispatch: Dispatch, getState: () => State): Promise<Account> => {
authentication.validateToken({token, refreshToken}) const accountId: number | null = typeof account.id === 'number' ? account.id : null;
const knownAccount: ?Account = accountId
? getState().accounts.available.find((item) => item.id === accountId)
: null;
if (knownAccount) {
// this account is already available
// activate it before validation
dispatch(activate(knownAccount));
}
return authentication.validateToken({token, refreshToken})
.catch((resp = {}) => { .catch((resp = {}) => {
// all the logic to get the valid token was failed, // all the logic to get the valid token was failed,
// looks like we have some problems with token // looks like we have some problems with token
@ -97,6 +108,7 @@ export function authenticate(account: Account | {
return dispatch(setLocale(user.lang)) return dispatch(setLocale(user.lang))
.then(() => account); .then(() => account);
}); });
};
} }
/** /**
@ -219,9 +231,17 @@ export function revoke(account: Account) {
if (accountToReplace) { if (accountToReplace) {
return dispatch(authenticate(accountToReplace)) return dispatch(authenticate(accountToReplace))
.then(() => { .finally(() => {
authentication.logout(account); // we need to logout user, even in case, when we can
// not authenticate him with new account
authentication.logout(account)
.catch(() => {
// we don't care
});
dispatch(remove(account)); dispatch(remove(account));
})
.catch(() => {
// we don't care
}); });
} }

View File

@ -190,6 +190,35 @@ describe('components/accounts/actions', () => {
) )
); );
}); });
describe('when one account available', () => {
beforeEach(() => {
getState.returns({
accounts: {
active: account.id,
available: [account]
},
auth: {
credentials: {},
},
user,
});
});
it('should activate account before auth api call', () => {
authentication.validateToken.returns(Promise.reject({ error: 'foo'}));
return expect(
authenticate(account)(dispatch, getState),
'to be rejected with',
{ error: 'foo'}
).then(() =>
expect(dispatch, 'to have a call satisfying', [
activate(account)
])
);
});
});
}); });
describe('#revoke()', () => { describe('#revoke()', () => {

View File

@ -1,16 +1,12 @@
// @flow // @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { AccountSwitcher } from 'components/accounts'; import { AccountSwitcher } from 'components/accounts';
import styles from './loggedInPanel.scss'; import styles from './loggedInPanel.scss';
import type { User } from 'components/user';
export default class LoggedInPanel extends Component<{ export default class LoggedInPanel extends Component<{
user: User username: string
}, { }, {
isAccountSwitcherActive: bool isAccountSwitcherActive: bool
}> { }> {
@ -38,7 +34,7 @@ export default class LoggedInPanel extends Component<{
} }
render() { render() {
const { user } = this.props; const { username } = this.props;
const { isAccountSwitcherActive } = this.state; const { isAccountSwitcherActive } = this.state;
return ( return (
@ -48,7 +44,7 @@ export default class LoggedInPanel extends Component<{
})}> })}>
<button className={styles.activeAccountButton} onClick={this.onExpandAccountSwitcher}> <button className={styles.activeAccountButton} onClick={this.onExpandAccountSwitcher}>
<span className={styles.userIcon} /> <span className={styles.userIcon} />
<span className={styles.userName}>{user.username}</span> <span className={styles.userName}>{username}</span>
<span className={styles.expandIcon} /> <span className={styles.expandIcon} />
</button> </button>

View File

@ -1,31 +1,26 @@
import PropTypes from 'prop-types'; // @flow
import type { Account } from 'components/accounts/reducer';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import buttons from 'components/ui/buttons.scss'; import buttons from 'components/ui/buttons.scss';
import messages from './Userbar.intl.json'; import messages from './Userbar.intl.json';
import styles from './userbar.scss'; import styles from './userbar.scss';
import { userShape } from 'components/user/User';
import LoggedInPanel from './LoggedInPanel'; import LoggedInPanel from './LoggedInPanel';
export default class Userbar extends Component { export default class Userbar extends Component<{
account: ?Account,
guestAction: 'register' | 'login',
}> {
static displayName = 'Userbar'; static displayName = 'Userbar';
static propTypes = {
user: userShape,
guestAction: PropTypes.oneOf(['register', 'login'])
};
static defaultProps = { static defaultProps = {
guestAction: 'register' guestAction: 'register'
}; };
render() { render() {
const { user } = this.props; const { account } = this.props;
let { guestAction } = this.props; let { guestAction } = this.props;
switch (guestAction) { switch (guestAction) {
@ -48,12 +43,12 @@ export default class Userbar extends Component {
return ( return (
<div className={styles.userbar}> <div className={styles.userbar}>
{user.isGuest {account
? ( ? (
guestAction <LoggedInPanel username={account.username} />
) )
: ( : (
<LoggedInPanel {...this.props} /> guestAction
) )
} }
</div> </div>

View File

@ -1,4 +1,6 @@
// @flow // @flow
import type { User } from 'components/user';
import type { Account } from 'components/accounts/reducer';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { resetAuth } from 'components/auth/actions'; import { resetAuth } from 'components/auth/actions';
@ -7,24 +9,23 @@ import { FormattedMessage as Message } from 'react-intl';
import { Route, Link, Switch } from 'react-router-dom'; import { Route, Link, Switch } from 'react-router-dom';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import classNames from 'classnames'; import classNames from 'classnames';
import AuthPage from 'pages/auth/AuthPage'; import AuthPage from 'pages/auth/AuthPage';
import ProfilePage from 'pages/profile/ProfilePage'; import ProfilePage from 'pages/profile/ProfilePage';
import RulesPage from 'pages/rules/RulesPage'; import RulesPage from 'pages/rules/RulesPage';
import PageNotFound from 'pages/404/PageNotFound'; import PageNotFound from 'pages/404/PageNotFound';
import { ScrollIntoView } from 'components/ui/scroll'; import { ScrollIntoView } from 'components/ui/scroll';
import PrivateRoute from 'containers/PrivateRoute'; import PrivateRoute from 'containers/PrivateRoute';
import AuthFlowRoute from 'containers/AuthFlowRoute'; import AuthFlowRoute from 'containers/AuthFlowRoute';
import Userbar from 'components/userbar/Userbar'; import Userbar from 'components/userbar/Userbar';
import PopupStack from 'components/ui/popup/PopupStack'; import PopupStack from 'components/ui/popup/PopupStack';
import loader from 'services/loader'; import loader from 'services/loader';
import type { User } from 'components/user'; import { getActiveAccount } from 'components/accounts/reducer';
import styles from './root.scss'; import styles from './root.scss';
import messages from './RootPage.intl.json'; import messages from './RootPage.intl.json';
class RootPage extends Component<{ class RootPage extends Component<{
account: ?Account,
user: User, user: User,
isPopupActive: bool, isPopupActive: bool,
onLogoClick: Function, onLogoClick: Function,
@ -46,7 +47,7 @@ class RootPage extends Component<{
render() { render() {
const props = this.props; const props = this.props;
const {user, isPopupActive, onLogoClick} = this.props; const {user, account, isPopupActive, onLogoClick} = this.props;
const isRegisterPage = props.location.pathname === '/register'; const isRegisterPage = props.location.pathname === '/register';
if (document && document.body) { if (document && document.body) {
@ -70,7 +71,8 @@ class RootPage extends Component<{
<Message {...messages.siteName} /> <Message {...messages.siteName} />
</Link> </Link>
<div className={styles.userbar}> <div className={styles.userbar}>
<Userbar {...props} <Userbar
account={account}
guestAction={isRegisterPage ? 'login' : 'register'} guestAction={isRegisterPage ? 'login' : 'register'}
/> />
</div> </div>
@ -95,6 +97,7 @@ class RootPage extends Component<{
export default withRouter(connect((state) => ({ export default withRouter(connect((state) => ({
user: state.user, user: state.user,
account: getActiveAccount(state),
isPopupActive: state.popup.popups.length > 0 isPopupActive: state.popup.popups.length > 0
}), { }), {
onLogoClick: resetAuth onLogoClick: resetAuth

View File

@ -1,11 +1,13 @@
{ {
"account1": { "account1": {
"username": "SleepWalker", "username": "SleepWalker",
"email": "danilenkos@auroraglobal.com",
"login": "SleepWalker", "login": "SleepWalker",
"password": "qwer1234" "password": "qwer1234"
}, },
"account2": { "account2": {
"username": "test", "username": "test",
"email": "admin@udf.su",
"login": "test", "login": "test",
"password": "qwer1234" "password": "qwer1234"
} }

View File

@ -24,6 +24,7 @@ describe('when user\'s token and refreshToken are invalid', () => {
}); });
it('should allow select account', () => { it('should allow select account', () => {
// TODO: need a way to get valid token for one of the accounts
cy.visit('/'); cy.visit('/');
cy.get('[data-e2e-go-back]').click(); cy.get('[data-e2e-go-back]').click();
@ -39,6 +40,16 @@ describe('when user\'s token and refreshToken are invalid', () => {
cy.contains('account preferences'); cy.contains('account preferences');
}); });
it('should allow logout', () => {
cy.visit('/');
cy.get('[data-e2e-toolbar]').contains(account2.username).click();
cy.get('[data-e2e-toolbar]').contains('Log out').click();
cy.contains(account2.email).should('not.exist');
cy.get('[data-e2e-toolbar]').contains(account2.username).should('not.exist');
});
it('should allow enter new login from choose account', () => { it('should allow enter new login from choose account', () => {
cy.visit('/'); cy.visit('/');