#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
}));
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))
.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';
/**
* @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';
/**
* @api private

View File

@ -1,5 +1,5 @@
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 bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware';
@ -22,7 +22,9 @@ export function factory(store) {
request.addMiddleware(refreshTokenMiddleware(store));
request.addMiddleware(bearerHeaderMiddleware(store));
promise = Promise.resolve().then(() => {
promise = Promise.resolve()
.then(() => store.dispatch(logoutStrangers()))
.then(() => {
const {user, accounts} = store.getState();
if (accounts.active || user.token) {
@ -31,7 +33,8 @@ export function factory(store) {
}
return Promise.reject();
}).catch(() => {
})
.catch(() => {
// the user is guest or user authentication failed
const {user} = store.getState();

View File

@ -1,4 +1,5 @@
import expect from 'unexpected';
import sinon from 'sinon';
import accounts from 'services/api/accounts';
import authentication from 'services/api/authentication';
@ -9,7 +10,8 @@ import {
activate, ACTIVATE,
remove,
reset,
logoutAll
logoutAll,
logoutStrangers
} from 'components/accounts/actions';
import { SET_LOCALE } from 'components/i18n/actions';
@ -114,6 +116,20 @@ describe('components/accounts/actions', () => {
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()', () => {
@ -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) {
window.localStorage = {
getItem(key) {
return this[key];
return this[key] || null;
},
setItem(key, value) {
this[key] = value;