@strapi/admin 4.4.0-beta.3 → 4.4.0-rc.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 (92) hide show
  1. package/admin/src/StrapiApp.js +2 -0
  2. package/admin/src/components/AuthenticatedApp/index.js +2 -2
  3. package/admin/src/components/LanguageProvider/index.js +1 -0
  4. package/admin/src/components/ThemeToggleProvider/index.js +21 -10
  5. package/admin/src/content-manager/components/DynamicTable/index.js +2 -2
  6. package/admin/src/content-manager/components/InputJSON/index.js +2 -0
  7. package/admin/src/contexts/ApiTokenPermissions/index.js +24 -0
  8. package/admin/src/hooks/index.js +1 -0
  9. package/admin/src/hooks/useRegenerate/index.js +34 -0
  10. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/ActionBoundRoutes/index.js +56 -0
  11. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/BoundRoute/getMethodColor.js +41 -0
  12. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/BoundRoute/index.js +72 -0
  13. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/CollapsableContentType/CheckBoxWrapper.js +30 -0
  14. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/CollapsableContentType/index.js +150 -0
  15. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/ContenTypesSection/index.js +37 -0
  16. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormApiTokenContainer/index.js +254 -0
  17. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormBody/index.js +77 -0
  18. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormHead/index.js +85 -0
  19. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Permissions/index.js +40 -0
  20. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Regenerate/index.js +68 -0
  21. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/index.js +215 -197
  22. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/init.js +13 -0
  23. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/reducer.js +72 -0
  24. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/getDateOfExpiration.js +16 -0
  25. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/index.js +5 -0
  26. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/schema.js +2 -1
  27. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/transformPermissionsData.js +36 -0
  28. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/DefaultButton/index.js +63 -0
  29. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/DeleteButton/index.js +1 -0
  30. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/ReadButton/index.js +19 -0
  31. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/UpdateButton/index.js +3 -36
  32. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/index.js +13 -11
  33. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/index.js +3 -2
  34. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/utils/tableHeaders.js +8 -8
  35. package/admin/src/pages/SettingsPage/pages/ApiTokens/ProtectedEditView/index.js +1 -1
  36. package/admin/src/permissions/defaultPermissions.js +2 -6
  37. package/admin/src/translations/en.json +17 -0
  38. package/admin/src/translations/zh-Hans.json +0 -1
  39. package/build/2077.c935ee42.chunk.js +205 -0
  40. package/build/4318.7d167b58.chunk.js +30 -0
  41. package/build/{4715.3f6cac0a.chunk.js → 4715.58cd558f.chunk.js} +32 -31
  42. package/build/4982.05eda880.chunk.js +324 -0
  43. package/build/7379.d246dd38.chunk.js +1 -0
  44. package/build/{7866.c793a31d.chunk.js → 7866.1201afbd.chunk.js} +2 -2
  45. package/build/{8773.eccaa5f3.chunk.js → 8773.c06c24c0.chunk.js} +2 -2
  46. package/build/{Admin-authenticatedApp.50e41ff2.chunk.js → Admin-authenticatedApp.9dec5230.chunk.js} +6 -6
  47. package/build/{Admin_homePage.118926e0.chunk.js → Admin_homePage.6d5e3236.chunk.js} +1 -1
  48. package/build/{Admin_profilePage.9d50ac44.chunk.js → Admin_profilePage.da32abbc.chunk.js} +1 -1
  49. package/build/{Admin_settingsPage.98a711e5.chunk.js → Admin_settingsPage.98e2a62b.chunk.js} +16 -16
  50. package/build/admin-app.a61d5c2e.chunk.js +112 -0
  51. package/build/admin-edit-roles-page.4dd6bcb9.chunk.js +1 -0
  52. package/build/api-tokens-create-page.93dd0689.chunk.js +1 -0
  53. package/build/api-tokens-edit-page.b0adac81.chunk.js +1 -0
  54. package/build/api-tokens-list-page.bb36535f.chunk.js +16 -0
  55. package/build/{content-manager.2a6f876d.chunk.js → content-manager.feb0d540.chunk.js} +2 -2
  56. package/build/{content-type-builder.d4610e20.chunk.js → content-type-builder.a684b2e8.chunk.js} +11 -11
  57. package/build/en-json.a9918c93.chunk.js +1 -0
  58. package/build/index.html +1 -1
  59. package/build/{main.fdc482f3.js → main.e4065f58.js} +1304 -1288
  60. package/build/{runtime~main.29105d25.js → runtime~main.4204f341.js} +2 -2
  61. package/build/sso-settings-page.9ceb0140.chunk.js +1 -0
  62. package/build/{webhook-edit-page.d2ea3351.chunk.js → webhook-edit-page.9e46fc3f.chunk.js} +1 -1
  63. package/build/{zh-Hans-json.77a42bc5.chunk.js → zh-Hans-json.9c99f8d4.chunk.js} +1 -1
  64. package/package.json +10 -9
  65. package/server/bootstrap.js +19 -1
  66. package/server/config/admin-actions.js +20 -0
  67. package/server/content-types/api-token-permission.js +36 -0
  68. package/server/content-types/api-token.js +25 -1
  69. package/server/content-types/index.js +1 -0
  70. package/server/controllers/api-token.js +24 -1
  71. package/server/controllers/content-api.js +15 -0
  72. package/server/controllers/index.js +1 -0
  73. package/server/routes/api-tokens.js +11 -0
  74. package/server/routes/content-api.js +20 -0
  75. package/server/routes/index.js +2 -0
  76. package/server/services/api-token.js +310 -29
  77. package/server/services/constants.js +10 -0
  78. package/server/services/permission/engine.js +36 -226
  79. package/server/services/permission.js +4 -1
  80. package/server/strategies/admin.js +7 -1
  81. package/server/strategies/api-token.js +71 -11
  82. package/server/validation/api-tokens.js +12 -2
  83. package/build/2077.61cebc93.chunk.js +0 -195
  84. package/build/4982.c6f88c5d.chunk.js +0 -314
  85. package/build/admin-app.8bc3e80f.chunk.js +0 -112
  86. package/build/admin-edit-roles-page.554ba3fa.chunk.js +0 -1
  87. package/build/api-tokens-create-page.4c262d6e.chunk.js +0 -1
  88. package/build/api-tokens-edit-page.10a9d368.chunk.js +0 -1
  89. package/build/api-tokens-list-page.442c9f3c.chunk.js +0 -15
  90. package/build/en-json.12bc5a14.chunk.js +0 -1
  91. package/build/sso-settings-page.445184e0.chunk.js +0 -1
  92. package/server/services/permission/engine-hooks.js +0 -82
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef } from 'react';
1
+ import React, { useEffect, useState, useRef, useReducer } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
3
  import {
4
4
  SettingsPageTitle,
@@ -8,62 +8,134 @@ import {
8
8
  useNotification,
9
9
  useTracking,
10
10
  useGuidedTour,
11
- Link,
11
+ useRBAC,
12
12
  } from '@strapi/helper-plugin';
13
- import { HeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
14
13
  import { Main } from '@strapi/design-system/Main';
15
- import { Button } from '@strapi/design-system/Button';
16
- import Check from '@strapi/icons/Check';
17
- import ArrowLeft from '@strapi/icons/ArrowLeft';
18
14
  import { Formik } from 'formik';
19
- import { Stack } from '@strapi/design-system/Stack';
20
- import { Box } from '@strapi/design-system/Box';
21
- import { Typography } from '@strapi/design-system/Typography';
22
- import { Grid, GridItem } from '@strapi/design-system/Grid';
23
- import { TextInput } from '@strapi/design-system/TextInput';
24
- import { Textarea } from '@strapi/design-system/Textarea';
25
- import { Select, Option } from '@strapi/design-system/Select';
26
- import get from 'lodash/get';
15
+ import { get } from 'lodash';
27
16
  import { useRouteMatch, useHistory } from 'react-router-dom';
28
17
  import { useQuery } from 'react-query';
29
18
  import { formatAPIErrors } from '../../../../../utils';
30
19
  import { axiosInstance } from '../../../../../core/utils';
31
- import schema from './utils/schema';
20
+ import { schema } from './utils';
32
21
  import LoadingView from './components/LoadingView';
33
- import HeaderContentBox from './components/ContentBox';
22
+ import FormHead from './components/FormHead';
23
+ import FormBody from './components/FormBody';
24
+ import adminPermissions from '../../../../../permissions';
25
+ import { ApiTokenPermissionsContextProvider } from '../../../../../contexts/ApiTokenPermissions';
26
+ import init from './init';
27
+ import reducer, { initialState } from './reducer';
28
+
29
+ const MSG_ERROR_NAME_TAKEN = 'Name already taken';
34
30
 
35
31
  const ApiTokenCreateView = () => {
36
- let apiToken;
37
32
  useFocusWhenNavigate();
38
33
  const { formatMessage } = useIntl();
39
34
  const { lockApp, unlockApp } = useOverlayBlocker();
40
35
  const toggleNotification = useNotification();
41
36
  const history = useHistory();
37
+ const [apiToken, setApiToken] = useState(
38
+ history.location.state?.apiToken.accessKey
39
+ ? {
40
+ ...history.location.state.apiToken,
41
+ }
42
+ : null
43
+ );
42
44
  const { trackUsage } = useTracking();
43
45
  const trackUsageRef = useRef(trackUsage);
44
46
  const { setCurrentStep } = useGuidedTour();
45
-
47
+ const {
48
+ allowedActions: { canCreate, canUpdate, canRegenerate },
49
+ } = useRBAC(adminPermissions.settings['api-tokens']);
50
+ const [state, dispatch] = useReducer(reducer, initialState, (state) => init(state, {}));
46
51
  const {
47
52
  params: { id },
48
53
  } = useRouteMatch('/settings/api-tokens/:id');
49
54
 
50
55
  const isCreating = id === 'create';
51
56
 
57
+ useQuery(
58
+ 'content-api-permissions',
59
+ async () => {
60
+ const [permissions, routes] = await Promise.all(
61
+ ['/admin/content-api/permissions', '/admin/content-api/routes'].map(async (url) => {
62
+ const { data } = await axiosInstance.get(url);
63
+
64
+ return data.data;
65
+ })
66
+ );
67
+
68
+ dispatch({
69
+ type: 'UPDATE_PERMISSIONS_LAYOUT',
70
+ value: permissions,
71
+ });
72
+
73
+ dispatch({
74
+ type: 'UPDATE_ROUTES',
75
+ value: routes,
76
+ });
77
+
78
+ if (apiToken) {
79
+ if (apiToken?.type === 'read-only') {
80
+ dispatch({
81
+ type: 'ON_CHANGE_READ_ONLY',
82
+ });
83
+ }
84
+ if (apiToken?.type === 'full-access') {
85
+ dispatch({
86
+ type: 'SELECT_ALL_ACTIONS',
87
+ });
88
+ }
89
+ if (apiToken?.type === 'custom') {
90
+ dispatch({
91
+ type: 'UPDATE_PERMISSIONS',
92
+ value: apiToken?.permissions,
93
+ });
94
+ }
95
+ }
96
+ },
97
+ {
98
+ onError() {
99
+ toggleNotification({
100
+ type: 'warning',
101
+ message: { id: 'notification.error', defaultMessage: 'An error occured' },
102
+ });
103
+ },
104
+ }
105
+ );
106
+
52
107
  useEffect(() => {
53
108
  trackUsageRef.current(isCreating ? 'didAddTokenFromList' : 'didEditTokenFromList');
54
109
  }, [isCreating]);
55
110
 
56
- if (history.location.state?.apiToken.accessKey) {
57
- apiToken = history.location.state.apiToken;
58
- }
59
-
60
- const { status, data } = useQuery(
111
+ const { status } = useQuery(
61
112
  ['api-token', id],
62
113
  async () => {
63
114
  const {
64
115
  data: { data },
65
116
  } = await axiosInstance.get(`/admin/api-tokens/${id}`);
66
117
 
118
+ setApiToken({
119
+ ...data,
120
+ });
121
+
122
+ if (data?.type === 'read-only') {
123
+ dispatch({
124
+ type: 'ON_CHANGE_READ_ONLY',
125
+ });
126
+ }
127
+ if (data?.type === 'full-access') {
128
+ dispatch({
129
+ type: 'SELECT_ALL_ACTIONS',
130
+ });
131
+ }
132
+ if (data?.type === 'custom') {
133
+ dispatch({
134
+ type: 'UPDATE_PERMISSIONS',
135
+ value: data?.permissions,
136
+ });
137
+ }
138
+
67
139
  return data;
68
140
  },
69
141
  {
@@ -77,10 +149,6 @@ const ApiTokenCreateView = () => {
77
149
  }
78
150
  );
79
151
 
80
- if (data) {
81
- apiToken = data;
82
- }
83
-
84
152
  const handleSubmit = async (body, actions) => {
85
153
  trackUsageRef.current(isCreating ? 'willCreateToken' : 'willEditToken');
86
154
  lockApp();
@@ -89,37 +157,98 @@ const ApiTokenCreateView = () => {
89
157
  const {
90
158
  data: { data: response },
91
159
  } = isCreating
92
- ? await axiosInstance.post(`/admin/api-tokens`, body)
93
- : await axiosInstance.put(`/admin/api-tokens/${id}`, body);
160
+ ? await axiosInstance.post(`/admin/api-tokens`, {
161
+ ...body,
162
+ lifespan:
163
+ body.lifespan && parseInt(body.lifespan, 10)
164
+ ? parseInt(body.lifespan, 10)
165
+ : body.lifespan,
166
+ permissions: body.type === 'custom' ? state.selectedActions : null,
167
+ })
168
+ : await axiosInstance.put(`/admin/api-tokens/${id}`, {
169
+ name: body.name,
170
+ description: body.description,
171
+ type: body.type,
172
+ permissions: body.type === 'custom' ? state.selectedActions : null,
173
+ });
94
174
 
95
- apiToken = response;
175
+ if (isCreating) {
176
+ history.replace(`/settings/api-tokens/${response.id}`, { apiToken: response });
177
+ setCurrentStep('apiTokens.success');
178
+ }
179
+ unlockApp();
180
+ setApiToken({
181
+ ...response,
182
+ });
96
183
 
97
184
  toggleNotification({
98
185
  type: 'success',
99
- message: formatMessage({ id: 'notification.success.saved', defaultMessage: 'Saved' }),
186
+ message: isCreating
187
+ ? formatMessage({
188
+ id: 'notification.success.tokencreated',
189
+ defaultMessage: 'API Token successfully created',
190
+ })
191
+ : formatMessage({
192
+ id: 'notification.success.tokenedited',
193
+ defaultMessage: 'API Token successfully edited',
194
+ }),
100
195
  });
101
196
 
102
197
  trackUsageRef.current(isCreating ? 'didCreateToken' : 'didEditToken', {
103
198
  type: apiToken.type,
104
199
  });
105
-
106
- if (isCreating) {
107
- history.replace(`/settings/api-tokens/${response.id}`, { apiToken: response });
108
- setCurrentStep('apiTokens.success');
109
- }
110
200
  } catch (err) {
111
201
  const errors = formatAPIErrors(err.response.data);
112
202
  actions.setErrors(errors);
113
203
 
114
- toggleNotification({
115
- type: 'warning',
116
- message: get(err, 'response.data.message', 'notification.error'),
117
- });
204
+ if (err?.response?.data?.error?.message === MSG_ERROR_NAME_TAKEN) {
205
+ toggleNotification({
206
+ type: 'warning',
207
+ message: get(err, 'response.data.message', 'notification.error.tokennamenotunique'),
208
+ });
209
+ } else {
210
+ toggleNotification({
211
+ type: 'warning',
212
+ message: get(err, 'response.data.message', 'notification.error'),
213
+ });
214
+ }
215
+ unlockApp();
118
216
  }
217
+ };
119
218
 
120
- unlockApp();
219
+ const [hasChangedPermissions, setHasChangedPermissions] = useState(false);
220
+
221
+ const handleChangeCheckbox = ({ target: { value } }) => {
222
+ setHasChangedPermissions(true);
223
+ dispatch({
224
+ type: 'ON_CHANGE',
225
+ value,
226
+ });
227
+ };
228
+
229
+ const handleChangeSelectAllCheckbox = ({ target: { value } }) => {
230
+ setHasChangedPermissions(true);
231
+ dispatch({
232
+ type: 'SELECT_ALL_IN_PERMISSION',
233
+ value,
234
+ });
235
+ };
236
+
237
+ const setSelectedAction = ({ target: { value } }) => {
238
+ dispatch({
239
+ type: 'SET_SELECTED_ACTION',
240
+ value,
241
+ });
242
+ };
243
+
244
+ const providerValue = {
245
+ ...state,
246
+ onChange: handleChangeCheckbox,
247
+ onChangeSelectAll: handleChangeSelectAllCheckbox,
248
+ setSelectedAction,
121
249
  };
122
250
 
251
+ const canEditInputs = (canUpdate && !isCreating) || (canCreate && isCreating);
123
252
  const isLoading = !isCreating && !apiToken && status !== 'success';
124
253
 
125
254
  if (isLoading) {
@@ -127,162 +256,51 @@ const ApiTokenCreateView = () => {
127
256
  }
128
257
 
129
258
  return (
130
- <Main>
131
- <SettingsPageTitle name="API Tokens" />
132
- <Formik
133
- validationSchema={schema}
134
- validateOnChange={false}
135
- initialValues={{
136
- name: apiToken?.name || '',
137
- description: apiToken?.description || '',
138
- type: apiToken?.type || 'read-only',
139
- }}
140
- onSubmit={handleSubmit}
141
- >
142
- {({ errors, handleChange, isSubmitting, values }) => {
143
- return (
144
- <Form>
145
- <HeaderLayout
146
- title={
147
- apiToken?.name ||
148
- formatMessage({
149
- id: 'Settings.apiTokens.createPage.title',
150
- defaultMessage: 'Create API Token',
151
- })
152
- }
153
- primaryAction={
154
- <Button
155
- disabled={isSubmitting}
156
- loading={isSubmitting}
157
- startIcon={<Check />}
158
- type="submit"
159
- size="L"
160
- >
161
- {formatMessage({
162
- id: 'global.save',
163
- defaultMessage: 'Save',
164
- })}
165
- </Button>
166
- }
167
- navigationAction={
168
- <Link startIcon={<ArrowLeft />} to="/settings/api-tokens">
169
- {formatMessage({
170
- id: 'global.back',
171
- defaultMessage: 'Back',
172
- })}
173
- </Link>
174
- }
175
- />
176
- <ContentLayout>
177
- <Stack spacing={6}>
178
- {Boolean(apiToken?.name) && <HeaderContentBox apiToken={apiToken.accessKey} />}
179
- <Box
180
- background="neutral0"
181
- hasRadius
182
- shadow="filterShadow"
183
- paddingTop={6}
184
- paddingBottom={6}
185
- paddingLeft={7}
186
- paddingRight={7}
187
- >
188
- <Stack spacing={4}>
189
- <Typography variant="delta" as="h2">
190
- {formatMessage({
191
- id: 'global.details',
192
- defaultMessage: 'Details',
193
- })}
194
- </Typography>
195
- <Grid gap={5}>
196
- <GridItem key="name" col={6} xs={12}>
197
- <TextInput
198
- name="name"
199
- error={
200
- errors.name
201
- ? formatMessage(
202
- errors.name?.id
203
- ? errors.name
204
- : { id: errors.name, defaultMessage: errors.name }
205
- )
206
- : null
207
- }
208
- label={formatMessage({
209
- id: 'Settings.apiTokens.form.name',
210
- defaultMessage: 'Name',
211
- })}
212
- onChange={handleChange}
213
- value={values.name}
214
- required
215
- />
216
- </GridItem>
217
- <GridItem key="description" col={6} xs={12}>
218
- <Textarea
219
- label={formatMessage({
220
- id: 'Settings.apiTokens.form.description',
221
- defaultMessage: 'Description',
222
- })}
223
- name="description"
224
- error={
225
- errors.description
226
- ? formatMessage(
227
- errors.description?.id
228
- ? errors.description
229
- : {
230
- id: errors.description,
231
- defaultMessage: errors.description,
232
- }
233
- )
234
- : null
235
- }
236
- onChange={handleChange}
237
- >
238
- {values.description}
239
- </Textarea>
240
- </GridItem>
241
- <GridItem key="type" col={6} xs={12}>
242
- <Select
243
- name="type"
244
- label={formatMessage({
245
- id: 'Settings.apiTokens.form.type',
246
- defaultMessage: 'Token type',
247
- })}
248
- value={values.type}
249
- error={
250
- errors.type
251
- ? formatMessage(
252
- errors.type?.id
253
- ? errors.type
254
- : { id: errors.type, defaultMessage: errors.type }
255
- )
256
- : null
257
- }
258
- onChange={(value) => {
259
- handleChange({ target: { name: 'type', value } });
260
- }}
261
- >
262
- <Option value="read-only">
263
- {formatMessage({
264
- id: 'Settings.apiTokens.types.read-only',
265
- defaultMessage: 'Read-only',
266
- })}
267
- </Option>
268
- <Option value="full-access">
269
- {formatMessage({
270
- id: 'Settings.apiTokens.types.full-access',
271
- defaultMessage: 'Full access',
272
- })}
273
- </Option>
274
- </Select>
275
- </GridItem>
276
- </Grid>
277
- </Stack>
278
- </Box>
279
- </Stack>
280
- </ContentLayout>
281
- </Form>
282
- );
283
- }}
284
- </Formik>
285
- </Main>
259
+ <ApiTokenPermissionsContextProvider value={providerValue}>
260
+ <Main>
261
+ <SettingsPageTitle name="API Tokens" />
262
+ <Formik
263
+ validationSchema={schema}
264
+ validateOnChange={false}
265
+ initialValues={{
266
+ name: apiToken?.name || '',
267
+ description: apiToken?.description || '',
268
+ type: apiToken?.type,
269
+ lifespan: apiToken?.lifespan ? apiToken.lifespan.toString() : apiToken?.lifespan,
270
+ }}
271
+ enableReinitialize
272
+ onSubmit={(body, actions) => handleSubmit(body, actions)}
273
+ >
274
+ {({ errors, handleChange, isSubmitting, values, setFieldValue }) => {
275
+ if (hasChangedPermissions && values?.type !== 'custom') {
276
+ setFieldValue('type', 'custom');
277
+ }
278
+
279
+ return (
280
+ <Form>
281
+ <FormHead
282
+ apiToken={apiToken}
283
+ setApiToken={setApiToken}
284
+ canEditInputs={canEditInputs}
285
+ canRegenerate={canRegenerate}
286
+ isSubmitting={isSubmitting}
287
+ />
288
+ <FormBody
289
+ apiToken={apiToken}
290
+ errors={errors}
291
+ onChange={handleChange}
292
+ canEditInputs={canEditInputs}
293
+ isCreating={isCreating}
294
+ values={values}
295
+ onDispatch={dispatch}
296
+ setHasChangedPermissions={setHasChangedPermissions}
297
+ />
298
+ </Form>
299
+ );
300
+ }}
301
+ </Formik>
302
+ </Main>
303
+ </ApiTokenPermissionsContextProvider>
286
304
  );
287
305
  };
288
306
 
@@ -0,0 +1,13 @@
1
+ import { transformPermissionsData } from './utils';
2
+
3
+ const init = (state, permissions = []) => {
4
+ return {
5
+ ...state,
6
+ selectedAction: null,
7
+ routes: [],
8
+ selectedActions: [],
9
+ data: transformPermissionsData(permissions),
10
+ };
11
+ };
12
+
13
+ export default init;
@@ -0,0 +1,72 @@
1
+ /* eslint-disable consistent-return */
2
+ import produce from 'immer';
3
+ import { pull } from 'lodash';
4
+ import { transformPermissionsData } from './utils';
5
+
6
+ export const initialState = {
7
+ data: {},
8
+ selectedActions: [],
9
+ };
10
+
11
+ const reducer = (state, action) =>
12
+ produce(state, (draftState) => {
13
+ switch (action.type) {
14
+ case 'ON_CHANGE': {
15
+ if (draftState.selectedActions.includes(action.value)) {
16
+ pull(draftState.selectedActions, action.value);
17
+ } else {
18
+ draftState.selectedActions.push(action.value);
19
+ }
20
+ break;
21
+ }
22
+ case 'SELECT_ALL_IN_PERMISSION': {
23
+ const areAllSelected = action.value.every((item) =>
24
+ draftState.selectedActions.includes(item.actionId)
25
+ );
26
+
27
+ if (areAllSelected) {
28
+ action.value.forEach((item) => {
29
+ pull(draftState.selectedActions, item.actionId);
30
+ });
31
+ } else {
32
+ action.value.forEach((item) => {
33
+ draftState.selectedActions.push(item.actionId);
34
+ });
35
+ }
36
+ break;
37
+ }
38
+
39
+ case 'SELECT_ALL_ACTIONS': {
40
+ draftState.selectedActions = [...draftState.data.allActionsIds];
41
+
42
+ break;
43
+ }
44
+ case 'ON_CHANGE_READ_ONLY': {
45
+ const onlyReadOnlyActions = draftState.data.allActionsIds.filter(
46
+ (actionId) => actionId.includes('find') || actionId.includes('findOne')
47
+ );
48
+ draftState.selectedActions = [...onlyReadOnlyActions];
49
+ break;
50
+ }
51
+ case 'UPDATE_PERMISSIONS_LAYOUT': {
52
+ draftState.data = transformPermissionsData(action.value);
53
+ break;
54
+ }
55
+ case 'UPDATE_ROUTES': {
56
+ draftState.routes = { ...action.value };
57
+ break;
58
+ }
59
+ case 'UPDATE_PERMISSIONS': {
60
+ draftState.selectedActions = [...action.value];
61
+ break;
62
+ }
63
+ case 'SET_SELECTED_ACTION': {
64
+ draftState.selectedAction = action.value;
65
+ break;
66
+ }
67
+ default:
68
+ return draftState;
69
+ }
70
+ });
71
+
72
+ export default reducer;
@@ -0,0 +1,16 @@
1
+ import { addDays, format } from 'date-fns';
2
+ import * as locales from 'date-fns/locale';
3
+
4
+ const getDateOfExpiration = (createdAt, duration, language = 'en') => {
5
+ if (duration && typeof duration === 'number') {
6
+ const durationInDays = duration / 24 / 60 / 60 / 1000;
7
+
8
+ return format(addDays(new Date(createdAt), durationInDays), 'PPP', {
9
+ locale: locales[language],
10
+ });
11
+ }
12
+
13
+ return 'Unlimited';
14
+ };
15
+
16
+ export default getDateOfExpiration;
@@ -0,0 +1,5 @@
1
+ import getDateOfExpiration from './getDateOfExpiration';
2
+ import schema from './schema';
3
+ import transformPermissionsData from './transformPermissionsData';
4
+
5
+ export { getDateOfExpiration, schema, transformPermissionsData };
@@ -5,9 +5,10 @@ const schema = yup.object().shape({
5
5
  name: yup.string(translatedErrors.string).required(translatedErrors.required),
6
6
  type: yup
7
7
  .string(translatedErrors.string)
8
- .oneOf(['read-only', 'full-access'])
8
+ .oneOf(['read-only', 'full-access', 'custom'])
9
9
  .required(translatedErrors.required),
10
10
  description: yup.string().nullable(),
11
+ lifespan: yup.number().integer().min(1).nullable().defined(translatedErrors.required),
11
12
  });
12
13
 
13
14
  export default schema;
@@ -0,0 +1,36 @@
1
+ import { flatten } from 'lodash';
2
+
3
+ const transformPermissionsData = (data) => {
4
+ const layout = {
5
+ allActionsIds: [],
6
+ permissions: [],
7
+ };
8
+
9
+ layout.permissions = Object.keys(data).map((apiId) => ({
10
+ apiId,
11
+ label: apiId.split('::')[1],
12
+ controllers: flatten(
13
+ Object.keys(data[apiId].controllers).map((controller) => ({
14
+ controller,
15
+ actions: flatten(
16
+ data[apiId].controllers[controller].map((action) => {
17
+ const actionId = `${apiId}.${controller}.${action}`;
18
+
19
+ if (apiId.includes('api::')) {
20
+ layout.allActionsIds.push(actionId);
21
+ }
22
+
23
+ return {
24
+ action,
25
+ actionId,
26
+ };
27
+ })
28
+ ),
29
+ }))
30
+ ),
31
+ }));
32
+
33
+ return layout;
34
+ };
35
+
36
+ export default transformPermissionsData;