• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.47
/src/js/actions/userActions.js
1
'use strict';
2

3
// Copyright 2019 Northern.tech AS
4
//
5
//    Licensed under the Apache License, Version 2.0 (the "License");
6
//    you may not use this file except in compliance with the License.
7
//    You may obtain a copy of the License at
8
//
9
//        http://www.apache.org/licenses/LICENSE-2.0
10
//
11
//    Unless required by applicable law or agreed to in writing, software
12
//    distributed under the License is distributed on an "AS IS" BASIS,
13
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
//    See the License for the specific language governing permissions and
15
//    limitations under the License.
16
import hashString from 'md5';
17
import Cookies from 'universal-cookie';
18

19
import GeneralApi, { apiRoot } from '../api/general-api';
20
import UsersApi from '../api/users-api';
21
import { cleanUp, maxSessionAge, setSessionInfo } from '../auth';
22
import { HELPTOOLTIPS } from '../components/helptips/helptooltips';
23
import * as AppConstants from '../constants/appConstants';
24
import * as UserConstants from '../constants/userConstants';
25
import { duplicateFilter, extractErrorMessage, isEmpty, preformatWithRequestID } from '../helpers';
26
import { getCurrentUser, getOnboardingState, getTooltipsState, getUserSettings as getUserSettingsSelector } from '../selectors';
27
import { clearAllRetryTimers } from '../utils/retrytimer';
28
import { commonErrorFallback, commonErrorHandler, initializeAppData, setOfflineThreshold, setSnackbar } from './appActions';
29

30
const cookies = new Cookies();
183✔
31
const {
32
  defaultPermissionSets,
33
  emptyRole,
34
  emptyUiPermissions,
35
  itemUiPermissionsReducer,
36
  OWN_USER_ID,
37
  PermissionTypes,
38
  rolesById: defaultRolesById,
39
  scopedPermissionAreas,
40
  twoFAStates,
41
  uiPermissionsByArea,
42
  uiPermissionsById,
43
  useradmApiUrl,
44
  useradmApiUrlv2
45
} = UserConstants;
183✔
46

47
const handleLoginError = (err, has2FA) => dispatch => {
183✔
48
  const errorText = extractErrorMessage(err);
3✔
49
  const is2FABackend = errorText.includes('2fa');
3✔
50
  if (is2FABackend && !has2FA) {
3!
51
    return Promise.reject({ error: '2fa code missing' });
3✔
52
  }
53
  const twoFAError = is2FABackend ? ' and verification code' : '';
×
54
  const errorMessage = `There was a problem logging in. Please check your email${
×
55
    twoFAError ? ',' : ' and'
×
56
  } password${twoFAError}. If you still have problems, contact an administrator.`;
57
  return Promise.reject(dispatch(setSnackbar(preformatWithRequestID(err.response, errorMessage), null, 'Copy to clipboard')));
×
58
};
59

60
/*
61
  User management
62
*/
63
export const loginUser = (userData, stayLoggedIn) => dispatch =>
183✔
64
  UsersApi.postLogin(`${useradmApiUrl}/auth/login`, { ...userData, no_expiry: stayLoggedIn })
7✔
65
    .catch(err => {
66
      cleanUp();
3✔
67
      return Promise.resolve(dispatch(handleLoginError(err, userData['token2fa'])));
3✔
68
    })
69
    .then(res => {
70
      const token = res.text;
3✔
71
      if (!token) {
3!
72
        return;
×
73
      }
74
      // save token to local storage & set maxAge if noexpiry checkbox not checked
75
      let now = new Date();
3✔
76
      now.setSeconds(now.getSeconds() + maxSessionAge);
3✔
77
      const expiresAt = stayLoggedIn ? undefined : now.toISOString();
3!
78
      setSessionInfo({ token, expiresAt });
3✔
79
      cookies.remove('JWT', { path: '/' });
3✔
80
      return dispatch(getUser(OWN_USER_ID))
3✔
81
        .catch(e => {
82
          cleanUp();
1✔
83
          return Promise.reject(dispatch(setSnackbar(extractErrorMessage(e))));
1✔
84
        })
85
        .then(() => {
86
          window.sessionStorage.removeItem('pendings-redirect');
2✔
87
          if (window.location.pathname !== '/ui/') {
2!
88
            window.location.replace('/ui/');
2✔
89
          }
90
          return Promise.all([dispatch({ type: UserConstants.SUCCESSFULLY_LOGGED_IN, value: { expiresAt, token } })]);
2✔
91
        });
92
    });
93

94
export const logoutUser = () => (dispatch, getState) => {
183✔
95
  if (Object.keys(getState().app.uploadsById).length) {
12!
96
    return Promise.reject();
×
97
  }
98
  return GeneralApi.post(`${useradmApiUrl}/auth/logout`).finally(() => {
12✔
99
    cleanUp();
10✔
100
    clearAllRetryTimers(setSnackbar);
10✔
101
    return Promise.resolve(dispatch({ type: UserConstants.USER_LOGOUT }));
10✔
102
  });
103
};
104

