#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 React, { Component } from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import loader from 'services/loader';
import { skins, SKIN_DARK, COLOR_WHITE } from 'components/ui';
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 messages from './AccountSwitcher.intl.json';
@ -22,7 +22,6 @@ export class AccountSwitcher extends Component {
onAfterAction: PropTypes.func, // called after each action performed
onSwitch: PropTypes.func, // called after switching an account. The active account will be passed as arg
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),
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
@ -40,8 +39,7 @@ export class AccountSwitcher extends Component {
render() {
const { accounts, skin, allowAdd, allowLogout, highlightActiveAccount } = this.props;
// const activeAccount = accounts.active || this.props.user;
const activeAccount = this.props.user;
const activeAccount = getActiveAccount({ accounts });
let {available} = accounts;
@ -83,14 +81,14 @@ export class AccountSwitcher extends Component {
</div>
</div>
) : null}
{available.map((account, id) => (
{available.map((account, index) => (
<div className={classNames(styles.item, styles.accountSwitchItem)}
key={account.id}
onClick={this.onSwitch(account)}
>
<div className={classNames(
styles.accountIcon,
styles[`accountIcon${id % 7 + (highlightActiveAccount ? 2 : 1)}`]
styles[`accountIcon${index % 7 + (highlightActiveAccount ? 2 : 1)}`]
)} />
{allowLogout ? (
@ -156,12 +154,8 @@ export class AccountSwitcher extends Component {
};
}
import { connect } from 'react-redux';
import { authenticate, revoke } from 'components/accounts/actions';
export default connect(({accounts, user}) => ({
export default connect(({accounts}) => ({
accounts,
user
}), {
switchAccount: authenticate,
removeAccount: revoke

View File

@ -9,6 +9,7 @@ import { setAccountSwitcher } from 'components/auth/actions';
import { getActiveAccount } from 'components/accounts/reducer';
import logger from 'services/logger';
import type { Account, State as AccountsState } from './reducer';
import {
add,
remove,
@ -16,7 +17,6 @@ import {
reset,
updateToken
} from './actions/pure-actions';
import type { Account, State as AccountsState } from './reducer';
type Dispatch = (action: Object) => Promise<*>;
@ -45,8 +45,19 @@ export function authenticate(account: Account | {
const {token, refreshToken} = account;
const email = account.email || null;
return (dispatch: Dispatch, getState: () => State): Promise<Account> =>
authentication.validateToken({token, refreshToken})
return (dispatch: Dispatch, getState: () => State): Promise<Account> => {
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 = {}) => {
// all the logic to get the valid token was failed,
// looks like we have some problems with token
@ -97,6 +108,7 @@ export function authenticate(account: Account | {
return dispatch(setLocale(user.lang))
.then(() => account);
});
};
}
/**
@ -219,9 +231,17 @@ export function revoke(account: Account) {
if (accountToReplace) {
return dispatch(authenticate(accountToReplace))
.then(() => {
authentication.logout(account);
.finally(() => {
// 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));
})
.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()', () => {

View File

@ -1,16 +1,12 @@
// @flow
import React, { Component } from 'react';
import classNames from 'classnames';
import { AccountSwitcher } from 'components/accounts';
import styles from './loggedInPanel.scss';
import type { User } from 'components/user';
export default class LoggedInPanel extends Component<{
user: User
username: string
}, {
isAccountSwitcherActive: bool
}> {
@ -38,7 +34,7 @@ export default class LoggedInPanel extends Component<{
}
render() {
const { user } = this.props;
const { username } = this.props;
const { isAccountSwitcherActive } = this.state;
return (
@ -48,7 +44,7 @@ export default class LoggedInPanel extends Component<{
})}>
<button className={styles.activeAccountButton} onClick={this.onExpandAccountSwitcher}>
<span className={styles.userIcon} />
<span className={styles.userName}>{user.username}</span>
<span className={styles.userName}>{username}</span>
<span className={styles.expandIcon} />
</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 { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import buttons from 'components/ui/buttons.scss';
import messages from './Userbar.intl.json';
import styles from './userbar.scss';
import { userShape } from 'components/user/User';
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 propTypes = {
user: userShape,
guestAction: PropTypes.oneOf(['register', 'login'])
};
static defaultProps = {
guestAction: 'register'
};
render() {
const { user } = this.props;
const { account } = this.props;
let { guestAction } = this.props;
switch (guestAction) {
@ -48,12 +43,12 @@ export default class Userbar extends Component {
return (
<div className={styles.userbar}>
{user.isGuest
{account
? (
guestAction
<LoggedInPanel username={account.username} />
)
: (
<LoggedInPanel {...this.props} />
guestAction
)
}
</div>

View File

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

View File

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

View File

@ -24,6 +24,7 @@ describe('when user\'s token and refreshToken are invalid', () => {
});
it('should allow select account', () => {
// TODO: need a way to get valid token for one of the accounts
cy.visit('/');
cy.get('[data-e2e-go-back]').click();
@ -39,6 +40,16 @@ describe('when user\'s token and refreshToken are invalid', () => {
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', () => {
cy.visit('/');