#232: fix remember me logic on frontend

This commit is contained in:
SleepWalker 2016-12-05 21:14:38 +02:00
parent 70ae13ae84
commit 9da79a15b4
4 changed files with 188 additions and 25 deletions

View File

@ -49,6 +49,11 @@ export function authenticate({token, refreshToken}) {
...user ...user
})); }));
if (!account.refreshToken) {
// mark user as stranger (user does not want us to remember his account)
sessionStorage.setItem(`stranger${account.id}`, 1);
}
return dispatch(setLocale(user.lang)) return dispatch(setLocale(user.lang))
.then(() => account); .then(() => account);
}); });
@ -75,6 +80,48 @@ export function revoke(account) {
}; };
} }
export function logoutAll() {
return (dispatch, getState) => {
const {accounts: {available}} = getState();
available.forEach((account) => authentication.logout(account));
dispatch(reset());
};
}
/**
* Logouts accounts, that was marked as "do not remember me"
*
* We detecting foreign accounts by the absence of refreshToken. The account
* won't be removed, until key `stranger${account.id}` is present in sessionStorage
*
* @return {function}
*/
export function logoutStrangers() {
return (dispatch, getState) => {
const {accounts: {available}} = getState();
const isStranger = ({refreshToken, id}) => !refreshToken && !sessionStorage.getItem(`stranger${id}`);
const accountToReplace = available.filter((account) => !isStranger(account))[0];
if (accountToReplace) {
available.filter(isStranger)
.forEach((account) => {
dispatch(remove(account));
authentication.logout(account);
});
return dispatch(authenticate(accountToReplace));
}
dispatch(logout());
return Promise.resolve();
};
}
export const ADD = 'accounts:add'; export const ADD = 'accounts:add';
/** /**
* @api private * @api private
@ -120,15 +167,6 @@ export function activate(account) {
}; };
} }
export function logoutAll() {
return (dispatch, getState) => {
const {accounts: {available}} = getState();
available.forEach((account) => authentication.logout(account));
dispatch(reset());
};
}
export const RESET = 'accounts:reset'; export const RESET = 'accounts:reset';
/** /**
* @api private * @api private

View File

@ -1,5 +1,5 @@
import { changeLang } from 'components/user/actions'; import { changeLang } from 'components/user/actions';
import { authenticate } from 'components/accounts/actions'; import { authenticate, logoutStrangers } from 'components/accounts/actions';
import request from 'services/request'; import request from 'services/request';
import bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware'; import bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware';
@ -22,22 +22,25 @@ export function factory(store) {
request.addMiddleware(refreshTokenMiddleware(store)); request.addMiddleware(refreshTokenMiddleware(store));
request.addMiddleware(bearerHeaderMiddleware(store)); request.addMiddleware(bearerHeaderMiddleware(store));
promise = Promise.resolve().then(() => { promise = Promise.resolve()
const {user, accounts} = store.getState(); .then(() => store.dispatch(logoutStrangers()))
.then(() => {
const {user, accounts} = store.getState();
if (accounts.active || user.token) { if (accounts.active || user.token) {
// authorizing user if it is possible // authorizing user if it is possible
return store.dispatch(authenticate(accounts.active || user)); return store.dispatch(authenticate(accounts.active || user));
} }
return Promise.reject(); return Promise.reject();
}).catch(() => { })
// the user is guest or user authentication failed .catch(() => {
const {user} = store.getState(); // the user is guest or user authentication failed
const {user} = store.getState();
// auto-detect guest language // auto-detect guest language
return store.dispatch(changeLang(user.lang)); return store.dispatch(changeLang(user.lang));
}); });
return promise; return promise;
} }

View File

@ -1,4 +1,5 @@
import expect from 'unexpected'; import expect from 'unexpected';
import sinon from 'sinon';
import accounts from 'services/api/accounts'; import accounts from 'services/api/accounts';
import authentication from 'services/api/authentication'; import authentication from 'services/api/authentication';
@ -9,7 +10,8 @@ import {
activate, ACTIVATE, activate, ACTIVATE,
remove, remove,
reset, reset,
logoutAll logoutAll,
logoutStrangers
} from 'components/accounts/actions'; } from 'components/accounts/actions';
import { SET_LOCALE } from 'components/i18n/actions'; import { SET_LOCALE } from 'components/i18n/actions';
@ -114,6 +116,20 @@ describe('components/accounts/actions', () => {
expect(dispatch, 'was not called') expect(dispatch, 'was not called')
); );
}); });
it('marks user as stranger, if there is no refreshToken', () => {
const expectedKey = `stranger${account.id}`;
authentication.validateToken.returns(Promise.resolve({
token: account.token
}));
sessionStorage.removeItem(expectedKey);
return authenticate(account)(dispatch).then(() => {
expect(sessionStorage.getItem(expectedKey), 'not to be null')
sessionStorage.removeItem(expectedKey);
});
});
}); });
describe('#revoke()', () => { describe('#revoke()', () => {
@ -242,4 +258,110 @@ describe('components/accounts/actions', () => {
]); ]);
}); });
}); });
describe('#logoutStrangers', () => {
const foreignAccount = {
...account,
id: 2,
refreshToken: undefined
};
const foreignAccount2 = {
...foreignAccount,
id: 3
};
beforeEach(() => {
getState.returns({
accounts: {
active: account,
available: [account, foreignAccount, foreignAccount2]
},
user
});
sinon.stub(authentication, 'logout').named('authentication.logout');
});
afterEach(() => {
authentication.logout.restore();
});
it('should remove stranger accounts', () => {
logoutStrangers()(dispatch, getState);
expect(dispatch, 'to have a call satisfying', [
remove(foreignAccount)
]);
expect(dispatch, 'to have a call satisfying', [
remove(foreignAccount2)
]);
});
it('should logout stranger accounts', () => {
logoutStrangers()(dispatch, getState);
expect(authentication.logout, 'to have calls satisfying', [
[foreignAccount],
[foreignAccount2]
]);
});
it('should activate another account if available', () =>
logoutStrangers()(dispatch, getState)
.then(() =>
expect(dispatch, 'to have a call satisfying', [
activate(account)
])
)
);
describe('when all accounts are strangers', () => {
beforeEach(() => {
getState.returns({
accounts: {
active: foreignAccount,
available: [foreignAccount, foreignAccount2]
},
user
});
logoutStrangers()(dispatch, getState);
});
it('logouts all accounts', () => {
expect(dispatch, 'to have a call satisfying', [
{payload: {isGuest: true}}
// updateUser({isGuest: true})
]);
expect(dispatch, 'to have a call satisfying', [
reset()
]);
});
});
describe('when an stranger has a mark in sessionStorage', () => {
const key = `stranger${foreignAccount.id}`;
beforeEach(() => {
sessionStorage.setItem(key, 1);
logoutStrangers()(dispatch, getState);
});
afterEach(() => {
sessionStorage.removeItem(key);
});
it('should not log out', () =>
expect(dispatch, 'to have calls satisfying', [
[expect.it('not to equal', {payload: foreignAccount})],
// for some reason it says, that dispatch(authenticate(...))
// must be removed if only one args assertion is listed :(
[expect.it('not to equal', {payload: foreignAccount})]
])
);
});
});
}); });

View File

@ -6,7 +6,7 @@ expect.use(require('unexpected-sinon'));
if (!window.localStorage) { if (!window.localStorage) {
window.localStorage = { window.localStorage = {
getItem(key) { getItem(key) {
return this[key]; return this[key] || null;
}, },
setItem(key, value) { setItem(key, value) {
this[key] = value; this[key] = value;