105
export const passwordResetStart = email => dispatch =>
183✔
106
  GeneralApi.post(`${useradmApiUrl}/auth/password-reset/start`, { email }).catch(err =>
2✔
107
    commonErrorHandler(err, `The password reset request cannot be processed:`, dispatch, undefined, true)
×
108
  );
109

110
export const passwordResetComplete = (secretHash, newPassword) => dispatch =>
183✔
111
  GeneralApi.post(`${useradmApiUrl}/auth/password-reset/complete`, { secret_hash: secretHash, password: newPassword }).catch((err = {}) => {
2!
112
    const { error, response = {} } = err;
×
113
    let errorMsg = '';
×
114
    if (response.status == 400) {
×
115
      errorMsg = 'the link you are using expired or the request is not valid, please try again.';
×
116
    } else {
117
      errorMsg = error;
×
118
    }
119
    dispatch(setSnackbar('The password reset request cannot be processed: ' + errorMsg));
×
120
    return Promise.reject(err);
×
121
  });
122

123
export const verifyEmailStart = () => (dispatch, getState) =>
183✔
124
  GeneralApi.post(`${useradmApiUrl}/auth/verify-email/start`, { email: getCurrentUser(getState()).email })
1✔
125
    .catch(err => commonErrorHandler(err, 'An error occured starting the email verification process:', dispatch))
×
126
    .finally(() => Promise.resolve(dispatch(getUser(OWN_USER_ID))));
1✔
127

128
export const setAccountActivationCode = code => dispatch => Promise.resolve(dispatch({ type: UserConstants.RECEIVED_ACTIVATION_CODE, code }));
183✔
129

130
export const verifyEmailComplete = secret => dispatch =>
183✔
131
  GeneralApi.post(`${useradmApiUrl}/auth/verify-email/complete`, { secret_hash: secret })
2✔
132
    .catch(err => commonErrorHandler(err, 'An error occured completing the email verification process:', dispatch))
1✔
133
    .finally(() => Promise.resolve(dispatch(getUser(OWN_USER_ID))));
2✔
134

135
export const verify2FA = tfaData => dispatch =>
183✔
136
  UsersApi.putVerifyTFA(`${useradmApiUrl}/2faverify`, tfaData)
2✔
137
    .then(() => Promise.resolve(dispatch(getUser(OWN_USER_ID))))
2✔
138
    .catch(err =>
139
      commonErrorHandler(err, 'An error occured validating the verification code: failed to verify token, please try again.', dispatch, undefined, true)
×
140
    );
141

142
export const getUserList = () => dispatch =>
183✔
143
  GeneralApi.get(`${useradmApiUrl}/users`)
18✔
144
    .then(res => {
145
      const users = res.data.reduce((accu, item) => {
15✔
146
        accu[item.id] = item;
30✔
147
        return accu;
30✔
148
      }, {});
149
      return dispatch({ type: UserConstants.RECEIVED_USER_LIST, users });
15✔
150
    })
151
    .catch(err => commonErrorHandler(err, `Users couldn't be loaded.`, dispatch, commonErrorFallback));
×
152

153
export const getUser = id => dispatch =>
183✔
154
  GeneralApi.get(`${useradmApiUrl}/users/${id}`).then(({ data: user }) =>
13✔
155
    Promise.all([
12✔
156
      dispatch({ type: UserConstants.RECEIVED_USER, user }),
157
      dispatch(setHideAnnouncement(false, user.id)),
158
      dispatch(updateUserColumnSettings(undefined, user.id)),
159
      user
160
    ])
161
  );
162

163
export const initializeSelf = () => dispatch => dispatch(getUser(UserConstants.OWN_USER_ID)).then(() => dispatch(initializeAppData()));
183✔
164

165
export const updateUserColumnSettings = (columns, currentUserId) => (dispatch, getState) => {
183✔
166
  const userId = currentUserId ?? getCurrentUser(getState()).id;
15✔
167
  const storageKey = `${userId}-column-widths`;
15✔
168
  let customColumns = [];
15✔
169
  if (!columns) {
15✔
170
    try {
13✔
171
      customColumns = JSON.parse(window.localStorage.getItem(storageKey)) || customColumns;
13!
172
    } catch {
173
      // most likely the column info doesn't exist yet or is lost - continue
174
    }
175
  } else {
176
    customColumns = columns;
2✔
177
  }
178
  window.localStorage.setItem(storageKey, JSON.stringify(customColumns));
15✔
179
  return Promise.resolve(dispatch({ type: UserConstants.SET_CUSTOM_COLUMNS, value: customColumns }));
15✔
180
};
181

182
const actions = {
183✔
183
  create: {
184
    successMessage: 'The user was created successfully.',
185
    errorMessage: 'creating'
186
  },
187
  edit: {
188
    successMessage: 'The user has been updated.',
189
    errorMessage: 'editing'
190
  },
191
  remove: {
192
    successMessage: 'The user was removed from the system.',
193
    errorMessage: 'removing'
194
  }
195
};
196

