Skip to content

Commit 1ed4db8

Browse files
author
Martin Krulis
committed
Updating way how the user switching controls are handled (better token refreshing, button for removing user from list).
1 parent ac7b6b2 commit 1ed4db8

File tree

7 files changed

+140
-33
lines changed

7 files changed

+140
-33
lines changed

src/components/Users/UserSwitching/UserSwitching.js

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,36 @@ import { FormattedMessage } from 'react-intl';
44
import MenuTitle from '../../widgets/Sidebar/MenuTitle';
55
import MenuAvatar from '../../widgets/Sidebar/MenuAvatar';
66
import ResourceRenderer from '../../helpers/ResourceRenderer';
7+
import { safeGet } from '../../../helpers/common';
78

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

15-
{users.map(({ user: { id, fullName, name: { firstName }, avatarUrl } }) => (
16-
<MenuAvatar
17-
avatarUrl={avatarUrl}
18-
key={id}
19-
title={fullName}
20-
firstName={firstName}
21-
useGravatar={activeUser.privateData.settings.useGravatar}
22-
onClick={() => loginAs(id)}
23-
isActive={id === activeUser.id}
24-
/>
25-
))}
16+
{users.map(
17+
({
18+
user: {
19+
id,
20+
fullName,
21+
name: { firstName },
22+
avatarUrl,
23+
},
24+
}) =>
25+
id !== activeUser.id && (
26+
<MenuAvatar
27+
avatarUrl={avatarUrl}
28+
key={id}
29+
title={fullName}
30+
firstName={firstName}
31+
useGravatar={safeGet(activeUser, ['privateData', 'settings', 'useGravatar'], false)}
32+
onClick={() => loginAs(id)}
33+
onRemove={() => removeUser(id)}
34+
/>
35+
)
36+
)}
2637
</ul>
2738
) : null
2839
}
@@ -34,6 +45,7 @@ UserSwitching.propTypes = {
3445
users: PropTypes.array,
3546
currentUser: PropTypes.object.isRequired,
3647
loginAs: PropTypes.func.isRequired,
48+
removeUser: PropTypes.func.isRequired,
3749
};
3850

3951
export default UserSwitching;

src/components/widgets/Sidebar/MenuAvatar/MenuAvatar.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ import classnames from 'classnames';
44

55
import styles from '../Sidebar.less';
66
import AvatarContainer from '../../../../containers/AvatarContainer/AvatarContainer';
7+
import Icon from '../../../icons';
78

