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

mendersoftware / gui / 908425489

pending completion
908425489

Pull #3799

gitlab-ci

mzedel
chore: aligned loader usage in devices list with deployment devices list

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3799: MEN-6553

4406 of 6423 branches covered (68.6%)

18 of 19 new or added lines in 3 files covered. (94.74%)

1777 existing lines in 167 files now uncovered.

8329 of 10123 relevant lines covered (82.28%)

144.7 hits per line

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

79.67
/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 * as AppConstants from '../constants/appConstants';
23
import { ALL_DEVICES } from '../constants/deviceConstants';
24
import * as OnboardingConstants from '../constants/onboardingConstants';
25
import { ALL_RELEASES } from '../constants/releaseConstants';
26
import * as UserConstants from '../constants/userConstants';
27
import { duplicateFilter, extractErrorMessage, preformatWithRequestID } from '../helpers';
28
import { getCurrentUser, getOnboardingState, getUserSettings as getUserSettingsSelector } from '../selectors';
29
import { clearAllRetryTimers } from '../utils/retrytimer';
30
import { commonErrorFallback, commonErrorHandler, initializeAppData, setOfflineThreshold, setSnackbar } from './appActions';
31

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

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

61
/*
62
  User management
63
*/
64
export const loginUser = (userData, stayLoggedIn) => dispatch =>
190✔
65
  UsersApi.postLogin(`${useradmApiUrl}/auth/login`, { ...userData, no_expiry: stayLoggedIn })
3✔
66
    .catch(err => {
UNCOV
67
      cleanUp();
×
UNCOV
68
      return Promise.resolve(dispatch(handleLoginError(err, userData['token2fa'])));
×
69
    })