197
const userActionErrorHandler = (err, type, dispatch) => commonErrorHandler(err, `There was an error ${actions[type].errorMessage} the user.`, dispatch);
183✔
198

199
export const createUser = userData => dispatch =>
183✔
200
  GeneralApi.post(`${useradmApiUrl}/users`, userData)
2✔
201
    .then(() =>
202
      Promise.all([
2✔
203
        dispatch({ type: UserConstants.CREATED_USER, user: userData }),
204
        dispatch(getUserList()),
205
        dispatch(setSnackbar(actions.create.successMessage))
206
      ])
207
    )
208
    .catch(err => userActionErrorHandler(err, 'create', dispatch));
×
209

210
export const removeUser = userId => dispatch =>
183✔
211
  GeneralApi.delete(`${useradmApiUrl}/users/${userId}`)
2✔
212
    .then(() =>
213
      Promise.all([dispatch({ type: UserConstants.REMOVED_USER, userId }), dispatch(getUserList()), dispatch(setSnackbar(actions.remove.successMessage))])
2✔
214
    )
215
    .catch(err => userActionErrorHandler(err, 'remove', dispatch));
×
216

217
export const editUser = (userId, userData) => (dispatch, getState) => {
183✔
218
  return GeneralApi.put(`${useradmApiUrl}/users/${userId}`, userData).then(() =>
3✔
219
    Promise.all([
3✔
220
      dispatch({ type: UserConstants.UPDATED_USER, userId: userId === UserConstants.OWN_USER_ID ? getState().users.currentUser : userId, user: userData }),
3!
221
      dispatch(setSnackbar(actions.edit.successMessage))
222
    ])
223
  );
224
};
225

226
export const enableUser2fa =
227
  (userId = OWN_USER_ID) =>
183✔
228
  dispatch =>
2✔
229
    GeneralApi.post(`${useradmApiUrl}/users/${userId}/2fa/enable`)
2✔
230
      .catch(err => commonErrorHandler(err, `There was an error enabling Two Factor authentication for the user.`, dispatch))
×
231
      .then(() => Promise.resolve(dispatch(getUser(userId))));
2✔
232

233
export const disableUser2fa =
234
  (userId = OWN_USER_ID) =>
183!
235
  dispatch =>
1✔
236
    GeneralApi.post(`${useradmApiUrl}/users/${userId}/2fa/disable`)
1✔
237
      .catch(err => commonErrorHandler(err, `There was an error disabling Two Factor authentication for the user.`, dispatch))
×
238
      .then(() => Promise.resolve(dispatch(getUser(userId))));
1✔
239

240
/* RBAC related things follow:  */
241

242
const mergePermissions = (existingPermissions = { ...emptyUiPermissions }, addedPermissions) =>
183!
243
  Object.entries(existingPermissions).reduce(
1,353✔
244
    (accu, [key, value]) => {
245
      let values;
246
      if (!accu[key]) {
2,548✔
247
        accu[key] = value;
293✔
248
        return accu;
293✔
249
      }
250
      if (Array.isArray(value)) {
2,255✔
251
        values = [...value, ...accu[key]].filter(duplicateFilter);
1,385✔
252
      } else {
253
        values = mergePermissions(accu[key], { ...value });
870✔
254
      }
255
      accu[key] = values;
2,255✔
256
      return accu;
2,255✔
257
    },
258
    { ...addedPermissions }
259
  );
260

261
const mapHttpPermission = permission =>
183✔
262
  Object.entries(uiPermissionsByArea).reduce(
112✔
263
    (accu, [area, definition]) => {
264
      const endpointMatches = definition.endpoints.filter(
560✔
265
        endpoint => endpoint.path.test(permission.value) && (endpoint.types.includes(permission.type) || permission.type === PermissionTypes.Any)
1,456✔
266
      );
267
      if (permission.value === PermissionTypes.Any || (permission.value.includes(apiRoot) && endpointMatches.length)) {
560✔
268
        const endpointUiPermission = endpointMatches.reduce((endpointAccu, endpoint) => [...endpointAccu, ...endpoint.uiPermissions], []);
168✔
269
        const collector = (endpointUiPermission || definition.uiPermissions)
88!
270
          .reduce((permissionsAccu, uiPermission) => {
271
            if (permission.type === PermissionTypes.Any || (!endpointMatches.length && uiPermission.verbs.some(verb => verb === permission.type))) {
184!
272
              permissionsAccu.push(uiPermission.value);
184✔
273
            }
274
            return permissionsAccu;
184✔
275
          }, [])
276
          .filter(duplicateFilter);
277
        if (Array.isArray(accu[area])) {
88✔
278
          accu[area] = [...accu[area], ...collector].filter(duplicateFilter);
48✔
279
        } else {
280
          accu[area] = mergePermissions(accu[area], { [scopedPermissionAreas[area].excessiveAccessSelector]: collector });
40✔
281
        }
282
      }
283
      return accu;
560✔
284
    },
285
    { ...emptyUiPermissions }
286
  );
