mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-11-30 10:42:34 +05:30
#389: allow switch accounts, when refreshToken is invalid. Fix oauth in case, when refreshToken is invalid
This commit is contained in:
parent
f64fb3b96b
commit
bf2976c009
@ -1,9 +1,8 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { getJwtPayload } from 'functions';
|
import { getJwtPayload } from 'functions';
|
||||||
import { browserHistory } from 'services/history';
|
|
||||||
import { sessionStorage } from 'services/localStorage';
|
import { sessionStorage } from 'services/localStorage';
|
||||||
import authentication from 'services/api/authentication';
|
import authentication from 'services/api/authentication';
|
||||||
import { setLogin } from 'components/auth/actions';
|
import { relogin as navigateToLogin } from 'components/auth/actions';
|
||||||
import { updateUser, setGuest } from 'components/user/actions';
|
import { updateUser, setGuest } from 'components/user/actions';
|
||||||
import { setLocale } from 'components/i18n/actions';
|
import { setLocale } from 'components/i18n/actions';
|
||||||
import { setAccountSwitcher } from 'components/auth/actions';
|
import { setAccountSwitcher } from 'components/auth/actions';
|
||||||
@ -238,8 +237,7 @@ export function relogin(email?: string) {
|
|||||||
email = activeAccount.email;
|
email = activeAccount.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
email && dispatch(setLogin(email));
|
dispatch(navigateToLogin(email || null));
|
||||||
browserHistory.push('/login');
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import ContactForm from 'components/contact/ContactForm';
|
|||||||
|
|
||||||
export { updateUser } from 'components/user/actions';
|
export { updateUser } from 'components/user/actions';
|
||||||
export { authenticate, logoutAll as logout } from 'components/accounts/actions';
|
export { authenticate, logoutAll as logout } from 'components/accounts/actions';
|
||||||
|
import { getCredentials } from './reducer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reoutes user to the previous page if it is possible
|
* Reoutes user to the previous page if it is possible
|
||||||
@ -224,19 +225,40 @@ export function setLogin(login: ?string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function relogin(login: ?string) {
|
||||||
|
return (dispatch: (Function | Object) => void) => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_CREDENTIALS,
|
||||||
|
payload: {
|
||||||
|
login,
|
||||||
|
returnUrl: location.pathname + location.search,
|
||||||
|
isRelogin: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
browserHistory.push('/login');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function requestTotp({login, password, rememberMe}: {
|
function requestTotp({login, password, rememberMe}: {
|
||||||
login: string,
|
login: string,
|
||||||
password: string,
|
password: string,
|
||||||
rememberMe: bool
|
rememberMe: bool
|
||||||
}) {
|
}) {
|
||||||
return {
|
return (dispatch: (Function | Object) => void, getState: () => Object) => {
|
||||||
type: SET_CREDENTIALS,
|
// merging with current credentials to propogate returnUrl
|
||||||
payload: {
|
const credentials = getCredentials(getState());
|
||||||
login,
|
|
||||||
password,
|
dispatch({
|
||||||
rememberMe,
|
type: SET_CREDENTIALS,
|
||||||
isTotpRequired: true
|
payload: {
|
||||||
}
|
...credentials,
|
||||||
|
login,
|
||||||
|
password,
|
||||||
|
rememberMe,
|
||||||
|
isTotpRequired: true
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,11 +19,13 @@ export default class ChooseAccountBody extends BaseAuthBody {
|
|||||||
<div>
|
<div>
|
||||||
{this.renderErrors()}
|
{this.renderErrors()}
|
||||||
|
|
||||||
<div className={styles.description}>
|
{client && (
|
||||||
<Message {...messages.description} values={{
|
<div className={styles.description}>
|
||||||
appName: <span className={styles.appName}>{client.name}</span>
|
<Message {...messages.description} values={{
|
||||||
}} />
|
appName: <span className={styles.appName}>{client.name}</span>
|
||||||
</div>
|
}} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.accountSwitcherContainer}>
|
<div className={styles.accountSwitcherContainer}>
|
||||||
<AccountSwitcher
|
<AccountSwitcher
|
||||||
|
@ -13,6 +13,15 @@ import {
|
|||||||
SET_SWITCHER
|
SET_SWITCHER
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
|
type Credentials = {
|
||||||
|
login?: string,
|
||||||
|
password?: string,
|
||||||
|
rememberMe?: bool,
|
||||||
|
returnUrl?: string,
|
||||||
|
isRelogin?: bool,
|
||||||
|
isTotpRequired?: bool,
|
||||||
|
};
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
credentials,
|
credentials,
|
||||||
error,
|
error,
|
||||||
@ -44,12 +53,7 @@ function credentials(
|
|||||||
state = {},
|
state = {},
|
||||||
{type, payload}: {
|
{type, payload}: {
|
||||||
type: string,
|
type: string,
|
||||||
payload: ?{
|
payload: ?Credentials
|
||||||
login?: string,
|
|
||||||
password?: string,
|
|
||||||
rememberMe?: bool,
|
|
||||||
isTotpRequired?: bool
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (type === SET_CREDENTIALS) {
|
if (type === SET_CREDENTIALS) {
|
||||||
@ -166,11 +170,6 @@ export function getLogin(state: Object): ?string {
|
|||||||
return state.auth.credentials.login || null;
|
return state.auth.credentials.login || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCredentials(state: Object): {
|
export function getCredentials(state: Object): Credentials {
|
||||||
login?: string,
|
|
||||||
password?: string,
|
|
||||||
rememberMe?: bool,
|
|
||||||
isTotpRequired?: bool
|
|
||||||
} {
|
|
||||||
return state.auth.credentials;
|
return state.auth.credentials;
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ class AuthPage extends Component<{
|
|||||||
<Route path="/activation/:key?" render={renderPanelTransition(Activation)} />
|
<Route path="/activation/:key?" render={renderPanelTransition(Activation)} />
|
||||||
<Route path="/resend-activation" render={renderPanelTransition(ResendActivation)} />
|
<Route path="/resend-activation" render={renderPanelTransition(ResendActivation)} />
|
||||||
<Route path="/oauth/permissions" render={renderPanelTransition(Permissions)} />
|
<Route path="/oauth/permissions" render={renderPanelTransition(Permissions)} />
|
||||||
|
<Route path="/choose-account" render={renderPanelTransition(ChooseAccount)} />
|
||||||
<Route path="/oauth/choose-account" render={renderPanelTransition(ChooseAccount)} />
|
<Route path="/oauth/choose-account" render={renderPanelTransition(ChooseAccount)} />
|
||||||
<Route path="/oauth/finish" component={Finish} />
|
<Route path="/oauth/finish" component={Finish} />
|
||||||
<Route path="/accept-rules" render={renderPanelTransition(AcceptRules)} />
|
<Route path="/accept-rules" render={renderPanelTransition(AcceptRules)} />
|
||||||
|
@ -11,6 +11,7 @@ import ForgotPasswordState from './ForgotPasswordState';
|
|||||||
import RecoverPasswordState from './RecoverPasswordState';
|
import RecoverPasswordState from './RecoverPasswordState';
|
||||||
import ActivationState from './ActivationState';
|
import ActivationState from './ActivationState';
|
||||||
import CompleteState from './CompleteState';
|
import CompleteState from './CompleteState';
|
||||||
|
import ChooseAccountState from './ChooseAccountState';
|
||||||
import ResendActivationState from './ResendActivationState';
|
import ResendActivationState from './ResendActivationState';
|
||||||
import type AbstractState from './AbstractState';
|
import type AbstractState from './AbstractState';
|
||||||
|
|
||||||
@ -219,6 +220,10 @@ export default class AuthFlow implements AuthContext {
|
|||||||
this.setState(new ResendActivationState());
|
this.setState(new ResendActivationState());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case '/choose-account':
|
||||||
|
this.setState(new ChooseAccountState());
|
||||||
|
break;
|
||||||
|
|
||||||
case '/':
|
case '/':
|
||||||
case '/login':
|
case '/login':
|
||||||
case '/password':
|
case '/password':
|
||||||
|
@ -13,6 +13,7 @@ import ActivationState from 'services/authFlow/ActivationState';
|
|||||||
import ResendActivationState from 'services/authFlow/ResendActivationState';
|
import ResendActivationState from 'services/authFlow/ResendActivationState';
|
||||||
import LoginState from 'services/authFlow/LoginState';
|
import LoginState from 'services/authFlow/LoginState';
|
||||||
import CompleteState from 'services/authFlow/CompleteState';
|
import CompleteState from 'services/authFlow/CompleteState';
|
||||||
|
import ChooseAccountState from 'services/authFlow/ChooseAccountState';
|
||||||
|
|
||||||
describe('AuthFlow', () => {
|
describe('AuthFlow', () => {
|
||||||
let flow;
|
let flow;
|
||||||
@ -275,6 +276,7 @@ describe('AuthFlow', () => {
|
|||||||
'/oauth2/v1': OAuthState,
|
'/oauth2/v1': OAuthState,
|
||||||
'/oauth2': OAuthState,
|
'/oauth2': OAuthState,
|
||||||
'/register': RegisterState,
|
'/register': RegisterState,
|
||||||
|
'/choose-account': ChooseAccountState,
|
||||||
'/recover-password': RecoverPasswordState,
|
'/recover-password': RecoverPasswordState,
|
||||||
'/recover-password/key123': RecoverPasswordState,
|
'/recover-password/key123': RecoverPasswordState,
|
||||||
'/forgot-password': ForgotPasswordState,
|
'/forgot-password': ForgotPasswordState,
|
||||||
|
@ -4,7 +4,13 @@ import CompleteState from './CompleteState';
|
|||||||
|
|
||||||
export default class ChooseAccountState extends AbstractState {
|
export default class ChooseAccountState extends AbstractState {
|
||||||
enter(context) {
|
enter(context) {
|
||||||
context.navigate('/oauth/choose-account');
|
const { auth } = context.getState();
|
||||||
|
|
||||||
|
if (auth.oauth) {
|
||||||
|
context.navigate('/oauth/choose-account');
|
||||||
|
} else {
|
||||||
|
context.navigate('/choose-account');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(context, payload) {
|
resolve(context, payload) {
|
||||||
@ -12,6 +18,7 @@ export default class ChooseAccountState extends AbstractState {
|
|||||||
context.setState(new CompleteState());
|
context.setState(new CompleteState());
|
||||||
} else {
|
} else {
|
||||||
context.navigate('/login');
|
context.navigate('/login');
|
||||||
|
context.run('setLogin', null);
|
||||||
context.setState(new LoginState());
|
context.setState(new LoginState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,10 +23,28 @@ describe('ChooseAccountState', () => {
|
|||||||
|
|
||||||
describe('#enter', () => {
|
describe('#enter', () => {
|
||||||
it('should navigate to /oauth/choose-account', () => {
|
it('should navigate to /oauth/choose-account', () => {
|
||||||
|
context.getState.returns({
|
||||||
|
auth: {
|
||||||
|
oauth: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expectNavigate(mock, '/oauth/choose-account');
|
expectNavigate(mock, '/oauth/choose-account');
|
||||||
|
|
||||||
state.enter(context);
|
state.enter(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should navigate to /choose-account if not oauth', () => {
|
||||||
|
context.getState.returns({
|
||||||
|
auth: {
|
||||||
|
oauth: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expectNavigate(mock, '/choose-account');
|
||||||
|
|
||||||
|
state.enter(context);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#resolve', () => {
|
describe('#resolve', () => {
|
||||||
@ -38,6 +56,7 @@ describe('ChooseAccountState', () => {
|
|||||||
|
|
||||||
it('should transition to login if user wants to add new account', () => {
|
it('should transition to login if user wants to add new account', () => {
|
||||||
expectNavigate(mock, '/login');
|
expectNavigate(mock, '/login');
|
||||||
|
expectRun(mock, 'setLogin', null);
|
||||||
expectState(mock, LoginState);
|
expectState(mock, LoginState);
|
||||||
|
|
||||||
state.resolve(context, {});
|
state.resolve(context, {});
|
||||||
|
@ -3,6 +3,7 @@ import logger from 'services/logger';
|
|||||||
import { getCredentials } from 'components/auth/reducer';
|
import { getCredentials } from 'components/auth/reducer';
|
||||||
|
|
||||||
import AbstractState from './AbstractState';
|
import AbstractState from './AbstractState';
|
||||||
|
import ChooseAccountState from './ChooseAccountState';
|
||||||
import CompleteState from './CompleteState';
|
import CompleteState from './CompleteState';
|
||||||
import ForgotPasswordState from './ForgotPasswordState';
|
import ForgotPasswordState from './ForgotPasswordState';
|
||||||
import LoginState from './LoginState';
|
import LoginState from './LoginState';
|
||||||
@ -31,7 +32,7 @@ export default class PasswordState extends AbstractState {
|
|||||||
rememberMe: bool
|
rememberMe: bool
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const {login} = getCredentials(context.getState());
|
const { login, returnUrl } = getCredentials(context.getState());
|
||||||
|
|
||||||
return context.run('login', {
|
return context.run('login', {
|
||||||
password,
|
password,
|
||||||
@ -39,12 +40,17 @@ export default class PasswordState extends AbstractState {
|
|||||||
login
|
login
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const {isTotpRequired} = getCredentials(context.getState());
|
const { isTotpRequired } = getCredentials(context.getState());
|
||||||
|
|
||||||
if (isTotpRequired) {
|
if (isTotpRequired) {
|
||||||
return context.setState(new MfaState());
|
return context.setState(new MfaState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (returnUrl) {
|
||||||
|
context.navigate(returnUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return context.setState(new CompleteState());
|
return context.setState(new CompleteState());
|
||||||
})
|
})
|
||||||
.catch((err = {}) =>
|
.catch((err = {}) =>
|
||||||
@ -57,7 +63,13 @@ export default class PasswordState extends AbstractState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goBack(context: AuthContext) {
|
goBack(context: AuthContext) {
|
||||||
context.run('setLogin', null);
|
const { isRelogin } = getCredentials(context.getState());
|
||||||
context.setState(new LoginState());
|
|
||||||
|
if (isRelogin) {
|
||||||
|
context.setState(new ChooseAccountState());
|
||||||
|
} else {
|
||||||
|
context.run('setLogin', null);
|
||||||
|
context.setState(new LoginState());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,10 @@ import sinon from 'sinon';
|
|||||||
|
|
||||||
import PasswordState from 'services/authFlow/PasswordState';
|
import PasswordState from 'services/authFlow/PasswordState';
|
||||||
import CompleteState from 'services/authFlow/CompleteState';
|
import CompleteState from 'services/authFlow/CompleteState';
|
||||||
|
import MfaState from 'services/authFlow/MfaState';
|
||||||
import LoginState from 'services/authFlow/LoginState';
|
import LoginState from 'services/authFlow/LoginState';
|
||||||
import ForgotPasswordState from 'services/authFlow/ForgotPasswordState';
|
import ForgotPasswordState from 'services/authFlow/ForgotPasswordState';
|
||||||
|
import ChooseAccountState from 'services/authFlow/ChooseAccountState';
|
||||||
|
|
||||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||||
|
|
||||||
@ -82,6 +84,67 @@ describe('PasswordState', () => {
|
|||||||
|
|
||||||
return expect(state.resolve(context, payload), 'to be fulfilled');
|
return expect(state.resolve(context, payload), 'to be fulfilled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should go to MfaState if totp required', () => {
|
||||||
|
const expectedLogin = 'foo';
|
||||||
|
const expectedPassword = 'bar';
|
||||||
|
const expectedRememberMe = true;
|
||||||
|
|
||||||
|
context.getState.returns({
|
||||||
|
auth: {
|
||||||
|
credentials: {
|
||||||
|
login: expectedLogin,
|
||||||
|
isTotpRequired: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expectRun(
|
||||||
|
mock,
|
||||||
|
'login',
|
||||||
|
sinon.match({
|
||||||
|
login: expectedLogin,
|
||||||
|
password: expectedPassword,
|
||||||
|
rememberMe: expectedRememberMe,
|
||||||
|
})
|
||||||
|
).returns(Promise.resolve());
|
||||||
|
expectState(mock, MfaState);
|
||||||
|
|
||||||
|
const payload = {password: expectedPassword, rememberMe: expectedRememberMe};
|
||||||
|
|
||||||
|
return expect(state.resolve(context, payload), 'to be fulfilled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to returnUrl if any', () => {
|
||||||
|
const expectedLogin = 'foo';
|
||||||
|
const expectedPassword = 'bar';
|
||||||
|
const expectedReturnUrl = '/returnUrl';
|
||||||
|
const expectedRememberMe = true;
|
||||||
|
|
||||||
|
context.getState.returns({
|
||||||
|
auth: {
|
||||||
|
credentials: {
|
||||||
|
login: expectedLogin,
|
||||||
|
returnUrl: expectedReturnUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expectRun(
|
||||||
|
mock,
|
||||||
|
'login',
|
||||||
|
sinon.match({
|
||||||
|
login: expectedLogin,
|
||||||
|
password: expectedPassword,
|
||||||
|
rememberMe: expectedRememberMe,
|
||||||
|
})
|
||||||
|
).returns(Promise.resolve());
|
||||||
|
expectNavigate(mock, expectedReturnUrl);
|
||||||
|
|
||||||
|
const payload = {password: expectedPassword, rememberMe: expectedRememberMe};
|
||||||
|
|
||||||
|
return expect(state.resolve(context, payload), 'to be fulfilled');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#reject', () => {
|
describe('#reject', () => {
|
||||||
@ -94,10 +157,33 @@ describe('PasswordState', () => {
|
|||||||
|
|
||||||
describe('#goBack', () => {
|
describe('#goBack', () => {
|
||||||
it('should transition to login state', () => {
|
it('should transition to login state', () => {
|
||||||
|
context.getState.returns({
|
||||||
|
auth: {
|
||||||
|
credentials: {
|
||||||
|
login: 'foo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
expectRun(mock, 'setLogin', null);
|
expectRun(mock, 'setLogin', null);
|
||||||
expectState(mock, LoginState);
|
expectState(mock, LoginState);
|
||||||
|
|
||||||
state.goBack(context);
|
state.goBack(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should transition to ChooseAccountState if this is relogin', () => {
|
||||||
|
context.getState.returns({
|
||||||
|
auth: {
|
||||||
|
credentials: {
|
||||||
|
login: 'foo',
|
||||||
|
isRelogin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expectState(mock, ChooseAccountState);
|
||||||
|
|
||||||
|
state.goBack(context);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user