70
    .then(res => {
71
      const token = res.text;
2✔
72
      if (!token) {
2!
UNCOV
73
        return;
×
74
      }
75
      // save token as cookie & set maxAge if noexpiry checkbox not checked
76
      cookies.set('JWT', token, { sameSite: 'strict', secure: true, path: '/', maxAge: stayLoggedIn ? undefined : 900 });
2!
77

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

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

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

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

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

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

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

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

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

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

165
export const updateUserColumnSettings = (columns, currentUserId) => (dispatch, getState) => {
190✔
166
  const userId = currentUserId ?? getCurrentUser(getState()).id;
14✔
167
  const storageKey = `${userId}-column-widths`;
14✔
168
  let customColumns = [];
14✔
169
  if (!columns) {
14✔
170
    try {
12✔
171
      customColumns = JSON.parse(window.localStorage.getItem(storageKey)) || customColumns;
12!
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));
14✔
179
  return Promise.resolve(dispatch({ type: UserConstants.SET_CUSTOM_COLUMNS, value: customColumns }));
14✔
180
};
181

182
const actions = {
190✔
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);
190✔
198

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

210
export const removeUser = userId => dispatch =>
190✔
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
    )
UNCOV
215
    .catch(err => userActionErrorHandler(err, 'remove', dispatch));
×
216

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

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

233
export const disableUser2fa =
234
  (userId = OWN_USER_ID) =>
190!
235
  dispatch =>
1✔
236
    GeneralApi.post(`${useradmApiUrl}/users/${userId}/2fa/disable`)
1✔
UNCOV
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 mapHttpPermission = permission =>
190✔
243
  Object.entries(uiPermissionsByArea).reduce((accu, [area, definition]) => {
8✔
244
    const endpointMatches = definition.endpoints.filter(
40✔
245
      endpoint => endpoint.path.test(permission.value) && (endpoint.types.includes(permission.type) || permission.type === PermissionTypes.Any)
96✔
246
    );
247
    if (permission.value === PermissionTypes.Any || (permission.value.includes(apiRoot) && endpointMatches.length)) {
40✔
248
      const collector = accu[area] || [];
8✔
249
      const endpointUiPermission = endpointMatches.reduce((endpointAccu, endpoint) => [...endpointAccu, ...endpoint.uiPermissions], []);
16✔
250
      accu[area] = (endpointUiPermission || definition.uiPermissions).reduce((permissionsAccu, uiPermission) => {
8!
251
        if (permission.type === PermissionTypes.Any || (!endpointMatches.length && uiPermission.verbs.some(verb => verb === permission.type))) {
16!
252
          permissionsAccu.push(uiPermission.value);
16✔
253
        }
254
        return permissionsAccu;
16✔
255
      }, collector);
256
    }
257
    return accu;
40✔
258
  }, {});
259

260
const permissionActionTypes = {
190✔
261
  any: mapHttpPermission,
262
  CREATE_DEPLOYMENT: permission =>
263
    permission.type === PermissionTypes.DeviceGroup
8!
264
      ? {
265
          deployments: [uiPermissionsById.deploy.value],
266
          groups: { [permission.value]: [uiPermissionsById.deploy.value] }
267
        }
268
      : {},
269
  http: mapHttpPermission,
270
  REMOTE_TERMINAL: permission =>
UNCOV
271
    permission.type === PermissionTypes.DeviceGroup
×
272
      ? {
273
          groups: { [permission.value]: [uiPermissionsById.connect.value] }
274
        }
275
      : {},
276
  VIEW_DEVICE: permission =>
277
    permission.type === PermissionTypes.DeviceGroup
8!
278
      ? {
279
          groups: { [permission.value]: [uiPermissionsById.read.value] }
280
        }
281
      : {}
282
};
283

284
const combinePermissions = (existingPermissions, additionalPermissions = {}) =>
190!
285
  Object.entries(additionalPermissions).reduce((accu, [name, permissions]) => {
270✔
286
    let maybeExistingPermissions = accu[name] || [];
213✔
287
    accu[name] = [...permissions, ...maybeExistingPermissions].filter(duplicateFilter);
213✔
288
    return accu;
213✔
289
  }, existingPermissions);
290

291
const mergePermissions = (existingPermissions = { ...emptyUiPermissions }, addedPermissions) =>
190!
292
  Object.entries(existingPermissions).reduce((accu, [key, value]) => {
135✔
293
    let values;
294
    if (!addedPermissions[key]) {
675✔
295
      accu[key] = value;
88✔
296
      return accu;
88✔
297
    }
298
    if (Array.isArray(value)) {
587✔
299
      values = [...value, ...addedPermissions[key]].filter(duplicateFilter);
349✔
300
    } else {
301
      values = combinePermissions({ ...value }, addedPermissions[key]);
238✔
302
    }
303
    accu[key] = values;
587✔
304
    return accu;
587✔
305
  }, {});
306

307
const tryParseCustomPermission = permission => {
190✔
308
  const uiPermissions = permissionActionTypes[permission.action](permission.object);
24✔
309
  const result = mergePermissions({ ...emptyUiPermissions }, uiPermissions);
24✔
310
  return { isCustom: true, permission, result };
24✔
311
};
312

313
const customPermissionHandler = (accu, permission) => {
190✔
314
  let processor = tryParseCustomPermission(permission);
24✔
315
  return {
24✔
316
    ...accu,
317
    isCustom: accu.isCustom || processor.isCustom,
40✔
318
    uiPermissions: mergePermissions(accu.uiPermissions, processor.result)
319
  };
320
};
321

322
const mapPermissionSet = (permissionSetName, names, existingGroupsPermissions = {}, scope) => {
190!
323
  const permission = Object.values(uiPermissionsById).find(permission => permission.permissionSets[scope] === permissionSetName).value;
104✔
324
  const scopedPermissions = names.reduce((accu, name) => combinePermissions(accu, { [name]: [permission] }), existingGroupsPermissions);
32✔
325
  return Object.entries(scopedPermissions).reduce((accu, [key, permissions]) => ({ ...accu, [key]: deriveImpliedAreaPermissions(scope, permissions) }), {});
32✔
326
};
327

328
const parseRolePermissions = ({ permission_sets_with_scope = [], permissions = [] }, permissionSets) => {
190✔
329
  const preliminaryResult = permission_sets_with_scope.reduce(
40✔
330
    (accu, permissionSet) => {
331
      let processor = permissionSets[permissionSet.name];
48✔
332
      if (!processor) {
48!
UNCOV
333
        return accu;
×
334
      }
335
      const scope = Object.keys(scopedPermissionAreas).find(scope => uiPermissionsByArea[scope].scope === permissionSet.scope?.type);
64✔
336
      if (scope) {
48✔
337
        const result = mapPermissionSet(permissionSet.name, permissionSet.scope.value, accu.uiPermissions[scope], scope);
32✔
338
        return { ...accu, uiPermissions: { ...accu.uiPermissions, [scope]: result } };
32✔
339
      } else if (!processor.result) {
16!
UNCOV
340
        return processor.permissions.reduce(customPermissionHandler, accu);
×
341
      }
342
      return {
16✔
343
        ...accu,
344
        isCustom: accu.isCustom || processor.isCustom,
32✔
345
        uiPermissions: mergePermissions(accu.uiPermissions, processor.result)
346
      };
347
    },
348
    { isCustom: false, uiPermissions: { ...emptyUiPermissions, groups: {}, releases: {} } }
349
  );
350
  return permissions.reduce(customPermissionHandler, preliminaryResult);
40✔
351
};
352

353
export const normalizeRbacRoles = (roles, rolesById, permissionSets) =>
190✔
354
  roles.reduce(
8✔
355
    (accu, role) => {
356
      let normalizedPermissions;
357
      let isCustom = false;
80✔
358
      if (rolesById[role.name]) {
80✔
359
        normalizedPermissions = {
40✔
360
          ...rolesById[role.name].uiPermissions,
361
          groups: { ...rolesById[role.name].uiPermissions.groups },
362
          releases: { ...rolesById[role.name].uiPermissions.releases }
363
        };
364
      } else {
365
        const result = parseRolePermissions(role, permissionSets);
40✔
366
        normalizedPermissions = result.uiPermissions;
40✔
367
        isCustom = result.isCustom;
40✔
368
      }
369

370
      const roleState = accu[role.name] ?? { ...emptyRole };
80✔
371
      accu[role.name] = {
80✔
372
        ...roleState,
373
        ...role,
374
        description: roleState.description ? roleState.description : role.description,
80✔
375
        editable: !defaultRolesById[role.name] && !isCustom && (typeof roleState.editable !== 'undefined' ? roleState.editable : true),
168!
376
        isCustom,
377
        name: roleState.name ? roleState.name : role.name,
80✔
378
        uiPermissions: normalizedPermissions
379
      };
380
      return accu;
80✔
381
    },
382
    { ...rolesById }
383
  );
384

385
export const mapUserRolesToUiPermissions = (userRoles, roles) =>
190✔
386
  userRoles.reduce(
386✔
387
    (accu, roleId) => {
388
      if (!(roleId && roles[roleId])) {
71!
UNCOV
389
        return accu;
×
390
      }
391
      return mergePermissions(accu, roles[roleId].uiPermissions);
71✔
392
    },
393
    { ...emptyUiPermissions }
394
  );
395

396
const scopedPermissionAreas = {
190✔
397
  groups: { key: 'groups', excessiveAccessSelector: ALL_DEVICES },
398
  releases: { key: 'releases', excessiveAccessSelector: ALL_RELEASES }
399
};
400

401
export const getPermissionSets = () => (dispatch, getState) =>
190✔
402
  GeneralApi.get(`${useradmApiUrlv2}/permission_sets?per_page=500`)
9✔
403
    .then(({ data }) => {
404
      const permissionSets = data.reduce(
8✔
405
        (accu, permissionSet) => {
406
          const permissionSetState = accu[permissionSet.name] ?? {};
104!
407
          let permissionSetObject = { ...permissionSetState, ...permissionSet };
104✔
408
          permissionSetObject.result = Object.values(uiPermissionsById).reduce(
104✔
409
            (accu, item) =>
410
              Object.entries(item.permissionSets).reduce((collector, [area, permissionSet]) => {
624✔
411
                if (scopedPermissionAreas[area]) {
1,144✔
412
                  return collector;
832✔
413
                }
414
                if (permissionSet === permissionSetObject.name) {
312✔
415
                  collector[area] = [...collector[area], item.value].filter(duplicateFilter);
24✔
416
                }
417
                return collector;
312✔
418
              }, accu),
419
            { ...emptyUiPermissions, ...(permissionSetObject.result ?? {}) }
120✔
420
          );
421
          const scopes = Object.keys(scopedPermissionAreas).filter(key => permissionSetObject.supported_scope_types?.includes(key));
208✔
422
          permissionSetObject = scopes.reduce((accu, scope) => {
104✔
UNCOV
423
            accu.result[scope] = mapPermissionSet(permissionSetObject.name, scopedPermissionAreas[scope].excessiveAccessSelector, scope);
×
UNCOV
424
            return accu;
×
425
          }, permissionSetObject);
426
          accu[permissionSet.name] = permissionSetObject;
104✔
427
          return accu;
104✔
428
        },
429
        { ...getState().users.permissionSetsById }
430
      );
431
      return Promise.all([dispatch({ type: UserConstants.RECEIVED_PERMISSION_SETS, value: permissionSets }), permissionSets]);
8✔
432
    })
UNCOV
433
    .catch(() => console.log('Permission set retrieval failed - likely accessing a non-RBAC backend'));
×
434

435
export const getRoles = () => (dispatch, getState) =>
190✔
436
  Promise.all([GeneralApi.get(`${useradmApiUrlv2}/roles?per_page=500`), dispatch(getPermissionSets())])
9✔
437
    .then(results => {
438
      if (!results) {
8!
UNCOV
439
        return Promise.resolve();
×
440
      }
441
      const [{ data: roles }, permissionSetTasks] = results;
8✔
442
      const rolesById = normalizeRbacRoles(roles, getState().users.rolesById, permissionSetTasks[permissionSetTasks.length - 1]);
8✔
443
      return Promise.resolve(dispatch({ type: UserConstants.RECEIVED_ROLES, value: rolesById }));
8✔
444
    })
UNCOV
445
    .catch(() => console.log('Role retrieval failed - likely accessing a non-RBAC backend'));
×
446

447
const deriveImpliedAreaPermissions = (area, areaPermissions) => {
190✔
448
  const highestAreaPermissionLevelSelected = areaPermissions.reduce(
37✔
449
    (highest, current) => (uiPermissionsById[current].permissionLevel > highest ? uiPermissionsById[current].permissionLevel : highest),
43✔
450
    1
451
  );
452
  return uiPermissionsByArea[area].uiPermissions.reduce((permissions, current) => {
37✔
453
    if (current.permissionLevel < highestAreaPermissionLevelSelected || areaPermissions.includes(current.value)) {
183✔
454
      permissions.push(current.value);
62✔
455
    }
456
    return permissions;
183✔
457
  }, []);
458
};
459

460
/**
461
 * transforms [{ group: "groupName",  uiPermissions: ["read", "manage", "connect"] }, ...] to
462
 * [{ name: "ReadDevices", scope: { type: "DeviceGroups", value: ["groupName", ...] } }, ...]
463
 */
464
const transformAreaRoleDataToScopedPermissionsSets = (area, areaPermissions, excessiveAccessSelector) => {
190✔
465
  const permissionSetObject = areaPermissions.reduce((accu, { item, uiPermissions }) => {
4✔
466
    const impliedPermissions = deriveImpliedAreaPermissions(area, uiPermissions);
5✔
467
    accu = impliedPermissions.reduce((itemPermissionAccu, impliedPermission) => {
5✔
468
      const permissionSetState = itemPermissionAccu[uiPermissionsById[impliedPermission].permissionSets[area]] ?? {
6✔
469
        type: uiPermissionsByArea[area].scope,
470
        value: []
471
      };
472
      itemPermissionAccu[uiPermissionsById[impliedPermission].permissionSets[area]] = {
6✔
473
        ...permissionSetState,
474
        value: [...permissionSetState.value, item]
475
      };
476
      return itemPermissionAccu;
6✔
477
    }, accu);
478
    return accu;
5✔
479
  }, {});
480
  return Object.entries(permissionSetObject).map(([name, { value, ...scope }]) => {
4✔
481
    if (value.includes(excessiveAccessSelector)) {
6✔
482
      return { name };
2✔
483
    }
484
    return { name, scope: { ...scope, value: value.filter(duplicateFilter) } };
4✔
485
  });
486
};
487

488
const transformRoleDataToRole = (roleData, roleState = {}) => {
190✔
489
  const role = { ...roleState, ...roleData };
3✔
490
  const { description = '', name, uiPermissions = emptyUiPermissions } = role;
3!
491
  const { maybeUiPermissions, remainderKeys } = Object.entries(emptyUiPermissions).reduce(
3✔
492
    (accu, [key, emptyPermissions]) => {
493
      if (!scopedPermissionAreas[key]) {
15✔
494
        accu.remainderKeys.push(key);
9✔
495
      } else if (uiPermissions[key]) {
6✔
496
        accu.maybeUiPermissions[key] = uiPermissions[key].reduce(itemUiPermissionsReducer, emptyPermissions);
4✔
497
      }
498
      return accu;
15✔
499
    },
500
    { maybeUiPermissions: {}, remainderKeys: [] }
501
  );
502
  const { permissionSetsWithScope, roleUiPermissions } = Object.keys(remainderKeys).reduce(
3✔
503
    (accu, area) => {
504
      const areaPermissions = role.uiPermissions[area];
9✔
505
      if (!Array.isArray(areaPermissions)) {
9!
506
        return accu;
9✔
507
      }
UNCOV
508
      const impliedPermissions = deriveImpliedAreaPermissions(area, areaPermissions);
×
UNCOV
509
      accu.roleUiPermissions[area] = impliedPermissions;
×
UNCOV
510
      const mappedPermissions = impliedPermissions.map(uiPermission => ({ name: uiPermissionsById[uiPermission].permissionSets[area] }));
×
UNCOV
511
      accu.permissionSetsWithScope.push(...mappedPermissions);
×
UNCOV
512
      return accu;
×
513
    },
514
    { permissionSetsWithScope: [{ name: defaultPermissionSets.Basic.name }], roleUiPermissions: {} }
515
  );
516
  const scopedPermissionSets = Object.values(scopedPermissionAreas).reduce((accu, { key, excessiveAccessSelector }) => {
3✔
517
    if (!uiPermissions[key]) {
6✔
518
      return accu;
2✔
519
    }
520
    accu.push(...transformAreaRoleDataToScopedPermissionsSets(key, uiPermissions[key], excessiveAccessSelector));
4✔
521
    return accu;
4✔
522
  }, []);
523
  return {
3✔
524
    permissionSetsWithScope: [...permissionSetsWithScope, ...scopedPermissionSets],
525
    role: {
526
      ...emptyRole,
527
      name,
528
      description: description ? description : roleState.description,
3!
529
      uiPermissions: {
530
        ...emptyUiPermissions,
531
        ...roleUiPermissions,
532
        ...maybeUiPermissions
533
      }
534
    }
535
  };
536
};
537

538
export const createRole = roleData => dispatch => {
190✔
539
  const { permissionSetsWithScope, role } = transformRoleDataToRole(roleData);
1✔
540
  return GeneralApi.post(`${useradmApiUrlv2}/roles`, {
1✔
541
    name: role.name,
542
    description: role.description,
543
    permission_sets_with_scope: permissionSetsWithScope
544
  })
545
    .then(() => Promise.all([dispatch({ type: UserConstants.CREATED_ROLE, role, roleId: role.name }), dispatch(getRoles())]))
1✔
UNCOV
546
    .catch(err => commonErrorHandler(err, `There was an error creating the role:`, dispatch));
×
547
};
548

549
export const editRole = roleData => (dispatch, getState) => {
190✔
550
  const { permissionSetsWithScope, role } = transformRoleDataToRole(roleData, getState().users.rolesById[roleData.name]);
2✔
551
  return GeneralApi.put(`${useradmApiUrlv2}/roles/${role.name}`, {
2✔
552
    description: role.description,
553
    name: role.name,
554
    permission_sets_with_scope: permissionSetsWithScope
555
  })
556
    .then(() => Promise.all([dispatch({ type: UserConstants.UPDATED_ROLE, role, roleId: role.name }), dispatch(getRoles())]))
1✔
UNCOV
557
    .catch(err => commonErrorHandler(err, `There was an error editing the role:`, dispatch));
×
558
};
559

560
export const removeRole = roleId => (dispatch, getState) =>
190✔
561
  GeneralApi.delete(`${useradmApiUrlv2}/roles/${roleId}`)
2✔
562
    .then(() => {
563
      // eslint-disable-next-line no-unused-vars
564
      const { [roleId]: toBeRemoved, ...rolesById } = getState().users.rolesById;
2✔
565
      return Promise.all([dispatch({ type: UserConstants.REMOVED_ROLE, value: rolesById }), dispatch(getRoles())]);
2✔
566
    })
UNCOV
567
    .catch(err => commonErrorHandler(err, `There was an error removing the role:`, dispatch));
×
568

569
/*
570
  Global settings
571
*/
572
export const getGlobalSettings = () => dispatch =>
190✔
573
  GeneralApi.get(`${useradmApiUrl}/settings`).then(({ data: settings, headers: { etag } }) => {
13✔
574
    window.sessionStorage.setItem(UserConstants.settingsKeys.initialized, true);
13✔
575
    return Promise.all([dispatch({ type: UserConstants.SET_GLOBAL_SETTINGS, settings }), dispatch(setOfflineThreshold()), etag]);
13✔
576
  });
577

578
export const saveGlobalSettings =
579
  (settings, beOptimistic = false, notify = false) =>
190✔
580
  (dispatch, getState) => {
9✔
581
    if (!window.sessionStorage.getItem(UserConstants.settingsKeys.initialized) && !beOptimistic) {
9!
UNCOV
582
      return;
×
583
    }
584
    return Promise.resolve(dispatch(getGlobalSettings())).then(result => {
9✔
585
      let updatedSettings = { ...getState().users.globalSettings, ...settings };
9✔
586
      if (getCurrentUser(getState()).verified) {
9✔
587
        updatedSettings['2fa'] = twoFAStates.enabled;
8✔
588
      } else {
589
        delete updatedSettings['2fa'];
1✔
590
      }
591
      let tasks = [dispatch({ type: UserConstants.SET_GLOBAL_SETTINGS, settings: updatedSettings })];
9✔
592
      const headers = result[result.length - 1] ? { 'If-Match': result[result.length - 1] } : {};
9!
593
      return GeneralApi.post(`${useradmApiUrl}/settings`, updatedSettings, { headers })
9✔
594
        .then(() => {
595
          if (notify) {
8✔
596
            tasks.push(dispatch(setSnackbar('Settings saved successfully')));
1✔
597
          }
598
          return Promise.all(tasks);
8✔
599
        })
600
        .catch(err => {
UNCOV
601
          if (beOptimistic) {
×
UNCOV
602
            return Promise.all([tasks]);
×
603
          }
UNCOV
604
          console.log(err);
×
UNCOV
605
          return commonErrorHandler(err, `The settings couldn't be saved.`, dispatch);
×
606
        });
607
    });
608
  };
609

610
export const getUserSettings = () => dispatch =>
190✔
611
  GeneralApi.get(`${useradmApiUrl}/settings/me`).then(({ data: settings, headers: { etag } }) => {
37✔
612
    window.sessionStorage.setItem(UserConstants.settingsKeys.initialized, true);
30✔
613
    return Promise.all([dispatch({ type: UserConstants.SET_USER_SETTINGS, settings }), etag]);
30✔
614
  });
615

616
export const saveUserSettings =
617
  (settings = { onboarding: {} }) =>
190!
618
  (dispatch, getState) => {
33✔
619
    if (!getState().users.currentUser) {
33!
UNCOV
620
      return Promise.resolve();
×
621
    }
622
    return Promise.resolve(dispatch(getUserSettings())).then(result => {
33✔
623
      const userSettings = getUserSettingsSelector(getState());
26✔
624
      const updatedSettings = {
26✔
625
        ...userSettings,
626
        ...settings,
627
        onboarding: {
628
          ...userSettings.onboarding,
629
          ...settings.onboarding
630
        }
631
      };
632
      const headers = result[result.length - 1] ? { 'If-Match': result[result.length - 1] } : {};
26!
633
      return Promise.all([
26✔
634
        Promise.resolve(dispatch({ type: UserConstants.SET_USER_SETTINGS, settings: updatedSettings })),
635
        GeneralApi.post(`${useradmApiUrl}/settings/me`, updatedSettings, { headers })
UNCOV
636
      ]).catch(() => dispatch({ type: UserConstants.SET_USER_SETTINGS, settings: userSettings }));
×
637
    });
638
  };
639

640
export const get2FAQRCode = () => dispatch =>
190✔
641
  GeneralApi.get(`${useradmApiUrl}/2faqr`).then(res => dispatch({ type: UserConstants.RECEIVED_QR_CODE, value: res.data.qr }));
2✔
642

643
/*
644
  Onboarding
645
*/
646
export const setShowHelptips = show => (dispatch, getState) => {
190✔
647
  let tasks = [dispatch({ type: UserConstants.SET_SHOW_HELP, show }), dispatch(saveUserSettings({ showHelptips: show }))];
2✔
648
  if (!getOnboardingState(getState()).complete) {
2!
649
    tasks.push(dispatch({ type: OnboardingConstants.SET_SHOW_ONBOARDING_HELP, show }));
2✔
650
  }
651
  return Promise.all(tasks);
2✔
652
};
653

654
export const toggleHelptips = () => (dispatch, getState) => {
190✔
655
  const showHelptips = getUserSettingsSelector(getState()).showHelptips;
2✔
656
  return Promise.resolve(dispatch(setShowHelptips(!showHelptips)));
2✔
657
};
658

659
export const setShowConnectingDialog = show => dispatch => dispatch({ type: UserConstants.SET_SHOW_CONNECT_DEVICE, show: Boolean(show) });
190✔
660

661
export const setHideAnnouncement = (shouldHide, userId) => (dispatch, getState) => {
190✔
662
  const currentUserId = userId || getCurrentUser(getState()).id;
12✔
663
  const hash = getState().app.hostedAnnouncement ? hashString(getState().app.hostedAnnouncement) : '';
12✔
664
  const announceCookie = cookies.get(`${currentUserId}${hash}`);
12✔
665
  if (shouldHide || (hash.length && typeof announceCookie !== 'undefined')) {
12!
666
    cookies.set(`${currentUserId}${hash}`, true, { maxAge: 604800 });
1✔
667
    return Promise.resolve(dispatch({ type: AppConstants.SET_ANNOUNCEMENT, announcement: undefined }));
1✔
668
  }
669
  return Promise.resolve();
11✔
670
};
671

672
export const getTokens = () => (dispatch, getState) =>
190✔
673
  GeneralApi.get(`${useradmApiUrl}/settings/tokens`).then(({ data: tokens }) => {
14✔
674
    const user = getCurrentUser(getState());
13✔
675
    const updatedUser = {
13✔
676
      ...user,
677
      tokens
678
    };
679
    return Promise.resolve(dispatch({ type: UserConstants.UPDATED_USER, user: updatedUser, userId: user.id }));
13✔
680
  });
681

682
const ONE_YEAR = 31536000;
190✔
683

684
export const generateToken =
685
  ({ expiresIn = ONE_YEAR, name }) =>
190✔
686
  dispatch =>
5✔
687
    GeneralApi.post(`${useradmApiUrl}/settings/tokens`, { name, expires_in: expiresIn })
5✔
688
      .then(({ data: token }) => Promise.all([dispatch(getTokens()), token]))
5✔
UNCOV
689
      .catch(err => commonErrorHandler(err, 'There was an error creating the token:', dispatch));
×
690

691
export const revokeToken = token => dispatch =>
190✔
692
  GeneralApi.delete(`${useradmApiUrl}/settings/tokens/${token.id}`).then(() => Promise.resolve(dispatch(getTokens())));
1✔
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