287

288
const permissionActionTypes = {
183✔
289
  any: mapHttpPermission,
290
  CREATE_DEPLOYMENT: permission =>
291
    permission.type === PermissionTypes.DeviceGroup
8!
292
      ? {
293
          deployments: [uiPermissionsById.deploy.value],
294
          groups: { [permission.value]: [uiPermissionsById.deploy.value] }
295
        }
296
      : {},
297
  http: mapHttpPermission,
298
  REMOTE_TERMINAL: permission =>
299
    permission.type === PermissionTypes.DeviceGroup
×
300
      ? {
301
          groups: { [permission.value]: [uiPermissionsById.connect.value] }
302
        }
303
      : {},
304
  VIEW_DEVICE: permission =>
305
    permission.type === PermissionTypes.DeviceGroup
8!
306
      ? {
307
          groups: { [permission.value]: [uiPermissionsById.read.value] }
308
        }
309
      : {}
310
};
311

312
const combinePermissions = (existingPermissions, additionalPermissions = {}) =>
183!
313
  Object.entries(additionalPermissions).reduce((accu, [name, permissions]) => {
72✔
314
    let maybeExistingPermissions = accu[name] || [];
72✔
315
    accu[name] = [...permissions, ...maybeExistingPermissions].filter(duplicateFilter);
72✔
316
    return accu;
72✔
317
  }, existingPermissions);
318

319
const tryParseCustomPermission = permission => {
183✔
320
  const uiPermissions = permissionActionTypes[permission.action](permission.object);
128✔
321
  const result = mergePermissions({ ...emptyUiPermissions }, uiPermissions);
128✔
322
  return { isCustom: true, permission, result };
128✔
323
};
324

325
const customPermissionHandler = (accu, permission) => {
183✔
326
  let processor = tryParseCustomPermission(permission);
128✔
327
  return {
128✔
328
    ...accu,
329
    isCustom: accu.isCustom || processor.isCustom,
152✔
330
    uiPermissions: mergePermissions(accu.uiPermissions, processor.result)
331
  };
332
};
333

334
const mapPermissionSet = (permissionSetName, names, scope, existingGroupsPermissions = {}) => {
183✔
335
  const permission = Object.values(uiPermissionsById).find(permission => permission.permissionSets[scope] === permissionSetName).value;
224✔
336
  const scopedPermissions = names.reduce((accu, name) => combinePermissions(accu, { [name]: [permission] }), existingGroupsPermissions);
72✔
337
  return Object.entries(scopedPermissions).reduce((accu, [key, permissions]) => ({ ...accu, [key]: deriveImpliedAreaPermissions(scope, permissions) }), {});
72✔
338
};
339

340
const isEmptyPermissionSet = permissionSet =>
183✔
341
  !Object.values(permissionSet).reduce((accu, permissions) => {
120✔
342
    if (Array.isArray(permissions)) {
600✔
343
      return accu || !!permissions.length;
360✔
344
    }
345
    return accu || !isEmpty(permissions);
240✔
346
  }, false);
347

348
const parseRolePermissions = ({ permission_sets_with_scope = [], permissions = [] }, permissionSets) => {
183✔
349
  const preliminaryResult = permission_sets_with_scope.reduce(
56✔
350
    (accu, permissionSet) => {
351
      let processor = permissionSets[permissionSet.name];
152✔
352
      if (!processor) {
152!
353
        return accu;
×
354
      }
355
      const scope = Object.keys(scopedPermissionAreas).find(scope => uiPermissionsByArea[scope].scope === permissionSet.scope?.type);
272✔
356
      if (scope) {
152✔
357
        const result = mapPermissionSet(permissionSet.name, permissionSet.scope.value, scope, accu.uiPermissions[scope]);
32✔
358
        return { ...accu, uiPermissions: { ...accu.uiPermissions, [scope]: result } };
32✔
359
      } else if (isEmptyPermissionSet(processor.result)) {
120!
360
        return processor.permissions.reduce(customPermissionHandler, accu);
×
361
      }
362
      return {
120✔
363
        ...accu,
364
        isCustom: accu.isCustom || processor.isCustom,
240✔
365
        uiPermissions: mergePermissions(accu.uiPermissions, processor.result)
366
      };
367
    },
368
    { isCustom: false, uiPermissions: { ...emptyUiPermissions, groups: {}, releases: {} } }
369
  );
370
  return permissions.reduce(customPermissionHandler, preliminaryResult);
56✔
371
};
372

