diff --git a/app/soapbox/reducers/__tests__/auth-test.js b/app/soapbox/reducers/__tests__/auth-test.js index a149042c4..77a9b1965 100644 --- a/app/soapbox/reducers/__tests__/auth-test.js +++ b/app/soapbox/reducers/__tests__/auth-test.js @@ -8,6 +8,7 @@ import { VERIFY_CREDENTIALS_FAIL, SWITCH_ACCOUNT, } from 'soapbox/actions/auth'; +import { ME_FETCH_SKIP } from 'soapbox/actions/me'; describe('auth reducer', () => { it('should return the initial state', () => { @@ -250,4 +251,13 @@ describe('auth reducer', () => { expect(result.get('me')).toEqual('5678'); }); }); + + describe('ME_FETCH_SKIP', () => { + it('sets `me` to null', () => { + const state = fromJS({ me: '1234' }); + const action = { type: ME_FETCH_SKIP }; + const result = reducer(state, action); + expect(result.get('me')).toEqual(null); + }); + }); }); diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 5985939da..29bc8fcd8 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -7,7 +7,8 @@ import { VERIFY_CREDENTIALS_SUCCESS, VERIFY_CREDENTIALS_FAIL, } from '../actions/auth'; -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { ME_FETCH_SKIP } from '../actions/me'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const defaultState = ImmutableMap({ app: ImmutableMap(), @@ -16,31 +17,50 @@ const defaultState = ImmutableMap({ me: null, }); +const validId = id => typeof id === 'string' && id !== 'null' && id !== 'undefined'; + const getSessionUser = () => { const id = sessionStorage.getItem('soapbox:auth:me'); - if (id && typeof id === 'string' && id !== 'null' && id !== 'undefined') { - return id; - } else { - return undefined; - } + return validId(id) ? id : undefined; }; const sessionUser = getSessionUser(); const localState = fromJS(JSON.parse(localStorage.getItem('soapbox:auth'))); +// Checks if the user has an ID and access token +const validUser = user => { + try { + return validId(user.get('id')) && validId(user.get('access_token')); + } catch(e) { + return false; + } +}; + +// Finds the first valid user in the state +const firstValidUser = state => state.get('users', ImmutableMap()).find(validUser); + // If `me` doesn't match an existing user, attempt to shift it. const maybeShiftMe = state => { const users = state.get('users', ImmutableMap()); const me = state.get('me'); - if (!users.get(me)) { - return state.set('me', users.first(ImmutableMap()).get('id', null)); + if (!validUser(users.get(me))) { + const nextUser = firstValidUser(state); + return state.set('me', nextUser ? nextUser.get('id') : null); } else { return state; } }; -const setSessionUser = state => state.update('me', null, me => sessionUser || me); +// Set the user from the session or localStorage, whichever is valid first +const setSessionUser = state => state.update('me', null, me => { + const user = ImmutableList([ + state.getIn(['users', sessionUser]), + state.getIn(['users', me]), + ]).find(validUser); + + return user ? user.get('id') : null; +}); // Upgrade the initial state const migrateLegacy = state => { @@ -63,6 +83,24 @@ const migrateLegacy = state => { }); }; +// Checks the state and makes it valid +const sanitizeState = state => { + return state.withMutations(state => { + // Remove invalid users, ensure ID match + state.update('users', ImmutableMap(), users => ( + users.filter((user, id) => ( + validUser(user) && user.get('id') === id + )) + )); + // Remove mismatched tokens + state.update('tokens', ImmutableMap(), tokens => ( + tokens.filter((token, id) => ( + validId(id) && token.get('access_token') === id + )) + )); + }); +}; + const persistAuth = state => localStorage.setItem('soapbox:auth', JSON.stringify(state.toJS())); const persistSession = state => { @@ -82,6 +120,7 @@ const initialize = state => { maybeShiftMe(state); setSessionUser(state); migrateLegacy(state); + sanitizeState(state); persistState(state); }); }; @@ -158,6 +197,8 @@ const reducer = (state, action) => { return action.error.response.status === 403 ? deleteToken(state, action.token) : state; case SWITCH_ACCOUNT: return state.set('me', action.accountId); + case ME_FETCH_SKIP: + return state.set('me', null); default: return state; }