Improve oauth state restore logic

This commit is contained in:
SleepWalker 2016-08-27 13:19:02 +03:00
parent fed52bb1c6
commit fdd56bc886
3 changed files with 66 additions and 24 deletions

View File

@ -162,12 +162,18 @@ export function oAuthValidate(oauthData) {
); );
} }
/**
* @param {object} params
* @param {bool} params.accept=false
*
* @return {Promise}
*/
export function oAuthComplete(params = {}) { export function oAuthComplete(params = {}) {
localStorage.removeItem('oauthData');
return wrapInLoader((dispatch, getState) => return wrapInLoader((dispatch, getState) =>
oauth.complete(getState().auth.oauth, params) oauth.complete(getState().auth.oauth, params)
.then((resp) => { .then((resp) => {
localStorage.removeItem('oauthData');
if (resp.redirectUri.startsWith('static_page')) { if (resp.redirectUri.startsWith('static_page')) {
resp.code = resp.redirectUri.match(/code=(.+)&/)[1]; resp.code = resp.redirectUri.match(/code=(.+)&/)[1];
resp.redirectUri = resp.redirectUri.match(/^(.+)\?/)[1]; resp.redirectUri = resp.redirectUri.match(/^(.+)\?/)[1];
@ -195,6 +201,7 @@ export function oAuthComplete(params = {}) {
function handleOauthParamsValidation(resp = {}) { function handleOauthParamsValidation(resp = {}) {
dispatchBsod(); dispatchBsod();
localStorage.removeItem('oauthData');
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
resp.userMessage && setTimeout(() => alert(resp.userMessage), 500); // 500 ms to allow re-render resp.userMessage && setTimeout(() => alert(resp.userMessage), 500); // 500 ms to allow re-render

View File

@ -6,6 +6,7 @@ import OAuthState from './OAuthState';
import ForgotPasswordState from './ForgotPasswordState'; 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 ResendActivationState from './ResendActivationState'; import ResendActivationState from './ResendActivationState';
export default class AuthFlow { export default class AuthFlow {
@ -39,8 +40,6 @@ export default class AuthFlow {
this.getState = store.getState.bind(store); this.getState = store.getState.bind(store);
this.dispatch = store.dispatch.bind(store); this.dispatch = store.dispatch.bind(store);
this.restoreOAuthState();
} }
resolve(payload = {}) { resolve(payload = {}) {
@ -131,6 +130,10 @@ export default class AuthFlow {
this.currentRequest = request; this.currentRequest = request;
if (this.restoreOAuthState()) {
return;
}
switch (path) { switch (path) {
case '/register': case '/register':
this.setState(new RegisterState()); this.setState(new RegisterState());
@ -176,15 +179,25 @@ export default class AuthFlow {
} }
/** /**
* Tries to restore last oauth request, if it was stored in localStorage
* in last 2 hours
* @api private * @api private
*
* @return {bool} - whether oauth state is being restored
*/ */
restoreOAuthState() { restoreOAuthState() {
try { try {
const data = JSON.parse(localStorage.getItem('oauthData')); const data = JSON.parse(localStorage.getItem('oauthData'));
const expirationTime = 2 * 60 * 60 * 1000; // 2h
if (Date.now() - data.timestamp < 60 * 60 * 1000) { if (Date.now() - data.timestamp < expirationTime) {
this.run('oAuthValidate', data.payload); this.run('oAuthValidate', data.payload)
.then(() => this.setState(new CompleteState()));
return true;
} }
} catch (err) {/* bad luck :( */} } catch (err) {/* bad luck :( */}
return false;
} }
} }

View File

@ -10,6 +10,7 @@ import ForgotPasswordState from 'services/authFlow/ForgotPasswordState';
import ActivationState from 'services/authFlow/ActivationState'; 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';
describe('AuthFlow', () => { describe('AuthFlow', () => {
let flow; let flow;
@ -34,10 +35,6 @@ describe('AuthFlow', () => {
}); });
describe('#setStore', () => { describe('#setStore', () => {
afterEach(() => {
localStorage.removeItem('oauthData');
});
it('should create #navigate, #getState, #dispatch', () => { it('should create #navigate, #getState, #dispatch', () => {
flow.setStore({ flow.setStore({
getState() {}, getState() {},
@ -48,39 +45,64 @@ describe('AuthFlow', () => {
expect(flow.dispatch, 'to be defined'); expect(flow.dispatch, 'to be defined');
expect(flow.navigate, 'to be defined'); expect(flow.navigate, 'to be defined');
}); });
});
it('should restore oauth state from localStorage', () => { describe('#restoreOAuthState', () => {
const oauthData = {}; let oauthData;
beforeEach(() => {
oauthData = {foo: 'bar'};
localStorage.setItem('oauthData', JSON.stringify({ localStorage.setItem('oauthData', JSON.stringify({
timestamp: Date.now() - 10, timestamp: Date.now() - 10,
payload: oauthData payload: oauthData
})); }));
sinon.stub(flow, 'run').named('flow.run'); sinon.stub(flow, 'run').named('flow.run');
flow.run.returns({then: (fn) => fn()});
sinon.stub(flow, 'setState').named('flow.setState');
});
flow.setStore({ afterEach(() => {
getState() {}, localStorage.removeItem('oauthData');
dispatch() {} });
});
it('should call to restoreOAuthState', () => {
sinon.stub(flow, 'restoreOAuthState').named('flow.restoreOAuthState');
flow.handleRequest({path: '/'});
expect(flow.restoreOAuthState, 'was called');
});
it('should restore oauth state from localStorage', () => {
flow.handleRequest({path: '/'});
expect(flow.run, 'to have a call satisfying', [ expect(flow.run, 'to have a call satisfying', [
'oAuthValidate', oauthData 'oAuthValidate', oauthData
]); ]);
}); });
it('should transition to CompleteState', () => {
flow.handleRequest({path: '/'});
expect(flow.setState, 'to have a call satisfying', [
expect.it('to be a', CompleteState)
]);
});
it('should not handle current request', () => {
flow.handleRequest({path: '/'});
expect(flow.setState, 'was called once');
});
it('should not restore outdated (>1h) oauth state', () => { it('should not restore outdated (>1h) oauth state', () => {
const oauthData = {};
localStorage.setItem('oauthData', JSON.stringify({ localStorage.setItem('oauthData', JSON.stringify({
timestamp: Date.now() - 60 * 60 * 1000, timestamp: Date.now() - 2 * 60 * 60 * 1000,
payload: oauthData payload: oauthData
})); }));
sinon.stub(flow, 'run').named('flow.run'); flow.handleRequest({path: '/'});
flow.setStore({
getState() {},
dispatch() {}
});
expect(flow.run, 'was not called'); expect(flow.run, 'was not called');
}); });