373
export const normalizeRbacRoles = (roles, rolesById, permissionSets) =>
183✔
374
  roles.reduce(
8✔
375
    (accu, role) => {
376
      let normalizedPermissions;
377
      let isCustom = false;
96✔
378
      if (rolesById[role.name]) {
96✔
379
        normalizedPermissions = {
40✔
380
          ...rolesById[role.name].uiPermissions,
381
          groups: { ...rolesById[role.name].uiPermissions.groups },
382
          releases: { ...rolesById[role.name].uiPermissions.releases }
383
        };
384
      } else {
385
        const result = parseRolePermissions(role, permissionSets);
56✔
386
        normalizedPermissions = result.uiPermissions;
56✔
387
        isCustom = result.isCustom;
56✔
388
      }
389

390
      const roleState = accu[role.name] ?? { ...emptyRole };
96✔
391
      accu[role.name] = {
96✔
392
        ...roleState,
393
        ...role,
394
        description: roleState.description ? roleState.description : role.description,
96✔
395
        editable: !defaultRolesById[role.name] && !isCustom && (typeof roleState.editable !== 'undefined' ? roleState.editable : true),
216!
396
        isCustom,
397
        name: roleState.name ? roleState.name : role.name,
96✔
398
        uiPermissions: normalizedPermissions
399
      };
400
      return accu;
96✔
401
    },
402
    { ...rolesById }
403
  );
404

405
export const mapUserRolesToUiPermissions = (userRoles, roles) =>
183✔
406
  userRoles.reduce(
78✔
407
    (accu, roleId) => {
408
      if (!(roleId && roles[roleId])) {
67!
409
        return accu;
×
410
      }
411
      return mergePermissions(accu, roles[roleId].uiPermissions);
67✔
412
    },
413
    { ...emptyUiPermissions }
414
  );
415

416
export const getPermissionSets = () => (dispatch, getState) =>
183✔
417
  GeneralApi.get(`${useradmApiUrlv2}/permission_sets?per_page=500`)
8✔
418
    .then(({ data }) => {
419
      const permissionSets = data.reduce(
8✔
420
        (accu, permissionSet) => {
421
          const permissionSetState = accu[permissionSet.name] ?? {};
112✔
422
          let permissionSetObject = { ...permissionSetState, ...permissionSet };
112✔
423
          permissionSetObject.result = Object.values(uiPermissionsById).reduce(
112✔
424
            (accu, item) =>
425
              Object.entries(item.permissionSets).reduce((collector, [area, permissionSet]) => {
672✔
426
                if (scopedPermissionAreas[area]) {
1,344✔
427
                  return collector;
896✔
428
                }
429
                if (permissionSet === permissionSetObject.name) {
448✔
430
                  collector[area] = [...collector[area], item.value].filter(duplicateFilter);
32✔
431
                }
432
                return collector;
448✔
433
              }, accu),
434
            { ...emptyUiPermissions, ...(permissionSetObject.result ?? {}) }
120✔
435
          );
436
          const scopes = Object.values(scopedPermissionAreas).reduce((accu, { key, scopeType }) => {
112✔
437
            if (permissionSetObject.supported_scope_types?.includes(key) || permissionSetObject.supported_scope_types?.includes(scopeType)) {
224✔
438
              accu.push(key);
40✔
439
            }
440
            return accu;
224✔
441
          }, []);
442
          permissionSetObject = scopes.reduce((accu, scope) => {
112✔
443
            accu.result[scope] = mapPermissionSet(permissionSetObject.name, [scopedPermissionAreas[scope].excessiveAccessSelector], scope);
40✔
444
            return accu;
40✔
445
          }, permissionSetObject);
446
          accu[permissionSet.name] = permissionSetObject;
112✔
447
          return accu;
112✔
448
        },
449
        { ...getState().users.permissionSetsById }
450
      );
451
      return Promise.all([dispatch({ type: UserConstants.RECEIVED_PERMISSION_SETS, value: permissionSets }), permissionSets]);
8✔
452
    })
453
    .catch(() => console.log('Permission set retrieval failed - likely accessing a non-RBAC backend'));
×
454

455
export const getRoles = () => (dispatch, getState) =>
183✔
456
  Promise.all([GeneralApi.get(`${useradmApiUrlv2}/roles?per_page=500`), dispatch(getPermissionSets())])
8✔
457
    .then(results => {
458
      if (!results) {
8!
459
        return Promise.resolve();
×
460
      }
461
      const [{ data: roles }, permissionSetTasks] = results;
8✔
462
      const rolesById = normalizeRbacRoles(roles, getState().users.rolesById, permissionSetTasks[permissionSetTasks.length - 1]);
8✔
463
      return Promise.resolve(dispatch({ type: UserConstants.RECEIVED_ROLES, value: rolesById }));
8✔
464
    })
465
    .catch(() => console.log('Role retrieval failed - likely accessing a non-RBAC backend'));
×
466

467
const deriveImpliedAreaPermissions = (area, areaPermissions) => {
183✔
468
  const highestAreaPermissionLevelSelected = areaPermissions.reduce(
79✔
469
    (highest, current) => (uiPermissionsById[current].permissionLevel > highest ? uiPermissionsById[current].permissionLevel : highest),
83✔
470
    1
471
  );
472
  return uiPermissionsByArea[area].uiPermissions.reduce((permissions, current) => {
79✔
473
    if (current.permissionLevel < highestAreaPermissionLevelSelected || areaPermissions.includes(current.value)) {
386✔
474
      permissions.push(current.value);
134✔
475
    }
476
    return permissions;
386✔
477
  }, []);
478
};
479

