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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 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) {
13!
96
    return Promise.reject();
×
97
  }
98
  return GeneralApi.post(`${useradmApiUrl}/auth/logout`).finally(() => {
13✔
99
    cleanUp();
11✔
100
    clearAllRetryTimers(setSnackbar);
11✔
101
    return Promise.resolve(dispatch({ type: UserConstants.USER_LOGOUT }));
11✔
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`)
17✔
144
    .then(res => {
145
      const users = res.data.reduce((accu, item) => {
13✔
146
        accu[item.id] = item;
26✔
147
        return accu;
26✔
148
      }, {});
149
      return dispatch({ type: UserConstants.RECEIVED_USER_LIST, users });
13✔
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,347✔
244
    (accu, [key, value]) => {
245
      let values;
246
      if (!accu[key]) {
2,534✔
247
        accu[key] = value;
289✔
248
        return accu;
289✔
249
      }
250
      if (Array.isArray(value)) {
2,245✔
251
        values = [...value, ...accu[key]].filter(duplicateFilter);
1,379✔
252
      } else {
253
        values = mergePermissions(accu[key], { ...value });
866✔
254
      }
255
      accu[key] = values;
2,245✔
256
      return accu;
2,245✔
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(
76✔
407
    (accu, roleId) => {
408
      if (!(roleId && roles[roleId])) {
65!
409
        return accu;
×
410
      }
411
      return mergePermissions(accu, roles[roleId].uiPermissions);
65✔
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