8-
const MenuAvatar = ({ title, avatarUrl, firstName, notificationsCount = 0, isActive = false, onClick }) => (
9+
const MenuAvatar = ({
10+
title,
11+
avatarUrl,
12+
firstName,
13+
notificationsCount = 0,
14+
isActive = false,
15+
onClick,
16+
onRemove = null,
17+
}) => (
918
<li
1019
className={classnames({
1120
active: isActive,
1221
})}>
1322
<a
14-
onClick={e => {
15-
e.preventDefault();
23+
onClick={ev => {
24+
ev.preventDefault();
1625
onClick();
1726
}}
1827
className={styles.cursorPointer}>
@@ -25,6 +34,21 @@ const MenuAvatar = ({ title, avatarUrl, firstName, notificationsCount = 0, isAct
2534
/>
2635
<span className={styles.menuItem}>{title}</span>
2736
{notificationsCount > 0 && <small className="label pull-right bg-yellow">{notificationsCount}</small>}
37+
38+
{onRemove && (
39+
<span className="pull-right">
40+
<Icon
41+
icon="user-slash"
42+
className="text-danger"
43+
timid
44+
gapRight
45+
onClick={ev => {
46+
ev.preventDefault();
47+
onRemove();
48+
}}
49+
/>
50+
</span>
51+
)}
2852
</a>
2953
</li>
3054
);
@@ -34,6 +58,7 @@ MenuAvatar.propTypes = {
3458
avatarUrl: PropTypes.string,
3559
firstName: PropTypes.string.isRequired,
3660
onClick: PropTypes.func,
61+
onRemove: PropTypes.func,
3762
notificationsCount: PropTypes.number,
3863
isActive: PropTypes.bool,
3964
};

src/containers/App/App.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import LayoutContainer from '../LayoutContainer';
1212
import { loggedInUserIdSelector, selectedInstanceId, accessTokenSelector } from '../../redux/selectors/auth';
1313
import { fetchUserIfNeeded } from '../../redux/modules/users';
1414
import { getUser, fetchUserStatus } from '../../redux/selectors/users';
15-
import { isTokenValid, isTokenInNeedOfRefreshment } from '../../redux/helpers/token';
15+
import { decode, isTokenValid, isTokenInNeedOfRefreshment } from '../../redux/helpers/token';
1616
import { fetchUsersInstancesIfNeeded } from '../../redux/modules/instances';
1717
import { fetchManyUserInstancesStatus } from '../../redux/selectors/instances';
1818
import { fetchAllGroups } from '../../redux/modules/groups';
@@ -21,6 +21,8 @@ import { fetchAllUserMessages } from '../../redux/modules/systemMessages';
2121
import { addNotification } from '../../redux/modules/notifications';
2222
import { logout, refresh, selectInstance } from '../../redux/modules/auth';
2323
import { getJsData, resourceStatus } from '../../redux/helpers/resourceManager';
24+
import { userSwitching } from '../../redux/selectors/userSwitching';
25+
import { refreshUserToken, removeUser as removeUserFromSwitching } from '../../redux/modules/userSwitching';
2426
import { URL_PATH_PREFIX } from '../../helpers/config';
2527
import { pathHasCustomLoadGroups } from '../../pages/routes';
2628
import { knownLocales } from '../../helpers/localizedData';
@@ -129,7 +131,18 @@ class App extends Component {
129131
* must be checked more often.
130132
*/
131133
checkAuthentication = () => {
132-
const { isLoggedIn, accessToken, refreshToken, logout, addNotification } = this.props;
134+
const {
135+
isLoggedIn,
136+
accessToken,
137+
refreshToken,
138+
logout,
139+
addNotification,
140+
userSwitchingState,
141+
userId,
142+
removeUserFromSwitching,
143+
refreshUserToken,
144+
} = this.props;
145+
133146
const token = accessToken ? accessToken.toJS() : null;
134147
if (isLoggedIn) {
135148
if (!isTokenValid(token)) {
@@ -147,6 +160,21 @@ class App extends Component {
147160
});
148161
}
149162
}
163+
164+
if (userSwitchingState) {
165+
Object.keys(userSwitchingState)
166+
.map(id => userSwitchingState[id])
167+
.forEach(({ accessToken, user, refreshing = false }) => {
168+
if (user.id !== userId && !refreshing) {
169+
const decodedToken = decode(accessToken);
170+
if (!accessToken || !isTokenValid(decodedToken)) {
171+
removeUserFromSwitching(user.id);
172+
} else if (isTokenInNeedOfRefreshment(decodedToken)) {
173+
refreshUserToken(user.id, accessToken);
174+
}
175+
}
176+
});
177+
}
150178
};
151179

152180
render() {
@@ -185,11 +213,14 @@ App.propTypes = {
185213
userId: PropTypes.string,
186214
instanceId: PropTypes.string,
187215
isLoggedIn: PropTypes.bool.isRequired,
216+
userSwitchingState: PropTypes.object,
188217
fetchUserStatus: PropTypes.string,
189218
fetchManyGroupsStatus: PropTypes.string,
190219
fetchManyUserInstancesStatus: PropTypes.string,
191220
loadAsync: PropTypes.func.isRequired,
192221
refreshToken: PropTypes.func.isRequired,
222+
refreshUserToken: PropTypes.func.isRequired,
223+
removeUserFromSwitching: PropTypes.func.isRequired,
193224
logout: PropTypes.func.isRequired,
194225
addNotification: PropTypes.func.isRequired,
195226
};
@@ -202,6 +233,7 @@ export default connect(
202233
userId,
203234
instanceId: selectedInstanceId(state),
204235
isLoggedIn: Boolean(userId),
236+
userSwitchingState: userSwitching(state),
205237
fetchUserStatus: fetchUserStatus(state, userId),
206238
fetchManyGroupsStatus: fetchManyGroupsStatus(state),
207239
fetchManyUserInstancesStatus: fetchManyUserInstancesStatus(state, userId),
@@ -210,6 +242,8 @@ export default connect(
210242
dispatch => ({
211243
loadAsync: (userId, hasCustomLoadGroups) => App.loadAsync(hasCustomLoadGroups)({}, dispatch, { userId }),
212244
refreshToken: () => dispatch(refresh()),
245+
refreshUserToken: (userId, token) => dispatch(refreshUserToken(userId, token)),
246+
removeUserFromSwitching: id => dispatch(removeUserFromSwitching(id)),
213247
logout: () => dispatch(logout()),
214248
addNotification: (msg, successful) => dispatch(addNotification(msg, successful)),
215249
})

src/containers/AvatarContainer/AvatarContainer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import { connect } from 'react-redux';
55
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
66
import { loggedInUserSelector } from '../../redux/selectors/users';
77
import Avatar, { LoadingAvatar, FailedAvatar, FakeAvatar } from '../../components/widgets/Avatar';
8+
import { safeGet } from '../../helpers/common';
89

910
const AvatarContainer = ({ currentUser, avatarUrl, fullName, firstName, size = 45, ...props }) => (
1011
<ResourceRenderer
1112
loading={<LoadingAvatar size={size} />}
1213
failed={<FailedAvatar size={size} />}
1314
resource={currentUser}>
1415
{currentUser =>
15-
currentUser.privateData.settings.useGravatar && avatarUrl !== null ? (
16+
safeGet(currentUser, ['privateData', 'settings', 'useGravatar'], false) && avatarUrl !== null ? (
1617
<Avatar size={size} src={avatarUrl} title={fullName} {...props} />
1718
) : (
1819
<FakeAvatar size={size} {...props}>

src/containers/UserSwitchingContainer/UserSwitchingContainer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
22
import { connect } from 'react-redux';
33
import UserSwitching from '../../components/Users/UserSwitching';
44

5-
import { switchUser } from '../../redux/modules/userSwitching';
5+
import { switchUser, removeUser } from '../../redux/modules/userSwitching';
66
import { loggedInUserSelector } from '../../redux/selectors/users';
77
import { usersSelector } from '../../redux/selectors/userSwitching';
88

@@ -20,5 +20,6 @@ export default connect(
2020
}),
2121
dispatch => ({
2222
loginAs: id => dispatch(switchUser(id)),
23+
removeUser: id => dispatch(removeUser(id)),
2324
})
2425
)(UserSwitching);

src/redux/modules/userSwitching.js

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@ import { handleActions, createAction } from 'redux-actions';
22
import { actionTypes as authActionTypes } from './authTypes';
33
import { decode, isTokenValid } from '../helpers/token';
44
import { addNotification } from './notifications';
5+
import { createApiAction } from '../middleware/apiMiddleware';
6+
import { safeGet } from '../../helpers/common';
57

68
export const actionTypes = {
79
REMOVE_USER: 'recodex/userSwitching/REMOVE_USER',
10+
REFRESH_TOKEN: 'recodex/userSwitching/REFRESH_TOKEN',
11+
REFRESH_TOKEN_PENDING: 'recodex/userSwitching/REFRESH_TOKEN_PENDING',
12+
REFRESH_TOKEN_FULFILLED: 'recodex/userSwitching/REFRESH_TOKEN_FULFILLED',
13+
REFRESH_TOKEN_REJECTED: 'recodex/userSwitching/REFRESH_TOKEN_REJECTED',
814
};
915

1016
export const removeUser = createAction(actionTypes.REMOVE_USER);
1117

1218
export const switchUser = userId => (dispatch, getState) => {
1319
const state = getState().userSwitching;
14-
const { user, accessToken } = state[userId] ? state[userId] : null;
20+
const { user, accessToken } = state[userId] || null;
1521
const decodedToken = decode(accessToken);
1622
if (!accessToken || !isTokenValid(decodedToken)) {
1723
dispatch(
@@ -34,21 +40,47 @@ export const switchUser = userId => (dispatch, getState) => {
3440
}
3541
};
3642

43+
export const refreshUserToken = (userId, accessToken) =>
44+
createApiAction({
45+
type: actionTypes.REFRESH_TOKEN,
46+
method: 'POST',
47+
endpoint: '/login/refresh',
48+
meta: { userId },
49+
accessToken,
50+
});
51+
3752
const initialState = {};
3853

54+
const updateUserState = (state, payload) =>
55+
safeGet(payload, ['user', 'privateData', 'isAllowed'])
56+
? {
57+
...state,
58+
[payload.user.id]: payload,
59+
}
60+
: removeUserFromState(state, payload.user.id);
61+
62+
const removeUserFromState = (state, userId) =>
63+
Object.keys(state)
64+
.filter(id => id !== userId)
65+
.reduce((acc, id) => ({ ...acc, [id]: state[id] }), {});
66+
3967
const reducer = handleActions(
4068
{
41-
[authActionTypes.LOGIN_FULFILLED]: (state, { payload }) =>
42-
state[payload.user.id]
43-
? state
44-
: {
45-
...state,
46-
[payload.user.id]: payload,
47-
},
48-
[actionTypes.REMOVE_USER]: (state, { payload: userId }) =>
49-
Object.keys(state)
50-
.filter(id => id !== userId)
51-
.reduce((acc, id) => ({ ...acc, [id]: state[id] }), {}),
69+
[actionTypes.REFRESH_TOKEN_PENDING]: (state, { meta: { userId } }) => {
70+
if (state[userId]) {
71+
const newState = { ...state };
72+
newState[userId].refreshing = true;
73+
return newState;
74+
} else {
75+
return state;
76+
}
77+
},
78+
79+
[authActionTypes.LOGIN_FULFILLED]: (state, { payload }) => updateUserState(state, payload),
80+
[actionTypes.REFRESH_TOKEN_FULFILLED]: (state, { payload }) => updateUserState(state, payload),
81+
82+
[actionTypes.REMOVE_USER]: (state, { payload: userId }) => removeUserFromState(state, userId),
83+
[actionTypes.REFRESH_TOKEN_REJECTED]: (state, { meta: { userId } }) => removeUserFromState(state, userId),
5284
},
5385
initialState
5486
);

src/redux/selectors/users.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ export const isSupervisor = userId =>
9999
role => isSupervisorRole(role)
100100
);
101101

102-
const userSettingsSelector = user =>
103-
isReady(user) ? user.getIn(['data', 'privateData', 'settings']).toJS() : EMPTY_OBJ;
102+
const userSettingsSelector = user => {
103+
const settings = isReady(user) && user.getIn(['data', 'privateData', 'settings']);
104+
return settings ? settings.toJS() : EMPTY_OBJ;
105+
};
104106

105107
export const getUserSettings = userId =>
106108
createSelector(

0 commit comments

Comments
 (0)