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

mendersoftware / gui / 1057188406

01 Nov 2023 04:24AM UTC coverage: 82.824% (-17.1%) from 99.964%
1057188406

Pull #4134

gitlab-ci

web-flow
chore: Bump uuid from 9.0.0 to 9.0.1

Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4134: chore: Bump uuid from 9.0.0 to 9.0.1

4349 of 6284 branches covered (0.0%)

8313 of 10037 relevant lines covered (82.82%)

200.97 hits per line

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

89.02
/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, logout } 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();
184✔
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;
184✔
46

47
const handleLoginError = (err, has2FA) => dispatch => {
184✔
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 =>
184✔
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 as cookie & set maxAge if noexpiry checkbox not checked
75
      cookies.set('JWT', token, { sameSite: 'strict', secure: true, path: '/', maxAge: stayLoggedIn ? undefined : 900 });
3!
76

77
      return dispatch(getUser(OWN_USER_ID))
3✔
78
        .catch(e => {
79
          cleanUp();
1✔
80
          return Promise.reject(dispatch(setSnackbar(extractErrorMessage(e))));
1✔
81
        })
82
        .then(() => {
83
          window.sessionStorage.removeItem('pendings-redirect');
2✔
84
          if (window.location.pathname !== '/ui/') {
2!
85
            window.location.replace('/ui/');
2✔
86
          }
87
          return Promise.all([dispatch({ type: UserConstants.SUCCESSFULLY_LOGGED_IN, value: token }), dispatch(initializeAppData())]);
2✔
88
        });
89
    });
90

91
export const logoutUser = reason => (dispatch, getState) => {
184✔
92
  if (getState().releases.uploadProgress) {
7✔
93
    return Promise.reject();
1✔
94
  }
95
  let tasks = [dispatch({ type: UserConstants.USER_LOGOUT })];
6✔
96
  return GeneralApi.post(`${useradmApiUrl}/auth/logout`).finally(() => {
6✔
97
    clearAllRetryTimers(setSnackbar);
3✔
98
    if (reason) {
3✔
99
      tasks.push(dispatch(setSnackbar(reason)));
2✔
100
    }
101
    logout();
3✔
102
    return Promise.all(tasks);
3✔
103
  });
104
};
105

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

241
/* RBAC related things follow:  */
242

243
const mergePermissions = (existingPermissions = { ...emptyUiPermissions }, addedPermissions) =>
184!
244
  Object.entries(existingPermissions).reduce(
1,797✔
245
    (accu, [key, value]) => {
246
      let values;
247
      if (!accu[key]) {
3,356✔
248
        accu[key] = value;
361✔
249
        return accu;
361✔
250
      }
251
      if (Array.isArray(value)) {
2,995✔
252
        values = [...value, ...accu[key]].filter(duplicateFilter);
1,841✔
253
      } else {
254
        values = mergePermissions(accu[key], { ...value });
1,154✔
255
      }
256
      accu[key] = values;
2,995✔
257
      return accu;
2,995✔
258
    },
259
    { ...addedPermissions }
260
  );
261

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

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

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

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

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

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

341
const isEmptyPermissionSet = permissionSet =>
184✔
342
  !Object.values(permissionSet).reduce((accu, permissions) => {
165✔
343
    if (Array.isArray(permissions)) {
825✔
344
      return accu || !!permissions.length;
495✔
345
    }
346
    return accu || !isEmpty(permissions);
330✔
347
  }, false);
348

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

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

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

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

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

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

468
const deriveImpliedAreaPermissions = (area, areaPermissions) => {
184✔
469
  const highestAreaPermissionLevelSelected = areaPermissions.reduce(
106✔
470
    (highest, current) => (uiPermissionsById[current].permissionLevel > highest ? uiPermissionsById[current].permissionLevel : highest),
113✔
471
    1
472
  );
473
  return uiPermissionsByArea[area].uiPermissions.reduce((permissions, current) => {
106✔
474
    if (current.permissionLevel < highestAreaPermissionLevelSelected || areaPermissions.includes(current.value)) {
521✔
475
      permissions.push(current.value);
182✔
476
    }
477
    return permissions;
521✔
478
  }, []);
479
};
480

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

726
const ONE_YEAR = 31536000;
184✔
727

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

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

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

748
export const setAllTooltipsReadState =
749
  (readState = UserConstants.READ_STATES.read) =>
184!
750
  dispatch => {
1✔
751
    const updatedTips = Object.keys(HELPTOOLTIPS).reduce((accu, id) => ({ ...accu, [id]: { readState } }), {});
28✔
752
    return Promise.resolve(dispatch({ type: UserConstants.SET_TOOLTIPS_STATE, value: updatedTips })).then(() => dispatch(saveUserSettings()));
1✔
753
  };
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