@northern.tech/store 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/.eslintrc.cjs +9 -0
  2. package/.prettierignore +1 -0
  3. package/CHANGELOG.md +12 -0
  4. package/actions.ts +32 -0
  5. package/api/general-api.test.ts +51 -0
  6. package/api/general-api.ts +60 -0
  7. package/api/types/AWSCredentials.ts +16 -0
  8. package/api/types/Actor.ts +35 -0
  9. package/api/types/Addon.ts +17 -0
  10. package/api/types/AddonTenantadm.ts +17 -0
  11. package/api/types/Alert.ts +34 -0
  12. package/api/types/AlertDetails.ts +23 -0
  13. package/api/types/AlertSubject.ts +23 -0
  14. package/api/types/ApiBurst.ts +21 -0
  15. package/api/types/ApiBurstTenantadm.ts +21 -0
  16. package/api/types/ApiLimits.ts +16 -0
  17. package/api/types/ApiLimitsTenantadm.ts +16 -0
  18. package/api/types/ApiQuota.ts +17 -0
  19. package/api/types/ApiQuotaTenantadm.ts +17 -0
  20. package/api/types/Artifact.ts +48 -0
  21. package/api/types/ArtifactDeployments.ts +48 -0
  22. package/api/types/ArtifactInfo.ts +11 -0
  23. package/api/types/ArtifactInfoDeployments.ts +11 -0
  24. package/api/types/ArtifactLink.ts +11 -0
  25. package/api/types/ArtifactTypeInfo.ts +13 -0
  26. package/api/types/ArtifactTypeInfoDeployments.ts +13 -0
  27. package/api/types/ArtifactUpdate.ts +10 -0
  28. package/api/types/ArtifactUploadLink.ts +15 -0
  29. package/api/types/Attribute.ts +36 -0
  30. package/api/types/AttributeInventory.ts +32 -0
  31. package/api/types/AuditLog.ts +26 -0
  32. package/api/types/AuthSet.ts +33 -0
  33. package/api/types/AuthSetDeviceauth.ts +37 -0
  34. package/api/types/AutoAuthRequest.ts +23 -0
  35. package/api/types/AzureSharedAccessSecret.ts +11 -0
  36. package/api/types/BillingInfo.ts +13 -0
  37. package/api/types/BinaryDeltaConfiguration.ts +15 -0
  38. package/api/types/BinaryDeltaConfigurationDeployments.ts +15 -0
  39. package/api/types/BinaryDeltaLimits.ts +15 -0
  40. package/api/types/BinaryDeltaLimitsDeployments.ts +15 -0
  41. package/api/types/BoundingBox.ts +11 -0
  42. package/api/types/CancelRequest.ts +10 -0
  43. package/api/types/CardData.ts +49 -0
  44. package/api/types/CardSetupData.ts +17 -0
  45. package/api/types/CheckoutData.ts +13 -0
  46. package/api/types/Configuration.ts +11 -0
  47. package/api/types/Count.ts +13 -0
  48. package/api/types/Credentials.ts +23 -0
  49. package/api/types/DBusSubsystem.ts +27 -0
  50. package/api/types/DeltaConfiguration.ts +14 -0
  51. package/api/types/DeltaConfigurationDeployments.ts +14 -0
  52. package/api/types/Deployment.ts +95 -0
  53. package/api/types/DeploymentAggregation.ts +18 -0
  54. package/api/types/DeploymentAggregationItem.ts +18 -0
  55. package/api/types/DeploymentAggregationTerm.ts +23 -0
  56. package/api/types/DeploymentAggregationTerms.ts +18 -0
  57. package/api/types/DeploymentAttributeProjection.ts +11 -0
  58. package/api/types/DeploymentAuditlogs.ts +11 -0
  59. package/api/types/DeploymentDeployments.ts +99 -0
  60. package/api/types/DeploymentFilterTerm.ts +37 -0
  61. package/api/types/DeploymentIdentifier.ts +10 -0
  62. package/api/types/DeploymentPhase.ts +23 -0
  63. package/api/types/DeploymentReporting.ts +46 -0
  64. package/api/types/DeploymentSearchTerms.ts +39 -0
  65. package/api/types/DeploymentSortTerm.ts +25 -0
  66. package/api/types/DeploymentStatistics.ts +15 -0
  67. package/api/types/DeploymentStatusStatistics.ts +54 -0
  68. package/api/types/Device.ts +29 -0
  69. package/api/types/DeviceAggregation.ts +18 -0
  70. package/api/types/DeviceAggregationItem.ts +18 -0
  71. package/api/types/DeviceAggregationTerm.ts +27 -0
  72. package/api/types/DeviceAggregationTerms.ts +22 -0
  73. package/api/types/DeviceAttribute.ts +23 -0
  74. package/api/types/DeviceAttributeProjection.ts +15 -0
  75. package/api/types/DeviceAuditlogs.ts +17 -0
  76. package/api/types/DeviceAuthEvent.ts +23 -0
  77. package/api/types/DeviceConfiguration.ts +16 -0
  78. package/api/types/DeviceDeployment.ts +11 -0
  79. package/api/types/DeviceDeployments.ts +31 -0
  80. package/api/types/DeviceDeviceauth.ts +42 -0
  81. package/api/types/DeviceFilterAttribute.ts +22 -0
  82. package/api/types/DeviceFilterTerm.ts +41 -0
  83. package/api/types/DeviceInventory.ts +19 -0
  84. package/api/types/DeviceInventoryInventory.ts +19 -0
  85. package/api/types/DeviceReporting.ts +22 -0
  86. package/api/types/DeviceSearchTerms.ts +39 -0
  87. package/api/types/DeviceSortTerm.ts +29 -0
  88. package/api/types/DeviceState.ts +32 -0
  89. package/api/types/DeviceStateDeviceconnect.ts +33 -0
  90. package/api/types/DeviceStateIot_manager.ts +14 -0
  91. package/api/types/DeviceStatus.ts +19 -0
  92. package/api/types/DeviceWithImage.ts +80 -0
  93. package/api/types/DirectUploadMetadata.ts +18 -0
  94. package/api/types/EmailVerificationCompletion.ts +13 -0
  95. package/api/types/EmailVerificationRequest.ts +13 -0
  96. package/api/types/Error.ts +17 -0
  97. package/api/types/ErrorAuditlogs.ts +13 -0
  98. package/api/types/ErrorDeployments.ts +17 -0
  99. package/api/types/ErrorDeviceauth.ts +13 -0
  100. package/api/types/ErrorDeviceconfig.ts +17 -0
  101. package/api/types/ErrorDeviceconnect.ts +17 -0
  102. package/api/types/ErrorDevicemonitor.ts +17 -0
  103. package/api/types/ErrorExt.ts +18 -0
  104. package/api/types/ErrorInventory.ts +17 -0
  105. package/api/types/ErrorIot_manager.ts +17 -0
  106. package/api/types/ErrorNotFound.ts +17 -0
  107. package/api/types/ErrorReporting.ts +18 -0
  108. package/api/types/ErrorTenantadm.ts +13 -0
  109. package/api/types/ErrorUseradm.ts +17 -0
  110. package/api/types/Event.ts +48 -0
  111. package/api/types/ExternalIdentity.ts +17 -0
  112. package/api/types/Features.ts +17 -0
  113. package/api/types/FileUpload.ts +23 -0
  114. package/api/types/Filter.ts +19 -0
  115. package/api/types/FilterAttribute.ts +21 -0
  116. package/api/types/FilterDefinition.ts +18 -0
  117. package/api/types/FilterDeployments.ts +19 -0
  118. package/api/types/FilterInventory.ts +20 -0
  119. package/api/types/FilterPredicate.ts +78 -0
  120. package/api/types/FilterPredicateDeployments.ts +48 -0
  121. package/api/types/FilterPredicateInventory.ts +76 -0
  122. package/api/types/GeoBoundingBox.ts +10 -0
  123. package/api/types/GeoBoundingBoxFilter.ts +6 -0
  124. package/api/types/GeoDistance.ts +17 -0
  125. package/api/types/GeoDistanceFilter.ts +10 -0
  126. package/api/types/GeoPoint.ts +15 -0
  127. package/api/types/Group.ts +10 -0
  128. package/api/types/HTTP.ts +19 -0
  129. package/api/types/IdentityData.ts +25 -0
  130. package/api/types/InputParameter.ts +8 -0
  131. package/api/types/Integration.ts +24 -0
  132. package/api/types/JobStatus.ts +13 -0
  133. package/api/types/Limit.ts +21 -0
  134. package/api/types/LimitDeployments.ts +21 -0
  135. package/api/types/LimitDeviceauth.ts +10 -0
  136. package/api/types/LimitUseradm.ts +11 -0
  137. package/api/types/Limits.ts +11 -0
  138. package/api/types/LineDescriptor.ts +11 -0
  139. package/api/types/LogSubsystem.ts +23 -0
  140. package/api/types/LoginOptions.ts +18 -0
  141. package/api/types/ManagementAPIConfiguration.ts +5 -0
  142. package/api/types/MenderTypes.ts +183 -0
  143. package/api/types/MonitorConfiguration.ts +22 -0
  144. package/api/types/NewConfigurationDeployment.ts +15 -0
  145. package/api/types/NewConfigurationDeploymentResponse.ts +10 -0
  146. package/api/types/NewDeployment.ts +42 -0
  147. package/api/types/NewDeploymentForGroup.ts +33 -0
  148. package/api/types/NewDeploymentPhase.ts +37 -0
  149. package/api/types/NewDeploymentPhaseDeployments.ts +20 -0
  150. package/api/types/NewDeploymentV2.ts +31 -0
  151. package/api/types/NewTenant.ts +41 -0
  152. package/api/types/Object.ts +35 -0
  153. package/api/types/OwnUserUpdate.ts +21 -0
  154. package/api/types/PasswordResetCompletion.ts +17 -0
  155. package/api/types/PasswordResetRequest.ts +13 -0
  156. package/api/types/Permission.ts +21 -0
  157. package/api/types/PermissionObject.ts +14 -0
  158. package/api/types/PermissionSet.ts +22 -0
  159. package/api/types/PermissionSetWithScope.ts +15 -0
  160. package/api/types/PersonalAccessToken.ts +29 -0
  161. package/api/types/PersonalAccessTokenRequest.ts +18 -0
  162. package/api/types/Plan.ts +19 -0
  163. package/api/types/PlanBindingDetails.ts +13 -0
  164. package/api/types/PlanChangeRequest.ts +14 -0
  165. package/api/types/PlanLimits.ts +12 -0
  166. package/api/types/PreAuthSet.ts +16 -0
  167. package/api/types/Release.ts +32 -0
  168. package/api/types/ReleaseDeployments.ts +32 -0
  169. package/api/types/ReleaseUpdate.ts +13 -0
  170. package/api/types/Releases.ts +9 -0
  171. package/api/types/ReleasesDeleteError.ts +21 -0
  172. package/api/types/ReleasesDeployments.ts +9 -0
  173. package/api/types/Role.ts +19 -0
  174. package/api/types/RolePermission.ts +25 -0
  175. package/api/types/RolePermissionObject.ts +17 -0
  176. package/api/types/RoleUseradm.ts +19 -0
  177. package/api/types/SAMLMetadata.ts +21 -0
  178. package/api/types/Scope.ts +19 -0
  179. package/api/types/SelectAttribute.ts +17 -0
  180. package/api/types/ServiceSubsystem.ts +19 -0
  181. package/api/types/Settings.ts +23 -0
  182. package/api/types/SortCriteria.ts +30 -0
  183. package/api/types/SsoObject.ts +21 -0
  184. package/api/types/Status.ts +17 -0
  185. package/api/types/StatusDeviceauth.ts +18 -0
  186. package/api/types/StatusTenantadm.ts +19 -0
  187. package/api/types/StorageLimit.ts +17 -0
  188. package/api/types/SubscriptionData.ts +8 -0
  189. package/api/types/SupportRequest.ts +17 -0
  190. package/api/types/Tag.ts +27 -0
  191. package/api/types/Tags.ts +10 -0
  192. package/api/types/TaskResult.ts +13 -0
  193. package/api/types/TaskResultCLI.ts +10 -0
  194. package/api/types/TaskResultHTTPRequest.ts +8 -0
  195. package/api/types/TaskResultHTTPResponse.ts +10 -0
  196. package/api/types/Tenant.ts +38 -0
  197. package/api/types/TenantApiLimits.ts +12 -0
  198. package/api/types/TenantApiLimitsTenantadm.ts +12 -0
  199. package/api/types/TenantIdName.ts +14 -0
  200. package/api/types/TenantIdsData.ts +10 -0
  201. package/api/types/TenantInfo.ts +14 -0
  202. package/api/types/TenantShortInfo.ts +21 -0
  203. package/api/types/TenantTenantadm.ts +38 -0
  204. package/api/types/TenantsIdName.ts +6 -0
  205. package/api/types/Update.ts +17 -0
  206. package/api/types/UpdateDeployments.ts +18 -0
  207. package/api/types/UpdateFile.ts +13 -0
  208. package/api/types/UpdateFileDeployments.ts +13 -0
  209. package/api/types/UpdateTypes.ts +8 -0
  210. package/api/types/UpgradeCompleteRequest.ts +23 -0
  211. package/api/types/User.ts +13 -0
  212. package/api/types/UserAuditlogs.ts +13 -0
  213. package/api/types/UserNew.ts +26 -0
  214. package/api/types/UserUpdate.ts +25 -0
  215. package/api/types/UserUseradm.ts +45 -0
  216. package/api/types/UserWithTenantInfo.ts +8 -0
  217. package/api/types/XDeltaArgs.ts +9 -0
  218. package/api/types/XDeltaArgsDeployments.ts +9 -0
  219. package/api/types/XDeltaArgsLimits.ts +10 -0
  220. package/api/types/XDeltaArgsLimitsDeployments.ts +10 -0
  221. package/api/users-api.ts +34 -0
  222. package/appSlice/constants.ts +166 -0
  223. package/appSlice/index.ts +159 -0
  224. package/appSlice/reducer.test.ts +97 -0
  225. package/appSlice/selectors.ts +33 -0
  226. package/appSlice/thunks.test.ts +118 -0
  227. package/appSlice/thunks.ts +148 -0
  228. package/auth.test.ts +36 -0
  229. package/auth.ts +82 -0
  230. package/commonConstants.tsx +252 -0
  231. package/commonSelectors.ts +255 -0
  232. package/constants.ts +22 -0
  233. package/deploymentsSlice/constants.ts +175 -0
  234. package/deploymentsSlice/index.ts +145 -0
  235. package/deploymentsSlice/reducer.test.ts +108 -0
  236. package/deploymentsSlice/selectors.ts +58 -0
  237. package/deploymentsSlice/thunks.test.ts +425 -0
  238. package/deploymentsSlice/thunks.ts +382 -0
  239. package/devicesSlice/constants.ts +56 -0
  240. package/devicesSlice/index.ts +207 -0
  241. package/devicesSlice/reducer.test.ts +194 -0
  242. package/devicesSlice/selectors.ts +127 -0
  243. package/devicesSlice/thunks.test.tsx +1245 -0
  244. package/devicesSlice/thunks.tsx +1098 -0
  245. package/monitorSlice/constants.ts +17 -0
  246. package/monitorSlice/index.ts +65 -0
  247. package/monitorSlice/reducer.test.ts +79 -0
  248. package/monitorSlice/selectors.ts +15 -0
  249. package/monitorSlice/thunks.test.ts +112 -0
  250. package/monitorSlice/thunks.ts +101 -0
  251. package/onboardingSlice/constants.ts +53 -0
  252. package/onboardingSlice/index.ts +60 -0
  253. package/onboardingSlice/reducer.test.ts +45 -0
  254. package/onboardingSlice/selectors.ts +15 -0
  255. package/onboardingSlice/thunks.test.ts +235 -0
  256. package/onboardingSlice/thunks.ts +142 -0
  257. package/organizationSlice/constants.ts +82 -0
  258. package/organizationSlice/index.ts +111 -0
  259. package/organizationSlice/reducer.test.ts +78 -0
  260. package/organizationSlice/selectors.ts +45 -0
  261. package/organizationSlice/thunks.test.ts +619 -0
  262. package/organizationSlice/thunks.ts +383 -0
  263. package/package.json +38 -0
  264. package/releasesSlice/constants.ts +18 -0
  265. package/releasesSlice/index.ts +116 -0
  266. package/releasesSlice/reducer.test.ts +78 -0
  267. package/releasesSlice/selectors.ts +33 -0
  268. package/releasesSlice/thunks.test.ts +389 -0
  269. package/releasesSlice/thunks.ts +379 -0
  270. package/selectors.ts +22 -0
  271. package/store.ts +89 -0
  272. package/storehooks.test.tsx +386 -0
  273. package/storehooks.ts +250 -0
  274. package/thunks.ts +21 -0
  275. package/tsconfig.json +11 -0
  276. package/tsup.config.ts +11 -0
  277. package/usersSlice/constants.ts +374 -0
  278. package/usersSlice/index.ts +151 -0
  279. package/usersSlice/reducer.test.ts +141 -0
  280. package/usersSlice/selectors.ts +60 -0
  281. package/usersSlice/thunks.test.ts +660 -0
  282. package/usersSlice/thunks.ts +773 -0
  283. package/utils.test.ts +187 -0
  284. package/utils.ts +249 -0