480
/**
481
 * transforms [{ group: "groupName",  uiPermissions: ["read", "manage", "connect"] }, ...] to
482
 * [{ name: "ReadDevices", scope: { type: "DeviceGroups", value: ["groupName", ...] } }, ...]
483
 */
484
const transformAreaRoleDataToScopedPermissionsSets = (area, areaPermissions, excessiveAccessSelector) => {
183✔
485
  const permissionSetObject = areaPermissions.reduce((accu, { item, uiPermissions }) => {
4✔
486
    const impliedPermissions = deriveImpliedAreaPermissions(area, uiPermissions);
5✔
487
    accu = impliedPermissions.reduce((itemPermissionAccu, impliedPermission) => {
5✔
488
      const permissionSetState = itemPermissionAccu[uiPermissionsById[impliedPermission].permissionSets[area]] ?? {
6✔
489
        type: uiPermissionsByArea[area].scope,
490
        value: []
491
      };
492
      itemPermissionAccu[uiPermissionsById[impliedPermission].permissionSets[area]] = {
6✔
493
        ...permissionSetState,
494
        value: [...permissionSetState.value, item]
495
      };
496
      return itemPermissionAccu;
6✔
497
    }, accu);
498
    return accu;
5✔
499
  }, {});
500
  return Object.entries(permissionSetObject).map(([name, { value, ...scope }]) => {
4✔
501
    if (value.includes(excessiveAccessSelector)) {
6✔
502
      return { name };
2✔
503
    }
504
    return { name, scope: { ...scope, value: value.filter(duplicateFilter) } };
4✔
505
  });
506
};
507

508
const transformRoleDataToRole = (roleData, roleState = {}) => {
183✔
509
  const role = { ...roleState, ...roleData };
3✔
510
  const { description = '', name, uiPermissions = emptyUiPermissions } = role;
3!
511
  const { maybeUiPermissions, remainderKeys } = Object.entries(emptyUiPermissions).reduce(
3✔
512
    (accu, [key, emptyPermissions]) => {
513
      if (!scopedPermissionAreas[key]) {
15✔
514
        accu.remainderKeys.push(key);
9✔
515
      } else if (uiPermissions[key]) {
6✔
516
        accu.maybeUiPermissions[key] = uiPermissions[key].reduce(itemUiPermissionsReducer, emptyPermissions);
4✔
517
      }
518
      return accu;
15✔
519
    },
520
    { maybeUiPermissions: {}, remainderKeys: [] }
521
  );
522
  const { permissionSetsWithScope, roleUiPermissions } = remainderKeys.reduce(
3✔
523
    (accu, area) => {
524
      const areaPermissions = role.uiPermissions[area];
9✔
525
      if (!Array.isArray(areaPermissions)) {
9✔
526
        return accu;
7✔
527
      }
528
      const impliedPermissions = deriveImpliedAreaPermissions(area, areaPermissions);
2✔
529
      accu.roleUiPermissions[area] = impliedPermissions;
2✔
530
      const mappedPermissions = impliedPermissions.map(uiPermission => ({ name: uiPermissionsById[uiPermission].permissionSets[area] }));
2✔
531
      accu.permissionSetsWithScope.push(...mappedPermissions);
2✔
532
      return accu;
2✔
533
    },
534
    { permissionSetsWithScope: [{ name: defaultPermissionSets.Basic.name }], roleUiPermissions: {} }
535
  );
536
  const scopedPermissionSets = Object.values(scopedPermissionAreas).reduce((accu, { key, excessiveAccessSelector }) => {
3✔
537
    if (!uiPermissions[key]) {
6✔
538
      return accu;
2✔
539
    }
540
    accu.push(...transformAreaRoleDataToScopedPermissionsSets(key, uiPermissions[key], excessiveAccessSelector));
4✔
541
    return accu;
4✔
542
  }, []);
543
  return {
3✔
544
    permissionSetsWithScope: [...permissionSetsWithScope, ...scopedPermissionSets],
545
    role: {
546
      ...emptyRole,
547
      name,
548
      description: description ? description : roleState.description,
3!
549
      uiPermissions: {
550
        ...emptyUiPermissions,
551
        ...roleUiPermissions,
552
        ...maybeUiPermissions
553
      }
554
    }
555
  };
556
};
557

558
const roleActions = {
183✔
559
  create: {
560
    successMessage: 'The role was created successfully.',
561
    errorMessage: 'creating'
562
  },
563
  edit: {
564
    successMessage: 'The role has been updated.',
565
    errorMessage: 'editing'
566
  },
567
  remove: {
568
    successMessage: 'The role was deleted successfully.',
569
    errorMessage: 'removing'
570
  }
571
};
572

