Skip to content
Permalink
Browse files
feat: Invalidate access token on logout
  • Loading branch information
meltyshev committed Sep 7, 2022
1 parent 6409083 commit 8109936ce2e4c46b005c56a212e29b4a97a0758e
Show file tree
Hide file tree
Showing 26 changed files with 242 additions and 37 deletions.
@@ -44,6 +44,11 @@ const logout = () => ({
payload: {},
});

logout.invalidateAccessToken = () => ({
type: ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE,
payload: {},
});

export default {
initializeCore,
logout,
@@ -103,10 +103,11 @@ const updateUserPassword = (id, data) => ({
},
});

updateUserPassword.success = (user) => ({
updateUserPassword.success = (user, accessToken) => ({
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
payload: {
user,
accessToken,
},
});

@@ -1,9 +1,14 @@
import http from './http';
import socket from './socket';

/* Actions */

const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);

const deleteCurrentAccessToken = (headers) =>
socket.delete('/access-tokens/me', undefined, headers);

export default {
createAccessToken,
deleteCurrentAccessToken,
};
@@ -15,6 +15,7 @@ const Header = React.memo(
project,
user,
notifications,
isLogouting,
canEditProject,
canEditUsers,
onProjectSettingsClick,
@@ -75,7 +76,11 @@ const Header = React.memo(
)}
</Menu.Item>
</NotificationsPopup>
<UserPopup onSettingsClick={onUserSettingsClick} onLogout={onLogout}>
<UserPopup
isLogouting={isLogouting}
onSettingsClick={onUserSettingsClick}
onLogout={onLogout}
>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
{user.name}
</Menu.Item>
@@ -93,6 +98,7 @@ Header.propTypes = {
user: PropTypes.object.isRequired,
notifications: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isLogouting: PropTypes.bool.isRequired,
canEditProject: PropTypes.bool.isRequired,
canEditUsers: PropTypes.bool.isRequired,
onProjectSettingsClick: PropTypes.func.isRequired,
@@ -1,20 +1,31 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { Button, Menu } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Popup } from '../../lib/custom-ui';

import styles from './UserPopup.module.scss';

const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
const UserStep = React.memo(({ isLogouting, onSettingsClick, onLogout, onClose }) => {
const [t] = useTranslation();

const handleSettingsClick = useCallback(() => {
onSettingsClick();
onClose();
}, [onSettingsClick, onClose]);

let logoutMenuItemProps;
if (isLogouting) {
logoutMenuItemProps = {
as: Button,
fluid: true,
basic: true,
loading: true,
disabled: true,
};
}

return (
<>
<Popup.Header>
@@ -29,7 +40,11 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={onLogout}>
<Menu.Item
{...logoutMenuItemProps} // eslint-disable-line react/jsx-props-no-spreading
className={styles.menuItem}
onClick={onLogout}
>
{t('action.logOut', {
context: 'title',
})}
@@ -41,6 +56,7 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
});

UserStep.propTypes = {
isLogouting: PropTypes.bool.isRequired,
onSettingsClick: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
@@ -21,6 +21,7 @@ export default {

CORE_INITIALIZE: 'CORE_INITIALIZE',
LOGOUT: 'LOGOUT',
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',

/* Modals */

@@ -6,13 +6,15 @@ import entryActions from '../entry-actions';
import Header from '../components/Header';

const mapStateToProps = (state) => {
const isLogouting = selectors.selectIsLogouting(state);
const currentUser = selectors.selectCurrentUser(state);
const currentProject = selectors.selectCurrentProject(state);
const notifications = selectors.selectNotificationsForCurrentUser(state);
const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);

return {
notifications,
isLogouting,
project: currentProject,
user: currentUser,
canEditProject: isCurrentUserManager,
@@ -1,18 +1,34 @@
import { getAccessToken } from '../utils/access-token-storage';
import ActionTypes from '../constants/ActionTypes';

const initialState = {
accessToken: getAccessToken(),
userId: null,
};

// eslint-disable-next-line default-param-last
export default (state = initialState, { type, payload }) => {
switch (type) {
case ActionTypes.AUTHENTICATE__SUCCESS:
return {
...state,
accessToken: payload.accessToken,
};
case ActionTypes.SOCKET_RECONNECT_HANDLE:
case ActionTypes.CORE_INITIALIZE:
return {
...state,
userId: payload.user.id,
};
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS:
if (payload.accessToken) {
return {
...state,
accessToken: payload.accessToken,
};
}

return state;
default:
return state;
}
@@ -5,6 +5,7 @@ import ModalTypes from '../constants/ModalTypes';

const initialState = {
isInitializing: true,
isLogouting: false,
currentModal: null,
};

@@ -22,6 +23,11 @@ export default (state = initialState, { type, payload }) => {
...state,
isInitializing: false,
};
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
return {
...state,
isLogouting: true,
};
case ActionTypes.MODAL_OPEN:
return {
...state,
@@ -1,9 +1,8 @@
import { all, apply, call, fork, take } from 'redux-saga/effects';
import { all, apply, fork, take } from 'redux-saga/effects';

import watchers from './watchers';
import services from './services';
import { socket } from '../../api';
import { removeAccessToken } from '../../utils/access-token-storage';
import ActionTypes from '../../constants/ActionTypes';
import Paths from '../../constants/Paths';

@@ -15,6 +14,5 @@ export default function* coreSaga() {

yield take(ActionTypes.LOGOUT);

yield call(removeAccessToken);
window.location.href = Paths.LOGIN;
}
@@ -1,7 +1,8 @@
import { call, fork, join, put, take } from 'redux-saga/effects';
import { call, fork, join, put, select, take } from 'redux-saga/effects';

import selectors from '../../selectors';
import actions from '../../actions';
import { getAccessToken } from '../../utils/access-token-storage';
import { removeAccessToken } from '../../utils/access-token-storage';
import ErrorCodes from '../../constants/ErrorCodes';

let lastRequestTask;
@@ -13,14 +14,15 @@ function* queueRequest(method, ...args) {
} catch {} // eslint-disable-line no-empty
}

const accessToken = yield call(getAccessToken);
const accessToken = yield select(selectors.selectAccessToken);

try {
return yield call(method, ...args, {
Authorization: `Bearer ${accessToken}`,
});
} catch (error) {
if (error.code === ErrorCodes.UNAUTHORIZED) {
yield call(removeAccessToken);
yield put(actions.logout()); // TODO: next url
yield take();
}
@@ -1,8 +1,11 @@
import { call, put, take } from 'redux-saga/effects';

import request from '../request';
import requests from '../requests';
import actions from '../../../actions';
import api from '../../../api';
import i18n from '../../../i18n';
import { removeAccessToken } from '../../../utils/access-token-storage';

export function* initializeCore() {
const {
@@ -60,7 +63,17 @@ export function* changeCoreLanguage(language) {
}
}

export function* logout() {
export function* logout(invalidateAccessToken = true) {
yield call(removeAccessToken);

if (invalidateAccessToken) {
yield put(actions.logout.invalidateAccessToken());

try {
yield call(request, api.deleteCurrentAccessToken);
} catch (error) {} // eslint-disable-line no-empty
}

yield put(actions.logout());
yield take();
}
@@ -124,11 +124,13 @@ export function* updateUserPassword(id, data) {
return;
}

if (accessTokens && accessTokens[0]) {
yield call(setAccessToken, accessTokens[0]);
const accessToken = accessTokens && accessTokens[0];

if (accessToken) {
yield call(setAccessToken, accessToken);
}

yield put(actions.updateUserPassword.success(user));
yield put(actions.updateUserPassword.success(user, accessToken));
}

export function* updateCurrentUserPassword(data) {
@@ -215,7 +217,7 @@ export function* handleUserDelete(user) {
const currentUserId = yield select(selectors.selectCurrentUserId);

if (user.id === currentUserId) {
yield call(logout);
yield call(logout, false);
}

yield put(actions.handleUserDelete(user));
@@ -2,18 +2,13 @@ import { all, call, cancel, fork, take } from 'redux-saga/effects';

import watchers from './watchers';
import services from './services';
import { setAccessToken } from '../../utils/access-token-storage';
import ActionTypes from '../../constants/ActionTypes';

export default function* loginSaga() {
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));

const {
payload: { accessToken },
} = yield take(ActionTypes.AUTHENTICATE__SUCCESS);
yield take(ActionTypes.AUTHENTICATE__SUCCESS);

yield cancel(watcherTasks);

yield call(setAccessToken, accessToken);
yield call(services.goToRoot);
}
@@ -2,6 +2,7 @@ import { call, put } from 'redux-saga/effects';

import actions from '../../../actions';
import api from '../../../api';
import { setAccessToken } from '../../../utils/access-token-storage';

export function* authenticate(data) {
yield put(actions.authenticate(data));
@@ -14,6 +15,7 @@ export function* authenticate(data) {
return;
}

yield call(setAccessToken, accessToken);
yield put(actions.authenticate.success(accessToken));
}

This file was deleted.

@@ -4,8 +4,12 @@ import isUndefined from 'lodash/isUndefined';
import orm from '../orm';
import Config from '../constants/Config';

export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;

export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;

export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;

const nextPosition = (items, index, excludedId) => {
const filteredItems = isUndefined(excludedId)
? items
@@ -94,7 +98,9 @@ export const selectNextTaskPosition = createSelector(
);

export default {
selectAccessToken,
selectIsCoreInitializing,
selectIsLogouting,
selectNextBoardPosition,
selectNextListPosition,
selectNextCardPosition,
@@ -1,5 +1,4 @@
import router from './router';
import auth from './auth';
import core from './core';
import modals from './modals';
import users from './users';
@@ -14,7 +13,6 @@ import attachments from './attachments';

export default {
...router,
...auth,
...core,
...modals,
...users,

0 comments on commit 8109936

Please sign in to comment.