Skip to content

Commit

Permalink
Updating way how the user switching controls are handled (better toke…
Browse files Browse the repository at this point in the history
…n refreshing, button for removing user from list).
  • Loading branch information
Martin Krulis committed Sep 28, 2019
1 parent ac7b6b2 commit 1ed4db8
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 33 deletions.
36 changes: 24 additions & 12 deletions src/components/Users/UserSwitching/UserSwitching.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,36 @@ import { FormattedMessage } from 'react-intl';
import MenuTitle from '../../widgets/Sidebar/MenuTitle';
import MenuAvatar from '../../widgets/Sidebar/MenuAvatar';
import ResourceRenderer from '../../helpers/ResourceRenderer';
import { safeGet } from '../../../helpers/common';

const UserSwitching = ({ users = [], currentUser, loginAs, open }) => (
const UserSwitching = ({ users = [], currentUser, loginAs, removeUser }) => (
<ResourceRenderer resource={currentUser}>
{activeUser =>
users.filter(switching => switching.user.id !== activeUser.id).length > 0 ? (
<ul className="sidebar-menu">
<MenuTitle title={<FormattedMessage id="app.userSwitching.loginAs" defaultMessage="Login as" />} />

{users.map(({ user: { id, fullName, name: { firstName }, avatarUrl } }) => (
<MenuAvatar
avatarUrl={avatarUrl}
key={id}
title={fullName}
firstName={firstName}
useGravatar={activeUser.privateData.settings.useGravatar}
onClick={() => loginAs(id)}
isActive={id === activeUser.id}
/>
))}
{users.map(
({
user: {
id,
fullName,
name: { firstName },
avatarUrl,
},
}) =>
id !== activeUser.id && (
<MenuAvatar
avatarUrl={avatarUrl}
key={id}
title={fullName}
firstName={firstName}
useGravatar={safeGet(activeUser, ['privateData', 'settings', 'useGravatar'], false)}
onClick={() => loginAs(id)}
onRemove={() => removeUser(id)}
/>
)
)}
</ul>
) : null
}
Expand All @@ -34,6 +45,7 @@ UserSwitching.propTypes = {
users: PropTypes.array,
currentUser: PropTypes.object.isRequired,
loginAs: PropTypes.func.isRequired,
removeUser: PropTypes.func.isRequired,
};

export default UserSwitching;
31 changes: 28 additions & 3 deletions src/components/widgets/Sidebar/MenuAvatar/MenuAvatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@ import classnames from 'classnames';

import styles from '../Sidebar.less';
import AvatarContainer from '../../../../containers/AvatarContainer/AvatarContainer';
import Icon from '../../../icons';

const MenuAvatar = ({ title, avatarUrl, firstName, notificationsCount = 0, isActive = false, onClick }) => (
const MenuAvatar = ({
title,
avatarUrl,
firstName,
notificationsCount = 0,
isActive = false,
onClick,
onRemove = null,
}) => (
<li
className={classnames({
active: isActive,
})}>
<a
onClick={e => {
e.preventDefault();
onClick={ev => {
ev.preventDefault();
onClick();
}}
className={styles.cursorPointer}>
Expand All @@ -25,6 +34,21 @@ const MenuAvatar = ({ title, avatarUrl, firstName, notificationsCount = 0, isAct
/>
<span className={styles.menuItem}>{title}</span>
{notificationsCount > 0 && <small className="label pull-right bg-yellow">{notificationsCount}</small>}

{onRemove && (
<span className="pull-right">
<Icon
icon="user-slash"
className="text-danger"
timid
gapRight
onClick={ev => {
ev.preventDefault();
onRemove();
}}
/>
</span>
)}
</a>
</li>
);
Expand All @@ -34,6 +58,7 @@ MenuAvatar.propTypes = {
avatarUrl: PropTypes.string,
firstName: PropTypes.string.isRequired,
onClick: PropTypes.func,
onRemove: PropTypes.func,
notificationsCount: PropTypes.number,
isActive: PropTypes.bool,
};
Expand Down
38 changes: 36 additions & 2 deletions src/containers/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import LayoutContainer from '../LayoutContainer';
import { loggedInUserIdSelector, selectedInstanceId, accessTokenSelector } from '../../redux/selectors/auth';
import { fetchUserIfNeeded } from '../../redux/modules/users';
import { getUser, fetchUserStatus } from '../../redux/selectors/users';
import { isTokenValid, isTokenInNeedOfRefreshment } from '../../redux/helpers/token';
import { decode, isTokenValid, isTokenInNeedOfRefreshment } from '../../redux/helpers/token';
import { fetchUsersInstancesIfNeeded } from '../../redux/modules/instances';
import { fetchManyUserInstancesStatus } from '../../redux/selectors/instances';
import { fetchAllGroups } from '../../redux/modules/groups';
Expand All @@ -21,6 +21,8 @@ import { fetchAllUserMessages } from '../../redux/modules/systemMessages';
import { addNotification } from '../../redux/modules/notifications';
import { logout, refresh, selectInstance } from '../../redux/modules/auth';
import { getJsData, resourceStatus } from '../../redux/helpers/resourceManager';
import { userSwitching } from '../../redux/selectors/userSwitching';
import { refreshUserToken, removeUser as removeUserFromSwitching } from '../../redux/modules/userSwitching';
import { URL_PATH_PREFIX } from '../../helpers/config';
import { pathHasCustomLoadGroups } from '../../pages/routes';
import { knownLocales } from '../../helpers/localizedData';
Expand Down Expand Up @@ -129,7 +131,18 @@ class App extends Component {
* must be checked more often.
*/
checkAuthentication = () => {
const { isLoggedIn, accessToken, refreshToken, logout, addNotification } = this.props;
const {
isLoggedIn,
accessToken,
refreshToken,
logout,
addNotification,
userSwitchingState,
userId,
removeUserFromSwitching,
refreshUserToken,
} = this.props;

const token = accessToken ? accessToken.toJS() : null;
if (isLoggedIn) {
if (!isTokenValid(token)) {
Expand All @@ -147,6 +160,21 @@ class App extends Component {
});
}
}

if (userSwitchingState) {
Object.keys(userSwitchingState)
.map(id => userSwitchingState[id])
.forEach(({ accessToken, user, refreshing = false }) => {
if (user.id !== userId && !refreshing) {
const decodedToken = decode(accessToken);
if (!accessToken || !isTokenValid(decodedToken)) {
removeUserFromSwitching(user.id);
} else if (isTokenInNeedOfRefreshment(decodedToken)) {
refreshUserToken(user.id, accessToken);
}
}
});
}
};

render() {
Expand Down Expand Up @@ -185,11 +213,14 @@ App.propTypes = {
userId: PropTypes.string,
instanceId: PropTypes.string,
isLoggedIn: PropTypes.bool.isRequired,
userSwitchingState: PropTypes.object,
fetchUserStatus: PropTypes.string,
fetchManyGroupsStatus: PropTypes.string,
fetchManyUserInstancesStatus: PropTypes.string,
loadAsync: PropTypes.func.isRequired,
refreshToken: PropTypes.func.isRequired,
refreshUserToken: PropTypes.func.isRequired,
removeUserFromSwitching: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
addNotification: PropTypes.func.isRequired,
};
Expand All @@ -202,6 +233,7 @@ export default connect(
userId,
instanceId: selectedInstanceId(state),
isLoggedIn: Boolean(userId),
userSwitchingState: userSwitching(state),
fetchUserStatus: fetchUserStatus(state, userId),
fetchManyGroupsStatus: fetchManyGroupsStatus(state),
fetchManyUserInstancesStatus: fetchManyUserInstancesStatus(state, userId),
Expand All @@ -210,6 +242,8 @@ export default connect(
dispatch => ({
loadAsync: (userId, hasCustomLoadGroups) => App.loadAsync(hasCustomLoadGroups)({}, dispatch, { userId }),
refreshToken: () => dispatch(refresh()),
refreshUserToken: (userId, token) => dispatch(refreshUserToken(userId, token)),
removeUserFromSwitching: id => dispatch(removeUserFromSwitching(id)),
logout: () => dispatch(logout()),
addNotification: (msg, successful) => dispatch(addNotification(msg, successful)),
})
Expand Down
3 changes: 2 additions & 1 deletion src/containers/AvatarContainer/AvatarContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { connect } from 'react-redux';
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
import { loggedInUserSelector } from '../../redux/selectors/users';
import Avatar, { LoadingAvatar, FailedAvatar, FakeAvatar } from '../../components/widgets/Avatar';
import { safeGet } from '../../helpers/common';

const AvatarContainer = ({ currentUser, avatarUrl, fullName, firstName, size = 45, ...props }) => (
<ResourceRenderer
loading={<LoadingAvatar size={size} />}
failed={<FailedAvatar size={size} />}
resource={currentUser}>
{currentUser =>
currentUser.privateData.settings.useGravatar && avatarUrl !== null ? (
safeGet(currentUser, ['privateData', 'settings', 'useGravatar'], false) && avatarUrl !== null ? (
<Avatar size={size} src={avatarUrl} title={fullName} {...props} />
) : (
<FakeAvatar size={size} {...props}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import UserSwitching from '../../components/Users/UserSwitching';

import { switchUser } from '../../redux/modules/userSwitching';
import { switchUser, removeUser } from '../../redux/modules/userSwitching';
import { loggedInUserSelector } from '../../redux/selectors/users';
import { usersSelector } from '../../redux/selectors/userSwitching';

Expand All @@ -20,5 +20,6 @@ export default connect(
}),
dispatch => ({
loginAs: id => dispatch(switchUser(id)),
removeUser: id => dispatch(removeUser(id)),
})
)(UserSwitching);
56 changes: 44 additions & 12 deletions src/redux/modules/userSwitching.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ import { handleActions, createAction } from 'redux-actions';
import { actionTypes as authActionTypes } from './authTypes';
import { decode, isTokenValid } from '../helpers/token';
import { addNotification } from './notifications';
import { createApiAction } from '../middleware/apiMiddleware';
import { safeGet } from '../../helpers/common';

export const actionTypes = {
REMOVE_USER: 'recodex/userSwitching/REMOVE_USER',
REFRESH_TOKEN: 'recodex/userSwitching/REFRESH_TOKEN',
REFRESH_TOKEN_PENDING: 'recodex/userSwitching/REFRESH_TOKEN_PENDING',
REFRESH_TOKEN_FULFILLED: 'recodex/userSwitching/REFRESH_TOKEN_FULFILLED',
REFRESH_TOKEN_REJECTED: 'recodex/userSwitching/REFRESH_TOKEN_REJECTED',
};

export const removeUser = createAction(actionTypes.REMOVE_USER);

export const switchUser = userId => (dispatch, getState) => {
const state = getState().userSwitching;
const { user, accessToken } = state[userId] ? state[userId] : null;
const { user, accessToken } = state[userId] || null;
const decodedToken = decode(accessToken);
if (!accessToken || !isTokenValid(decodedToken)) {
dispatch(
Expand All @@ -34,21 +40,47 @@ export const switchUser = userId => (dispatch, getState) => {
}
};

export const refreshUserToken = (userId, accessToken) =>
createApiAction({
type: actionTypes.REFRESH_TOKEN,
method: 'POST',
endpoint: '/login/refresh',
meta: { userId },
accessToken,
});

const initialState = {};

const updateUserState = (state, payload) =>
safeGet(payload, ['user', 'privateData', 'isAllowed'])
? {
...state,
[payload.user.id]: payload,
}
: removeUserFromState(state, payload.user.id);

const removeUserFromState = (state, userId) =>
Object.keys(state)
.filter(id => id !== userId)
.reduce((acc, id) => ({ ...acc, [id]: state[id] }), {});

const reducer = handleActions(
{
[authActionTypes.LOGIN_FULFILLED]: (state, { payload }) =>
state[payload.user.id]
? state
: {
...state,
[payload.user.id]: payload,
},
[actionTypes.REMOVE_USER]: (state, { payload: userId }) =>
Object.keys(state)
.filter(id => id !== userId)
.reduce((acc, id) => ({ ...acc, [id]: state[id] }), {}),
[actionTypes.REFRESH_TOKEN_PENDING]: (state, { meta: { userId } }) => {
if (state[userId]) {
const newState = { ...state };
newState[userId].refreshing = true;
return newState;
} else {
return state;
}
},

[authActionTypes.LOGIN_FULFILLED]: (state, { payload }) => updateUserState(state, payload),
[actionTypes.REFRESH_TOKEN_FULFILLED]: (state, { payload }) => updateUserState(state, payload),

[actionTypes.REMOVE_USER]: (state, { payload: userId }) => removeUserFromState(state, userId),
[actionTypes.REFRESH_TOKEN_REJECTED]: (state, { meta: { userId } }) => removeUserFromState(state, userId),
},
initialState
);
Expand Down
6 changes: 4 additions & 2 deletions src/redux/selectors/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ export const isSupervisor = userId =>
role => isSupervisorRole(role)
);

const userSettingsSelector = user =>
isReady(user) ? user.getIn(['data', 'privateData', 'settings']).toJS() : EMPTY_OBJ;
const userSettingsSelector = user => {
const settings = isReady(user) && user.getIn(['data', 'privateData', 'settings']);
return settings ? settings.toJS() : EMPTY_OBJ;
};

export const getUserSettings = userId =>
createSelector(
Expand Down

0 comments on commit 1ed4db8

Please sign in to comment.