573
const roleActionErrorHandler = (err, type, dispatch) => commonErrorHandler(err, `There was an error ${roleActions[type].errorMessage} the role.`, dispatch);
183✔
574

575
export const createRole = roleData => dispatch => {
183✔
576
  const { permissionSetsWithScope, role } = transformRoleDataToRole(roleData);
1✔
577
  return GeneralApi.post(`${useradmApiUrlv2}/roles`, {
1✔
578
    name: role.name,
579
    description: role.description,
580
    permission_sets_with_scope: permissionSetsWithScope
581
  })
582
    .then(() =>
583
      Promise.all([
1✔
584
        dispatch({ type: UserConstants.CREATED_ROLE, role, roleId: role.name }),
585
        dispatch(getRoles()),
586
        dispatch(setSnackbar(roleActions.create.successMessage))
587
      ])
588
    )
589
    .catch(err => roleActionErrorHandler(err, 'create', dispatch));
×
590
};
591

592
export const editRole = roleData => (dispatch, getState) => {
183✔
593
  const { permissionSetsWithScope, role } = transformRoleDataToRole(roleData, getState().users.rolesById[roleData.name]);
2✔
594
  return GeneralApi.put(`${useradmApiUrlv2}/roles/${role.name}`, {
2✔
595
    description: role.description,
596
    name: role.name,
597
    permission_sets_with_scope: permissionSetsWithScope
598
  })
599
    .then(() =>
600
      Promise.all([
1✔
601
        dispatch({ type: UserConstants.UPDATED_ROLE, role, roleId: role.name }),
602
        dispatch(getRoles()),
603
        dispatch(setSnackbar(roleActions.edit.successMessage))
604
      ])
605
    )
606
    .catch(err => roleActionErrorHandler(err, 'edit', dispatch));
×
607
};
608

609
export const removeRole = roleId => (dispatch, getState) =>
183✔
610
  GeneralApi.delete(`${useradmApiUrlv2}/roles/${roleId}`)
2✔
611
    .then(() => {
612
      // eslint-disable-next-line no-unused-vars
613
      const { [roleId]: toBeRemoved, ...rolesById } = getState().users.rolesById;
2✔
614
      return Promise.all([
2✔
615
        dispatch({ type: UserConstants.REMOVED_ROLE, value: rolesById }),
616
        dispatch(getRoles()),
617
        dispatch(setSnackbar(roleActions.remove.successMessage))
618
      ]);
619
    })
620
    .catch(err => roleActionErrorHandler(err, 'remove', dispatch));
×
621

622
/*
623
  Global settings
624
*/
625
export const getGlobalSettings = () => dispatch =>
183✔
626
  GeneralApi.get(`${useradmApiUrl}/settings`).then(({ data: settings, headers: { etag } }) => {
11✔
627
    window.sessionStorage.setItem(UserConstants.settingsKeys.initialized, true);
11✔
628
    return Promise.all([dispatch({ type: UserConstants.SET_GLOBAL_SETTINGS, settings }), dispatch(setOfflineThreshold()), etag]);
11✔
629
  });
630

631
export const saveGlobalSettings =
632
  (settings, beOptimistic = false, notify = false) =>
183✔
633
  (dispatch, getState) => {
8✔
634
    if (!window.sessionStorage.getItem(UserConstants.settingsKeys.initialized) && !beOptimistic) {
8!
635
      return;
×
636
    }
637
    return Promise.resolve(dispatch(getGlobalSettings())).then(result => {
8✔
638
      let updatedSettings = { ...getState().users.globalSettings, ...settings };
8✔
639
      if (getCurrentUser(getState()).verified) {
8!
640
        updatedSettings['2fa'] = twoFAStates.enabled;
8✔
641
      } else {
642
        delete updatedSettings['2fa'];
×
643
      }
644
      let tasks = [dispatch({ type: UserConstants.SET_GLOBAL_SETTINGS, settings: updatedSettings })];
8✔
645
      const headers = result[result.length - 1] ? { 'If-Match': result[result.length - 1] } : {};
8!
646
      return GeneralApi.post(`${useradmApiUrl}/settings`, updatedSettings, { headers })
8✔
647
        .then(() => {
648
          if (notify) {
8✔
649
            tasks.push(dispatch(setSnackbar('Settings saved successfully')));
1✔
650
          }
651
          return Promise.all(tasks);
8✔
652
        })
653
        .catch(err => {
654
          if (beOptimistic) {
×
655
            return Promise.all([tasks]);
×
656
          }
657
          console.log(err);
×
658
          return commonErrorHandler(err, `The settings couldn't be saved.`, dispatch);
×
659
        });
660
    });
661
  };
662

663
export const getUserSettings = () => dispatch =>
183✔
664
  GeneralApi.get(`${useradmApiUrl}/settings/me`).then(({ data: settings, headers: { etag } }) => {
53✔
665
    window.sessionStorage.setItem(UserConstants.settingsKeys.initialized, true);
43✔
666
    return Promise.all([dispatch({ type: UserConstants.SET_USER_SETTINGS, settings }), etag]);
43✔
667
  });