@@ -0,0 +1,773 @@
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
+ // @ts-nocheck
17
+ import storeActions from '@northern.tech/store/actions';
18
+ import GeneralApi from '@northern.tech/store/api/general-api';
19
+ import UsersApi from '@northern.tech/store/api/users-api';
20
+ import { cleanUp, getSessionInfo, maxSessionAge, setSessionInfo } from '@northern.tech/store/auth';
21
+ import {
22
+ ALL_RELEASES,
23
+ APPLICATION_JSON_CONTENT_TYPE,
24
+ APPLICATION_JWT_CONTENT_TYPE,
25
+ SSO_TYPES,
26
+ TIMEOUTS,
27
+ apiRoot,
28
+ emptyRole,
29
+ emptyUiPermissions,
30
+ tenantadmApiUrlv2
31
+ } from '@northern.tech/store/constants';
32
+ import { getOnboardingState, getOrganization, getTooltipsState, getUserSettings as getUserSettingsSelector } from '@northern.tech/store/selectors';
33
+ import { commonErrorFallback, commonErrorHandler } from '@northern.tech/store/store';
34
+ import { setOfflineThreshold } from '@northern.tech/store/thunks';
35
+ import { extractErrorMessage, mergePermissions, preformatWithRequestID } from '@northern.tech/store/utils';
36
+ import { duplicateFilter, isEmpty } from '@northern.tech/utils/helpers';
37
+ import { clearAllRetryTimers } from '@northern.tech/utils/retrytimer';
38
+ import { createAsyncThunk } from '@reduxjs/toolkit';
39
+ import hashString from 'md5';
40
+ import Cookies from 'universal-cookie';
41
+
42
+ import { actions, sliceName } from '.';
43
+ import {
44
+ OWN_USER_ID,
45
+ PermissionTypes,
46
+ READ_STATES,
47
+ USER_LOGOUT,
48
+ defaultPermissionSets,
49
+ rolesById as defaultRolesById,
50
+ itemUiPermissionsReducer,
51
+ scopedPermissionAreas,
52
+ settingsKeys,
53
+ twoFAStates,
54
+ uiPermissionsByArea,
55
+ uiPermissionsById,
56
+ useradmApiUrl,
57
+ useradmApiUrlv2
58
+ } from './constants';
59
+ import { getCurrentUser, getRolesById } from './selectors';
60
+
61
+ const cookies = new Cookies();
62
+
63
+ const { setAnnouncement, setSnackbar } = storeActions;
64
+
65
+ const handleLoginError =
66
+ (err, { token2fa: has2FA, password }, rejectWithValue) =>
67
+ dispatch => {
68
+ const errorText = extractErrorMessage(err);
69
+ const is2FABackend = errorText.includes('2fa');
70
+ if (is2FABackend && !has2FA) {
71
+ return rejectWithValue({ error: '2fa code missing' });
72
+ }
73
+ if (password === undefined) {
74
+ // Enterprise supports two-steps login. On the first step you can enter only email
75
+ // and in case of SSO set up you will receive a redirect URL
76
+ // otherwise you will receive 401 status code and password field will be shown.
77
+ return Promise.reject();
78
+ }
79
+ const twoFAError = is2FABackend ? ' and verification code' : '';
80
+ const errorMessage = `There was a problem logging in. Please check your email${
81
+ twoFAError ? ',' : ' and'
82
+ } password${twoFAError}. If you still have problems, contact an administrator.`;
83
+ return Promise.reject(dispatch(setSnackbar(preformatWithRequestID(err.response, errorMessage), null, 'Copy to clipboard')));
84
+ };
85
+
86
+ /*
87
+ User management
88
+ */
89
+ export const loginUser = createAsyncThunk(`${sliceName}/loginUser`, ({ stayLoggedIn, ...userData }, { dispatch, rejectWithValue }) =>
90
+ UsersApi.postLogin(`${useradmApiUrl}/auth/login`, { ...userData, no_expiry: stayLoggedIn })
91
+ .catch(err => {
92
+ cleanUp();
93
+ return Promise.reject(dispatch(handleLoginError(err, userData, rejectWithValue)));
94
+ })
95
+ .then(({ text: response, contentType }) => {
96
+ // If the content type is application/json then backend returned SSO configuration.
97
+ // user should be redirected to the start sso url to finish login process.
98
+ if (contentType.includes(APPLICATION_JSON_CONTENT_TYPE)) {
99
+ const { id, kind } = response;
100
+ const type = kind.split('/')[1];
101
+ const ssoLoginUrl = SSO_TYPES[type].getStartUrl(id);
102
+ window.location.replace(ssoLoginUrl);
103
+ return;
104
+ }
105
+
106
+ const token = response;
107
+ if (contentType !== APPLICATION_JWT_CONTENT_TYPE || !token) {
108
+ return;
109
+ }
110
+ // save token to local storage & set maxAge if noexpiry checkbox not checked
111
+ let now = new Date();
112
+ now.setSeconds(now.getSeconds() + maxSessionAge);
113
+ const expiresAt = stayLoggedIn ? undefined : now.toISOString();
114
+ setSessionInfo({ token, expiresAt });
115
+ cookies.remove('JWT', { path: '/' });
116
+ return Promise.resolve(dispatch(getUser(OWN_USER_ID)))
117
+ .unwrap()
118
+ .catch(e => {
119
+ cleanUp();
120
+ return Promise.reject(dispatch(setSnackbar(extractErrorMessage(e))));
121
+ })
122
+ .then(() => {
123
+ window.sessionStorage.removeItem('pendings-redirect');
124
+ if (window.location.pathname !== '/ui/') {
125
+ window.location.replace('/ui/');
126
+ }
127
+ return Promise.resolve(dispatch(actions.successfullyLoggedIn({ expiresAt, token })));
128
+ });
129
+ })
130
+ );
131
+
132
+ export const logoutUser = createAsyncThunk(`${sliceName}/logoutUser`, (_, { dispatch, getState }) => {
133
+ if (Object.keys(getState().app.uploadsById).length) {
134
+ return Promise.reject();
135
+ }
136
+ return GeneralApi.post(`${useradmApiUrl}/auth/logout`).finally(() => {
137
+ cleanUp();
138
+ clearAllRetryTimers(setSnackbar);
139
+ return Promise.resolve(dispatch({ type: USER_LOGOUT }));
140
+ });
141
+ });
142
+
143
+ export const switchUserOrganization = createAsyncThunk(`${sliceName}/switchUserOrganization`, (tenantId, { getState }) => {
144
+ if (Object.keys(getState().app.uploadsById).length) {
145
+ return Promise.reject();
146
+ }
147
+ return GeneralApi.get(`${useradmApiUrl}/users/tenants/${tenantId}/token`).then(({ data: token }) => {
148
+ setSessionInfo({ ...getSessionInfo(), token });
149
+ window.location.reload();
150
+ });
151
+ });
152
+
153
+ export const passwordResetStart = createAsyncThunk(`${sliceName}/passwordResetStart`, (email, { dispatch }) =>
154
+ GeneralApi.post(`${useradmApiUrl}/auth/password-reset/start`, { email }).catch(err =>
155
+ commonErrorHandler(err, `The password reset request cannot be processed:`, dispatch, undefined, true)
156
+ )
157
+ );
158
+
159
+ export const passwordResetComplete = createAsyncThunk(`${sliceName}/passwordResetComplete`, ({ secretHash, newPassword }, { dispatch }) =>
160
+ GeneralApi.post(`${useradmApiUrl}/auth/password-reset/complete`, { secret_hash: secretHash, password: newPassword }).catch((err = {}) => {
161
+ const { error, response = {} } = err;
162
+ let errorMsg = '';
163
+ if (response.status == 400) {
164
+ errorMsg = 'the link you are using expired or the request is not valid, please try again.';
165
+ } else {
166
+ errorMsg = error;
167
+ }
168
+ dispatch(setSnackbar('The password reset request cannot be processed: ' + errorMsg));
169
+ return Promise.reject(err);
170
+ })
171
+ );
172
+
173
+ export const verifyEmailStart = createAsyncThunk(`${sliceName}/verifyEmailStart`, (_, { dispatch, getState }) =>
174
+ GeneralApi.post(`${useradmApiUrl}/auth/verify-email/start`, { email: getCurrentUser(getState()).email })
175
+ .catch(err => commonErrorHandler(err, 'An error occured starting the email verification process:', dispatch))
176
+ .finally(() => Promise.resolve(dispatch(getUser(OWN_USER_ID))))
177
+ );
178
+
179
+ export const verifyEmailComplete = createAsyncThunk(`${sliceName}/verifyEmailComplete`, (secret_hash, { dispatch }) =>
180
+ GeneralApi.post(`${useradmApiUrl}/auth/verify-email/complete`, { secret_hash })
181
+ .catch(err => commonErrorHandler(err, 'An error occured completing the email verification process:', dispatch))
182
+ .finally(() => Promise.resolve(dispatch(getUser(OWN_USER_ID))))
183
+ );
184
+
185
+ export const verify2FA = createAsyncThunk(`${sliceName}/verify2FA`, (tfaData, { dispatch }) =>
186
+ UsersApi.putVerifyTFA(`${useradmApiUrl}/2faverify`, tfaData)
187
+ .then(() => Promise.resolve(dispatch(getUser(OWN_USER_ID))))
188
+ .catch(err =>
189
+ commonErrorHandler(err, 'An error occured validating the verification code: failed to verify token, please try again.', dispatch, undefined, true)
190
+ )
191
+ );
192
+
193
+ export const getUserList = createAsyncThunk(`${sliceName}/getUserList`, (_, { dispatch }) =>
194
+ GeneralApi.get(`${useradmApiUrl}/users`)
195
+ .then(res => {
196
+ const users = res.data.reduce((accu, item) => {
197
+ accu[item.id] = item;
198
+ return accu;
199
+ }, {});
200
+ return dispatch(actions.receivedUserList(users));
201
+ })
202
+ .catch(err => commonErrorHandler(err, `Users couldn't be loaded.`, dispatch, commonErrorFallback))
203
+ );
204
+
205
+ export const getUser = createAsyncThunk(`${sliceName}/getUser`, (id, { dispatch, rejectWithValue }) =>
206
+ GeneralApi.get(`${useradmApiUrl}/users/${id}`)
207
+ .then(({ data: user }) =>
208
+ Promise.all([
209
+ dispatch(actions.receivedUser(user)),
210
+ dispatch(setHideAnnouncement({ shouldHide: false, userId: user.id })),
211
+ dispatch(updateUserColumnSettings({ currentUserId: user.id })),
212
+ user
213
+ ])
214
+ )
215
+ .catch(e => rejectWithValue(e))
216
+ );
217
+
218
+ export const initializeSelf = createAsyncThunk(`${sliceName}/initializeSelf`, (_, { dispatch }) => dispatch(getUser(OWN_USER_ID)));
219
+
220
+ export const updateUserColumnSettings = createAsyncThunk(`${sliceName}/updateUserColumnSettings`, ({ columns, currentUserId }, { dispatch, getState }) => {
221
+ const userId = currentUserId ?? getCurrentUser(getState()).id;
222
+ const storageKey = `${userId}-column-widths`;
223
+ let customColumns = [];
224
+ if (!columns) {
225
+ try {
226
+ customColumns = JSON.parse(window.localStorage.getItem(storageKey)) || customColumns;
227
+ } catch {
228
+ // most likely the column info doesn't exist yet or is lost - continue
229
+ }
230
+ } else {
231
+ customColumns = columns;
232
+ }
233
+ window.localStorage.setItem(storageKey, JSON.stringify(customColumns));
234
+ return Promise.resolve(dispatch(actions.setCustomColumns(customColumns)));
235
+ });
236
+
237
+ const userActions = {
238
+ add: {
239
+ successMessage: 'The user was added successfully.',
240
+ errorMessage: 'adding'
241
+ },
242
+ create: {
243
+ successMessage: 'The user was created successfully.',
244
+ errorMessage: 'creating'
245
+ },
246
+ edit: {
247
+ successMessage: 'The user has been updated.',
248
+ errorMessage: 'editing'
249
+ },
250
+ remove: {
251
+ successMessage: 'The user was removed from the system.',
252
+ errorMessage: 'removing'
253
+ }
254
+ };
255
+
256
+ const userActionErrorHandler = (err, type, dispatch) => commonErrorHandler(err, `There was an error ${userActions[type].errorMessage} the user.`, dispatch);
257
+
258
+ export const createUser = createAsyncThunk(`${sliceName}/createUser`, ({ shouldResetPassword, ...userData }, { dispatch }) =>
259
+ GeneralApi.post(`${useradmApiUrl}/users`, { ...userData, send_reset_password: shouldResetPassword })
260
+ .then(() => Promise.all([dispatch(actions.createdUser(userData)), dispatch(getUserList()), dispatch(setSnackbar(userActions.create.successMessage))]))
261
+ .catch(err => userActionErrorHandler(err, 'create', dispatch))
262
+ );
263
+
264
+ export const removeUser = createAsyncThunk(`${sliceName}/removeUser`, (userId, { dispatch }) =>
265
+ GeneralApi.delete(`${useradmApiUrl}/users/${userId}`)
266
+ .then(() => Promise.all([dispatch(actions.removedUser(userId)), dispatch(getUserList()), dispatch(setSnackbar(userActions.remove.successMessage))]))
267
+ .catch(err => userActionErrorHandler(err, 'remove', dispatch))
268
+ );
269
+
270
+ export const editUser = createAsyncThunk(`${sliceName}/editUser`, ({ id, ...userData }, { dispatch, getState }) =>
271
+ GeneralApi.put(`${useradmApiUrl}/users/${id}`, userData).then(() =>
272
+ Promise.all([
273
+ dispatch(actions.updatedUser({ ...userData, id: id === OWN_USER_ID ? getCurrentUser(getState()).id : id })),
274
+ dispatch(setSnackbar(userActions.edit.successMessage))
275
+ ])
276
+ )
277
+ );
278
+
279
+ export const addUserToCurrentTenant = createAsyncThunk(`${sliceName}/addUserToTenant`, (userId, { dispatch, getState }) => {
280
+ const { id } = getOrganization(getState());
281
+ return GeneralApi.post(`${useradmApiUrl}/users/${userId}/assign`, { tenant_ids: [id] })
282
+ .catch(err => commonErrorHandler(err, `There was an error adding the user to your organization:`, dispatch))
283
+ .then(() => Promise.all([dispatch(setSnackbar(userActions.add.successMessage)), dispatch(getUserList())]));
284
+ });
285
+
286
+ export const enableUser2fa = createAsyncThunk(`${sliceName}/enableUser2fa`, (userId = OWN_USER_ID, { dispatch }) =>
287
+ GeneralApi.post(`${useradmApiUrl}/users/${userId}/2fa/enable`)
288
+ .catch(err => commonErrorHandler(err, `There was an error enabling Two Factor authentication for the user.`, dispatch))
289
+ .then(() => Promise.resolve(dispatch(getUser(userId))))
290
+ );
291
+
292
+ export const disableUser2fa = createAsyncThunk(`${sliceName}/disableUser2fa`, (userId = OWN_USER_ID, { dispatch }) =>
293
+ GeneralApi.post(`${useradmApiUrl}/users/${userId}/2fa/disable`)
294
+ .catch(err => commonErrorHandler(err, `There was an error disabling Two Factor authentication for the user.`, dispatch))
295
+ .then(() => Promise.resolve(dispatch(getUser(userId))))
296
+ );
297
+
298
+ /* RBAC related things follow: */
299
+ const mapHttpPermission = permission =>
300
+ Object.entries(uiPermissionsByArea).reduce(
301
+ (accu, [area, definition]) => {
302
+ const endpointMatches = definition.endpoints.filter(
303
+ endpoint => endpoint.path.test(permission.value) && (endpoint.types.includes(permission.type) || permission.type === PermissionTypes.Any)
304
+ );
305
+ if (permission.value === PermissionTypes.Any || (permission.value.includes(apiRoot) && endpointMatches.length)) {
306
+ const endpointUiPermission = endpointMatches.reduce((endpointAccu, endpoint) => [...endpointAccu, ...endpoint.uiPermissions], []);
307
+ const collector = (endpointUiPermission || definition.uiPermissions)
308
+ .reduce((permissionsAccu, uiPermission) => {
309
+ if (permission.type === PermissionTypes.Any || (!endpointMatches.length && uiPermission.verbs.some(verb => verb === permission.type))) {
310
+ permissionsAccu.push(uiPermission.value);
311
+ }
312
+ return permissionsAccu;
313
+ }, [])
314
+ .filter(duplicateFilter);
315
+ if (Array.isArray(accu[area])) {
316
+ accu[area] = [...accu[area], ...collector].filter(duplicateFilter);
317
+ } else {
318
+ accu[area] = mergePermissions(accu[area], { [scopedPermissionAreas[area].excessiveAccessSelector]: collector });
319
+ }
320
+ }
321
+ return accu;
322
+ },
323
+ { ...emptyUiPermissions }
324
+ );
325
+
326
+ const permissionActionTypes = {
327
+ any: mapHttpPermission,
328
+ CREATE_DEPLOYMENT: permission =>
329
+ permission.type === PermissionTypes.DeviceGroup
330
+ ? {
331
+ deployments: [uiPermissionsById.deploy.value],
332
+ groups: { [permission.value]: [uiPermissionsById.deploy.value] }
333
+ }
334
+ : {},
335
+ http: mapHttpPermission,
336
+ REMOTE_TERMINAL: permission =>
337
+ permission.type === PermissionTypes.DeviceGroup
338
+ ? {
339
+ groups: { [permission.value]: [uiPermissionsById.connect.value] }
340
+ }
341
+ : {},
342
+ VIEW_DEVICE: permission =>
343
+ permission.type === PermissionTypes.DeviceGroup
344
+ ? {
345
+ groups: { [permission.value]: [uiPermissionsById.read.value] }
346
+ }
347
+ : {}
348
+ };
349
+
350
+ const combinePermissions = (existingPermissions, additionalPermissions = {}) =>
351
+ Object.entries(additionalPermissions).reduce((accu, [name, permissions]) => {
352
+ let maybeExistingPermissions = accu[name] || [];
353
+ accu[name] = [...permissions, ...maybeExistingPermissions].filter(duplicateFilter);
354
+ return accu;
355
+ }, existingPermissions);
356
+
357
+ const tryParseCustomPermission = permission => {
358
+ const uiPermissions = permissionActionTypes[permission.action](permission.object);
359
+ const result = mergePermissions({ ...emptyUiPermissions }, uiPermissions);
360
+ return { isCustom: true, permission, result };
361
+ };
362
+
363
+ const customPermissionHandler = (accu, permission) => {
364
+ let processor = tryParseCustomPermission(permission);
365
+ return {
366
+ ...accu,
367
+ isCustom: accu.isCustom || processor.isCustom,
368
+ uiPermissions: mergePermissions(accu.uiPermissions, processor.result)
369
+ };
370
+ };
371
+
372
+ const mapPermissionSet = (permissionSetName, names, scope, existingGroupsPermissions = {}) => {
373
+ const permission = Object.values(uiPermissionsById).find(permission => permission.permissionSets[scope] === permissionSetName).value;
374
+ const scopedPermissions = names.reduce((accu, name) => combinePermissions(accu, { [name]: [permission] }), existingGroupsPermissions);
375
+ return Object.entries(scopedPermissions).reduce((accu, [key, permissions]) => ({ ...accu, [key]: deriveImpliedAreaPermissions(scope, permissions) }), {});
376
+ };
377
+
378
+ const isEmptyPermissionSet = permissionSet =>
379
+ !Object.values(permissionSet).reduce((accu, permissions) => {
380
+ if (Array.isArray(permissions)) {
381
+ return accu || !!permissions.length;
382
+ }
383
+ return accu || !isEmpty(permissions);
384
+ }, false);
385
+
386
+ const parseRolePermissions = ({ permission_sets_with_scope = [], permissions = [] }, permissionSets) => {
387
+ const preliminaryResult = permission_sets_with_scope.reduce(
388
+ (accu, permissionSet) => {
389
+ let processor = permissionSets[permissionSet.name];
390
+ if (!processor) {
391
+ return accu;
392
+ }
393
+ const scope = Object.keys(scopedPermissionAreas).find(scope => uiPermissionsByArea[scope].scope === permissionSet.scope?.type);
394
+ if (scope) {
395
+ const result = mapPermissionSet(permissionSet.name, permissionSet.scope.value, scope, accu.uiPermissions[scope]);
396
+ return { ...accu, uiPermissions: { ...accu.uiPermissions, [scope]: result } };
397
+ } else if (isEmptyPermissionSet(processor.result)) {
398
+ return processor.permissions.reduce(customPermissionHandler, accu);
399
+ }
400
+ return {
401
+ ...accu,
402
+ isCustom: accu.isCustom || processor.isCustom,
403
+ uiPermissions: mergePermissions(accu.uiPermissions, processor.result)
404
+ };
405
+ },
406
+ { isCustom: false, uiPermissions: { ...emptyUiPermissions, groups: {}, releases: {} } }
407
+ );
408
+ return permissions.reduce(customPermissionHandler, preliminaryResult);
409
+ };
410
+
411
+ export const normalizeRbacRoles = (roles, rolesById, permissionSets) =>
412
+ roles.reduce(
413
+ (accu, role) => {
414
+ let normalizedPermissions;
415
+ let isCustom = false;
416
+ if (rolesById[role.name]) {
417
+ normalizedPermissions = {
418
+ ...rolesById[role.name].uiPermissions,
419
+ groups: { ...rolesById[role.name].uiPermissions.groups },
420
+ releases: { ...rolesById[role.name].uiPermissions.releases }
421
+ };
422
+ } else {
423
+ const result = parseRolePermissions(role, permissionSets);
424
+ normalizedPermissions = result.uiPermissions;
425
+ isCustom = result.isCustom;
426
+ }
427
+
428
+ const roleState = accu[role.name] ?? { ...emptyRole };
429
+ accu[role.name] = {
430
+ ...roleState,
431
+ ...role,
432
+ description: roleState.description ? roleState.description : role.description,
433
+ editable: !defaultRolesById[role.name] && !isCustom && (typeof roleState.editable !== 'undefined' ? roleState.editable : true),
434
+ isCustom,
435
+ name: roleState.name ? roleState.name : role.name,
436
+ uiPermissions: normalizedPermissions
437
+ };
438
+ return accu;
439
+ },
440
+ { ...rolesById }
441
+ );
442
+
443
+ export const getPermissionSets = createAsyncThunk(`${sliceName}/getPermissionSets`, (_, { dispatch, getState }) =>
444
+ GeneralApi.get(`${useradmApiUrlv2}/permission_sets?per_page=500`)
445
+ .then(({ data }) => {
446
+ const permissionSets = data.reduce(
447
+ (accu, permissionSet) => {
448
+ const permissionSetState = accu[permissionSet.name] ?? {};
449
+ let permissionSetObject = { ...permissionSetState, ...permissionSet };
450
+ permissionSetObject.result = Object.values(uiPermissionsById).reduce(
451
+ (accu, item) =>
452
+ Object.entries(item.permissionSets).reduce((collector, [area, permissionSet]) => {
453
+ if (scopedPermissionAreas[area]) {
454
+ return collector;
455
+ }
456
+ if (permissionSet === permissionSetObject.name) {
457
+ collector[area] = [...collector[area], item.value].filter(duplicateFilter);
458
+ }
459
+ return collector;
460
+ }, accu),
461
+ { ...emptyUiPermissions, ...(permissionSetObject.result ?? {}) }
462
+ );
463
+ const scopes = Object.values(scopedPermissionAreas).reduce((accu, { key, scopeType }) => {
464
+ if (permissionSetObject.supported_scope_types?.includes(key) || permissionSetObject.supported_scope_types?.includes(scopeType)) {
465
+ accu.push(key);
466
+ }
467
+ return accu;
468
+ }, []);
469
+ permissionSetObject = scopes.reduce((accu, scope) => {
470
+ accu.result[scope] = mapPermissionSet(permissionSetObject.name, [scopedPermissionAreas[scope].excessiveAccessSelector], scope);
471
+ return accu;
472
+ }, permissionSetObject);
473
+ accu[permissionSet.name] = permissionSetObject;
474
+ return accu;
475
+ },
476
+ { ...getState().users.permissionSetsById }
477
+ );
478
+ return Promise.all([dispatch(actions.receivedPermissionSets(permissionSets)), permissionSets]);
479
+ })
480
+ .catch(() => console.log('Permission set retrieval failed - likely accessing a non-RBAC backend'))
481
+ );
482
+
483
+ export const getRoles = createAsyncThunk(`${sliceName}/getRoles`, (_, { dispatch, getState }) =>
484
+ Promise.all([GeneralApi.get(`${useradmApiUrlv2}/roles?per_page=500`), dispatch(getPermissionSets())])
485
+ .then(results => {
486
+ if (!results) {
487
+ return Promise.resolve();
488
+ }
489
+ const [{ data: roles }, { payload: permissionSetTasks }] = results;
490
+ const rolesById = normalizeRbacRoles(roles, getRolesById(getState()), permissionSetTasks[permissionSetTasks.length - 1]);
491
+ return Promise.resolve(dispatch(actions.receivedRoles(rolesById)));
492
+ })
493
+ .catch(() => console.log('Role retrieval failed - likely accessing a non-RBAC backend'))
494
+ );
495
+
496
+ const deriveImpliedAreaPermissions = (area, areaPermissions, skipPermissions = []) => {
497
+ const highestAreaPermissionLevelSelected = areaPermissions.reduce(
498
+ (highest, current) => (uiPermissionsById[current].permissionLevel > highest ? uiPermissionsById[current].permissionLevel : highest),
499
+ 1
500
+ );
501
+ return uiPermissionsByArea[area].uiPermissions.reduce((permissions, current) => {
502
+ if ((current.permissionLevel < highestAreaPermissionLevelSelected || areaPermissions.includes(current.value)) && !skipPermissions.includes(current.value)) {
503
+ permissions.push(current.value);
504
+ }
505
+ return permissions;
506
+ }, []);
507
+ };
508
+
509
+ /**
510
+ * transforms [{ group: "groupName", uiPermissions: ["read", "manage", "connect"] }, ...] to
511
+ * [{ name: "ReadDevices", scope: { type: "DeviceGroups", value: ["groupName", ...] } }, ...]
512
+ */
513
+ const transformAreaRoleDataToScopedPermissionsSets = (area, areaPermissions, excessiveAccessSelector) => {
514
+ const permissionSetObject = areaPermissions.reduce((accu, { item, uiPermissions }) => {
515
+ // if permission area is release and item is release tag (not all releases) then exclude upload permission as it cannot be applied to tags
516
+ const skipPermissions = scopedPermissionAreas.releases.key === area && item !== ALL_RELEASES ? [uiPermissionsById.upload.value] : [];
517
+ const impliedPermissions = deriveImpliedAreaPermissions(area, uiPermissions, skipPermissions);
518
+ accu = impliedPermissions.reduce((itemPermissionAccu, impliedPermission) => {
519
+ const permissionSetState = itemPermissionAccu[uiPermissionsById[impliedPermission].permissionSets[area]] ?? {
520
+ type: uiPermissionsByArea[area].scope,
521
+ value: []
522
+ };
523
+ itemPermissionAccu[uiPermissionsById[impliedPermission].permissionSets[area]] = {
524
+ ...permissionSetState,
525
+ value: [...permissionSetState.value, item]
526
+ };
527
+ return itemPermissionAccu;
528
+ }, accu);
529
+ return accu;
530
+ }, {});
531
+ return Object.entries(permissionSetObject).map(([name, { value, ...scope }]) => {
532
+ if (value.includes(excessiveAccessSelector)) {
533
+ return { name };
534
+ }
535
+ return { name, scope: { ...scope, value: value.filter(duplicateFilter) } };
536
+ });
537
+ };
538
+
539
+ const transformRoleDataToRole = (roleData, roleState = {}) => {
540
+ const role = { ...roleState, ...roleData };
541
+ const { description = '', name, uiPermissions = emptyUiPermissions } = role;
542
+ const { maybeUiPermissions, remainderKeys } = Object.entries(emptyUiPermissions).reduce(
543
+ (accu, [key, emptyPermissions]) => {
544
+ if (!scopedPermissionAreas[key]) {
545
+ accu.remainderKeys.push(key);
546
+ } else if (uiPermissions[key]) {
547
+ accu.maybeUiPermissions[key] = uiPermissions[key].reduce(itemUiPermissionsReducer, emptyPermissions);
548
+ }
549
+ return accu;
550
+ },
551
+ { maybeUiPermissions: {}, remainderKeys: [] }
552
+ );
553
+ const { permissionSetsWithScope, roleUiPermissions } = remainderKeys.reduce(
554
+ (accu, area) => {
555
+ const areaPermissions = role.uiPermissions[area];
556
+ if (!Array.isArray(areaPermissions)) {
557
+ return accu;
558
+ }
559
+ const impliedPermissions = deriveImpliedAreaPermissions(area, areaPermissions);
560
+ accu.roleUiPermissions[area] = impliedPermissions;
561
+ const mappedPermissions = impliedPermissions.map(uiPermission => ({ name: uiPermissionsById[uiPermission].permissionSets[area] }));
562
+ accu.permissionSetsWithScope.push(...mappedPermissions);
563
+ return accu;
564
+ },
565
+ { permissionSetsWithScope: [{ name: defaultPermissionSets.Basic.name }], roleUiPermissions: {} }
566
+ );
567
+ const scopedPermissionSets = Object.values(scopedPermissionAreas).reduce((accu, { key, excessiveAccessSelector }) => {
568
+ if (!uiPermissions[key]) {
569
+ return accu;
570
+ }
571
+ accu.push(...transformAreaRoleDataToScopedPermissionsSets(key, uiPermissions[key], excessiveAccessSelector));
572
+ return accu;
573
+ }, []);
574
+ return {
575
+ permissionSetsWithScope: [...permissionSetsWithScope, ...scopedPermissionSets],
576
+ role: {
577
+ ...emptyRole,
578
+ name,
579
+ description: description ? description : roleState.description,
580
+ uiPermissions: {
581
+ ...emptyUiPermissions,
582
+ ...roleUiPermissions,
583
+ ...maybeUiPermissions
584
+ }
585
+ }
586
+ };
587
+ };
588
+
589
+ const roleActions = {
590
+ create: {
591
+ successMessage: 'The role was created successfully.',
592
+ errorMessage: 'creating'
593
+ },
594
+ edit: {
595
+ successMessage: 'The role has been updated.',
596
+ errorMessage: 'editing'
597
+ },
598
+ remove: {
599
+ successMessage: 'The role was deleted successfully.',
600
+ errorMessage: 'removing'
601
+ }
602
+ };
603
+
604
+ const roleActionErrorHandler = (err, type, dispatch) => commonErrorHandler(err, `There was an error ${roleActions[type].errorMessage} the role.`, dispatch);
605
+
606
+ export const createRole = createAsyncThunk(`${sliceName}/createRole`, (roleData, { dispatch }) => {
607
+ const { permissionSetsWithScope, role } = transformRoleDataToRole(roleData);
608
+ return GeneralApi.post(`${useradmApiUrlv2}/roles`, {
609
+ name: role.name,
610
+ description: role.description,
611
+ permission_sets_with_scope: permissionSetsWithScope
612
+ })
613
+ .then(() => Promise.all([dispatch(actions.createdRole(role)), dispatch(getRoles()), dispatch(setSnackbar(roleActions.create.successMessage))]))
614
+ .catch(err => roleActionErrorHandler(err, 'create', dispatch));
615
+ });
616
+
617
+ export const editRole = createAsyncThunk(`${sliceName}/editRole`, (roleData, { dispatch, getState }) => {
618
+ const { permissionSetsWithScope, role } = transformRoleDataToRole(roleData, getRolesById(getState())[roleData.name]);
619
+ return GeneralApi.put(`${useradmApiUrlv2}/roles/${role.name}`, {
620
+ description: role.description,
621
+ name: role.name,
622
+ permission_sets_with_scope: permissionSetsWithScope
623
+ })
624
+ .then(() => Promise.all([dispatch(actions.createdRole(role)), dispatch(getRoles()), dispatch(setSnackbar(roleActions.edit.successMessage))]))
625
+ .catch(err => roleActionErrorHandler(err, 'edit', dispatch));
626
+ });
627
+
628
+ export const removeRole = createAsyncThunk(`${sliceName}/removeRole`, (roleId, { dispatch }) =>
629
+ GeneralApi.delete(`${useradmApiUrlv2}/roles/${roleId}`)
630
+ .then(() => Promise.all([dispatch(actions.removedRole(roleId)), dispatch(getRoles()), dispatch(setSnackbar(roleActions.remove.successMessage))]))
631
+ .catch(err => roleActionErrorHandler(err, 'remove', dispatch))
632
+ );
633
+
634
+ /*
635
+ Global settings
636
+ */
637
+ export const getGlobalSettings = createAsyncThunk(`${sliceName}/getGlobalSettings`, (_, { dispatch }) =>
638
+ GeneralApi.get(`${useradmApiUrl}/settings`).then(({ data: settings, headers: { etag } }) => {
639
+ window.sessionStorage.setItem(settingsKeys.initialized, true);
640
+ return Promise.all([dispatch(actions.setGlobalSettings(settings)), dispatch(setOfflineThreshold()), etag]);
641
+ })
642
+ );
643
+
644
+ export const saveGlobalSettings = createAsyncThunk(
645
+ `${sliceName}/saveGlobalSettings`,
646
+ ({ beOptimistic = false, notify = false, ...settings }, { dispatch, getState }) => {
647
+ if (!window.sessionStorage.getItem(settingsKeys.initialized) && !beOptimistic) {
648
+ return;
649
+ }
650
+ return dispatch(getGlobalSettings())
651
+ .unwrap()
652
+ .then(result => {
653
+ let updatedSettings = { ...getState().users.globalSettings, ...settings };
654
+ if (getCurrentUser(getState()).verified) {
655
+ updatedSettings['2fa'] = twoFAStates.enabled;
656
+ } else {
657
+ delete updatedSettings['2fa'];
658
+ }
659
+ let tasks = [dispatch(actions.setGlobalSettings(updatedSettings))];
660
+ const headers = result[result.length - 1] ? { 'If-Match': result[result.length - 1] } : {};
661
+ return GeneralApi.post(`${useradmApiUrl}/settings`, updatedSettings, { headers })
662
+ .then(() => {
663
+ if (notify) {
664
+ tasks.push(dispatch(setSnackbar('Settings saved successfully')));
665
+ }
666
+ return Promise.all(tasks);
667
+ })
668
+ .catch(err => {
669
+ if (beOptimistic) {
670
+ return Promise.all([tasks]);
671
+ }
672
+ console.log(err);
673
+ return commonErrorHandler(err, `The settings couldn't be saved.`, dispatch);
674
+ });
675
+ });
676
+ }
677
+ );
678
+
679
+ export const getUserSettings = createAsyncThunk(`${sliceName}/getUserSettings`, (_, { dispatch }) =>
680
+ GeneralApi.get(`${useradmApiUrl}/settings/me`).then(({ data: settings, headers: { etag } }) => {
681
+ window.sessionStorage.setItem(settingsKeys.initialized, true);
682
+ return Promise.all([dispatch(actions.setUserSettings(settings)), etag]);
683
+ })
684
+ );
685
+
686
+ export const saveUserSettings = createAsyncThunk(`${sliceName}/saveUserSettings`, (settings = { onboarding: {} }, { dispatch, getState }) => {
687
+ if (!getCurrentUser(getState()).id) {
688
+ return Promise.resolve();
689
+ }
690
+ return dispatch(getUserSettings())
691
+ .unwrap()
692
+ .then(result => {
693
+ const userSettings = getUserSettingsSelector(getState());
694
+ const onboardingState = getOnboardingState(getState());
695
+ const tooltipState = getTooltipsState(getState());
696
+ const updatedSettings = {
697
+ ...userSettings,
698
+ ...settings,
699
+ onboarding: {
700
+ ...onboardingState,
701
+ ...settings.onboarding
702
+ },
703
+ tooltips: tooltipState
704
+ };
705
+ const headers = result[result.length - 1] ? { 'If-Match': result[result.length - 1] } : {};
706
+ return Promise.all([
707
+ Promise.resolve(dispatch(actions.setUserSettings(updatedSettings))),
708
+ GeneralApi.post(`${useradmApiUrl}/settings/me`, updatedSettings, { headers })
709
+ ]).catch(() => dispatch(actions.setUserSettings(userSettings)));
710
+ });
711
+ });
712
+
713
+ export const get2FAQRCode = createAsyncThunk(`${sliceName}/get2FAQRCode`, (_, { dispatch }) =>
714
+ GeneralApi.get(`${useradmApiUrl}/2faqr`).then(res => dispatch(actions.receivedQrCode(res.data.qr)))
715
+ );
716
+
717
+ export const setHideAnnouncement = createAsyncThunk(`${sliceName}/setHideAnnouncement`, ({ shouldHide, userId }, { dispatch, getState }) => {
718
+ const currentUserId = userId || getCurrentUser(getState()).id;
719
+ const hash = getState().app.hostedAnnouncement ? hashString(getState().app.hostedAnnouncement) : '';
720
+ const announceCookie = cookies.get(`${currentUserId}${hash}`);
721
+ if (shouldHide || (hash.length && typeof announceCookie !== 'undefined')) {
722
+ cookies.set(`${currentUserId}${hash}`, true, { maxAge: 604800 });
723
+ return Promise.resolve(dispatch(setAnnouncement()));
724
+ }
725
+ return Promise.resolve();
726
+ });
727
+
728
+ export const getTokens = createAsyncThunk(`${sliceName}/getTokens`, (_, { dispatch, getState }) =>
729
+ GeneralApi.get(`${useradmApiUrl}/settings/tokens`).then(({ data: tokens }) => {
730
+ const user = getCurrentUser(getState());
731
+ const updatedUser = {
732
+ ...user,
733
+ tokens
734
+ };
735
+ return Promise.resolve(dispatch(actions.updatedUser(updatedUser)));
736
+ })
737
+ );
738
+
739
+ const ONE_YEAR = 31536000;
740
+
741
+ export const generateToken = createAsyncThunk(`${sliceName}/generateToken`, ({ expiresIn = ONE_YEAR, name }, { dispatch }) =>
742
+ GeneralApi.post(`${useradmApiUrl}/settings/tokens`, { name, expires_in: expiresIn })
743
+ .then(({ data: token }) => Promise.all([dispatch(getTokens()), token]))
744
+ .catch(err => commonErrorHandler(err, 'There was an error creating the token:', dispatch))
745
+ );
746
+
747
+ export const revokeToken = createAsyncThunk(`${sliceName}/revokeToken`, (token, { dispatch }) =>
748
+ GeneralApi.delete(`${useradmApiUrl}/settings/tokens/${token.id}`).then(() => Promise.resolve(dispatch(getTokens())))
749
+ );
750
+
751
+ export const setTooltipReadState = createAsyncThunk(`${sliceName}/setTooltipReadState`, ({ persist, ...remainder }, { dispatch }) => {
752
+ let tasks = [dispatch(actions.setTooltipState(remainder))];
753
+ if (persist) {
754
+ tasks.push(dispatch(saveUserSettings()));
755
+ }
756
+ return Promise.all(tasks);
757
+ });
758
+
759
+ export const setAllTooltipsReadState = createAsyncThunk(`${sliceName}/toggleHelptips`, ({ readState = READ_STATES.read, tooltips }, { dispatch }) => {
760
+ const updatedTips = tooltips.reduce((accu, id) => ({ ...accu, [id]: { readState } }), {});
761
+ return Promise.resolve(dispatch(actions.setTooltipsState(updatedTips))).then(() => dispatch(saveUserSettings()));
762
+ });
763
+
764
+ export const submitFeedback = createAsyncThunk(`${sliceName}/submitFeedback`, ({ satisfaction, feedback, ...meta }, { dispatch }) =>
765
+ GeneralApi.post(`${tenantadmApiUrlv2}/contact/support`, {
766
+ subject: 'feedback submission',
767
+ body: JSON.stringify({ feedback, satisfaction, meta })
768
+ }).then(() => {
769
+ const today = new Date();
770
+ dispatch(saveUserSettings({ feedbackCollectedAt: today.toISOString().split('T')[0] }));
771
+ setTimeout(() => dispatch(actions.setShowFeedbackDialog(false)), TIMEOUTS.threeSeconds);
772
+ })
773
+ );