668

669
export const saveUserSettings =
670
  (settings = { onboarding: {} }) =>
183✔
671
  (dispatch, getState) => {
50✔
672
    if (!getState().users.currentUser) {
50!
673
      return Promise.resolve();
×
674
    }
675
    return Promise.resolve(dispatch(getUserSettings())).then(result => {
50✔
676
      const userSettings = getUserSettingsSelector(getState());
40✔
677
      const onboardingState = getOnboardingState(getState());
40✔
678
      const tooltipState = getTooltipsState(getState());
40✔
679
      const updatedSettings = {
40✔
680
        ...userSettings,
681
        ...settings,
682
        onboarding: {
683
          ...onboardingState,
684
          ...settings.onboarding
685
        },
686
        tooltips: tooltipState
687
      };
688
      const headers = result[result.length - 1] ? { 'If-Match': result[result.length - 1] } : {};
40!
689
      return Promise.all([
40✔
690
        Promise.resolve(dispatch({ type: UserConstants.SET_USER_SETTINGS, settings: updatedSettings })),
691
        GeneralApi.post(`${useradmApiUrl}/settings/me`, updatedSettings, { headers })
692
      ]).catch(() => dispatch({ type: UserConstants.SET_USER_SETTINGS, settings: userSettings }));
×
693
    });
694
  };
695

696
export const get2FAQRCode = () => dispatch =>
183✔
697
  GeneralApi.get(`${useradmApiUrl}/2faqr`).then(res => dispatch({ type: UserConstants.RECEIVED_QR_CODE, value: res.data.qr }));
2✔
698

699
/*
700
  Onboarding
701
*/
702
export const setShowConnectingDialog = show => dispatch => dispatch({ type: UserConstants.SET_SHOW_CONNECT_DEVICE, show: Boolean(show) });
183✔
703

704
export const setHideAnnouncement = (shouldHide, userId) => (dispatch, getState) => {
183✔
705
  const currentUserId = userId || getCurrentUser(getState()).id;
13✔
706
  const hash = getState().app.hostedAnnouncement ? hashString(getState().app.hostedAnnouncement) : '';
13✔
707
  const announceCookie = cookies.get(`${currentUserId}${hash}`);
13✔
708
  if (shouldHide || (hash.length && typeof announceCookie !== 'undefined')) {
13!
709
    cookies.set(`${currentUserId}${hash}`, true, { maxAge: 604800 });
1✔
710
    return Promise.resolve(dispatch({ type: AppConstants.SET_ANNOUNCEMENT, announcement: undefined }));
1✔
711
  }
712
  return Promise.resolve();
12✔
713
};
714

715
export const getTokens = () => (dispatch, getState) =>
183✔
716
  GeneralApi.get(`${useradmApiUrl}/settings/tokens`).then(({ data: tokens }) => {
14✔
717
    const user = getCurrentUser(getState());
13✔
718
    const updatedUser = {
13✔
719
      ...user,
720
      tokens
721
    };
722
    return Promise.resolve(dispatch({ type: UserConstants.UPDATED_USER, user: updatedUser, userId: user.id }));
13✔
723
  });
724

725
const ONE_YEAR = 31536000;
183✔
726

727
export const generateToken =
728
  ({ expiresIn = ONE_YEAR, name }) =>
183✔
729
  dispatch =>
5✔
730
    GeneralApi.post(`${useradmApiUrl}/settings/tokens`, { name, expires_in: expiresIn })
5✔
731
      .then(({ data: token }) => Promise.all([dispatch(getTokens()), token]))
5✔
732
      .catch(err => commonErrorHandler(err, 'There was an error creating the token:', dispatch));
×
733

734
export const revokeToken = token => dispatch =>
183✔
735
  GeneralApi.delete(`${useradmApiUrl}/settings/tokens/${token.id}`).then(() => Promise.resolve(dispatch(getTokens())));
1✔
736

737
export const setTooltipReadState =
738
  (id, readState = UserConstants.READ_STATES.read, persist) =>
183!
739
  dispatch =>
1✔
740
    Promise.resolve(dispatch({ type: UserConstants.SET_TOOLTIP_STATE, id, value: { readState } })).then(() => {
1✔
741
      if (persist) {
1!
742
        return Promise.resolve(dispatch(saveUserSettings()));
1✔
743
      }
744
      return Promise.resolve();
×
745
    });
746

747
export const setAllTooltipsReadState =
748
  (readState = UserConstants.READ_STATES.read) =>
183!
749
  dispatch => {
1✔
750
    const updatedTips = Object.keys(HELPTOOLTIPS).reduce((accu, id) => ({ ...accu, [id]: { readState } }), {});
28✔
751
    return Promise.resolve(dispatch({ type: UserConstants.SET_TOOLTIPS_STATE, value: updatedTips })).then(() => dispatch(saveUserSettings()));
1✔
752
  };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc