@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
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ collectionName: 'strapi_api_token_permissions',
5
+ info: {
6
+ name: 'API Token Permission',
7
+ description: '',
8
+ singularName: 'api-token-permission',
9
+ pluralName: 'api-token-permissions',
10
+ displayName: 'API Token Permission',
11
+ },
12
+ options: {},
13
+ pluginOptions: {
14
+ 'content-manager': {
15
+ visible: false,
16
+ },
17
+ 'content-type-builder': {
18
+ visible: false,
19
+ },
20
+ },
21
+ attributes: {
22
+ action: {
23
+ type: 'string',
24
+ minLength: 1,
25
+ configurable: false,
26
+ required: true,
27
+ },
28
+ token: {
29
+ configurable: false,
30
+ type: 'relation',
31
+ relation: 'manyToOne',
32
+ inversedBy: 'permissions',
33
+ target: 'admin::api-token',
34
+ },
35
+ },
36
+ };
@@ -26,6 +26,7 @@ module.exports = {
26
26
  minLength: 1,
27
27
  configurable: false,
28
28
  required: true,
29
+ unique: true,
29
30
  },
30
31
  description: {
31
32
  type: 'string',
@@ -38,7 +39,7 @@ module.exports = {
38
39
  type: 'enumeration',
39
40
  enum: Object.values(constants.API_TOKEN_TYPE),
40
41
  configurable: false,
41
- required: false,
42
+ required: true,
42
43
  default: constants.API_TOKEN_TYPE.READ_ONLY,
43
44
  },
44
45
  accessKey: {
@@ -47,5 +48,28 @@ module.exports = {
47
48
  configurable: false,
48
49
  required: true,
49
50
  },
51
+ lastUsedAt: {
52
+ type: 'datetime',
53
+ configurable: false,
54
+ required: false,
55
+ },
56
+ permissions: {
57
+ type: 'relation',
58
+ target: 'admin::api-token-permission',
59
+ relation: 'oneToMany',
60
+ mappedBy: 'token',
61
+ configurable: false,
62
+ required: false,
63
+ },
64
+ expiresAt: {
65
+ type: 'datetime',
66
+ configurable: false,
67
+ required: false,
68
+ },
69
+ lifespan: {
70
+ type: 'integer',
71
+ configurable: false,
72
+ required: false,
73
+ },
50
74
  },
51
75
  };
@@ -5,4 +5,5 @@ module.exports = {
5
5
  user: { schema: require('./User') },
6
6
  role: { schema: require('./Role') },
7
7
  'api-token': { schema: require('./api-token') },
8
+ 'api-token-permission': { schema: require('./api-token-permission') },
8
9
  };
@@ -24,6 +24,8 @@ module.exports = {
24
24
  name: trim(body.name),
25
25
  description: trim(body.description),
26
26
  type: body.type,
27
+ permissions: body.permissions,
28
+ lifespan: body.lifespan,
27
29
  };
28
30
 
29
31
  await validateApiTokenCreationInput(attributes);
@@ -37,6 +39,21 @@ module.exports = {
37
39
  ctx.created({ data: apiToken });
38
40
  },
39
41
 
42
+ async regenerate(ctx) {
43
+ const { id } = ctx.params;
44
+ const apiTokenService = getService('api-token');
45
+
46
+ const apiTokenExists = await apiTokenService.getById(id);
47
+ if (!apiTokenExists) {
48
+ ctx.notFound('API Token not found');
49
+ return;
50
+ }
51
+
52
+ const accessToken = await apiTokenService.regenerate(id);
53
+
54
+ ctx.created({ data: accessToken });
55
+ },
56
+
40
57
  async list(ctx) {
41
58
  const apiTokenService = getService('api-token');
42
59
  const apiTokens = await apiTokenService.list();
@@ -59,7 +76,6 @@ module.exports = {
59
76
 
60
77
  if (!apiToken) {
61
78
  ctx.notFound('API Token not found');
62
-
63
79
  return;
64
80
  }
65
81
 
@@ -108,4 +124,11 @@ module.exports = {
108
124
  const apiToken = await apiTokenService.update(id, attributes);
109
125
  ctx.send({ data: apiToken });
110
126
  },
127
+
128
+ async getLayout(ctx) {
129
+ const apiTokenService = getService('api-token');
130
+ const layout = await apiTokenService.getApiTokenLayout();
131
+
132
+ ctx.send({ data: layout });
133
+ },
111
134
  };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ async getPermissions(ctx) {
5
+ const actionsMap = await strapi.contentAPI.permissions.getActionsMap();
6
+
7
+ ctx.send({ data: actionsMap });
8
+ },
9
+
10
+ async getRoutes(ctx) {
11
+ const routesMap = await strapi.contentAPI.getRoutesMap();
12
+
13
+ ctx.send({ data: routesMap });
14
+ },
15
+ };
@@ -9,4 +9,5 @@ module.exports = {
9
9
  role: require('./role'),
10
10
  user: require('./user'),
11
11
  webhooks: require('./webhooks'),
12
+ 'content-api': require('./content-api'),
12
13
  };
@@ -56,4 +56,15 @@ module.exports = [
56
56
  ],
57
57
  },
58
58
  },
59
+ {
60
+ method: 'POST',
61
+ path: '/api-tokens/:id/regenerate',
62
+ handler: 'api-token.regenerate',
63
+ config: {
64
+ policies: [
65
+ 'admin::isAuthenticatedAdmin',
66
+ { name: 'admin::hasPermissions', config: { actions: ['admin::api-tokens.regenerate'] } },
67
+ ],
68
+ },
69
+ },
59
70
  ];
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ module.exports = [
4
+ {
5
+ method: 'GET',
6
+ path: '/content-api/permissions',
7
+ handler: 'content-api.getPermissions',
8
+ config: {
9
+ policies: ['admin::isAuthenticatedAdmin'],
10
+ },
11
+ },
12
+ {
13
+ method: 'GET',
14
+ path: '/content-api/routes',
15
+ handler: 'content-api.getRoutes',
16
+ config: {
17
+ policies: ['admin::isAuthenticatedAdmin'],
18
+ },
19
+ },
20
+ ];
@@ -7,6 +7,7 @@ const users = require('./users');
7
7
  const roles = require('./roles');
8
8
  const webhooks = require('./webhooks');
9
9
  const apiTokens = require('./api-tokens');
10
+ const contentApi = require('./content-api');
10
11
 
11
12
  module.exports = [
12
13
  ...admin,
@@ -16,4 +17,5 @@ module.exports = [
16
17
  ...roles,
17
18
  ...webhooks,
18
19
  ...apiTokens,
20
+ ...contentApi,
19
21
  ];
@@ -1,9 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
+ const { isNil } = require('lodash/fp');
5
+ const { omit, difference, isEmpty, map, isArray, uniq } = require('lodash/fp');
6
+ const { ValidationError, NotFoundError } = require('@strapi/utils').errors;
7
+ const constants = require('./constants');
4
8
 
5
9
  /**
6
- * @typedef {'read-only'|'full-access'} TokenType
10
+ * @typedef {'read-only'|'full-access'|'custom'} TokenType
7
11
  */
8
12
 
9
13
  /**
@@ -11,20 +15,135 @@ const crypto = require('crypto');
11
15
  *
12
16
  * @property {number|string} id
13
17
  * @property {string} name
14
- * @property {string} [description]
18
+ * @property {string} description
15
19
  * @property {string} accessKey
20
+ * @property {number} lastUsedAt
21
+ * @property {number} lifespan
22
+ * @property {number} expiresAt
16
23
  * @property {TokenType} type
24
+ * @property {(number|ApiTokenPermission)[]} permissions
17
25
  */
18
26
 
27
+ /**
28
+ * @typedef ApiTokenPermission
29
+ *
30
+ * @property {number|string} id
31
+ * @property {string} action
32
+ * @property {ApiToken|number} token
33
+ */
34
+
35
+ /** @constant {Array<string>} */
36
+ const SELECT_FIELDS = [
37
+ 'id',
38
+ 'name',
39
+ 'description',
40
+ 'lastUsedAt',
41
+ 'type',
42
+ 'lifespan',
43
+ 'expiresAt',
44
+ 'createdAt',
45
+ 'updatedAt',
46
+ ];
47
+
19
48
  /** @constant {Array<string>} */
20
- const SELECT_FIELDS = ['id', 'name', 'description', 'type', 'createdAt'];
49
+ const POPULATE_FIELDS = ['permissions'];
50
+
51
+ // TODO: we need to ensure the permissions are actually valid registered permissions!
52
+
53
+ /**
54
+ * Assert that a token's permissions attribute is valid for its type
55
+ *
56
+ * @param {ApiToken} token
57
+ */
58
+ const assertCustomTokenPermissionsValidity = (attributes) => {
59
+ // Ensure non-custom tokens doesn't have permissions
60
+ if (attributes.type !== constants.API_TOKEN_TYPE.CUSTOM && !isEmpty(attributes.permissions)) {
61
+ throw new ValidationError('Non-custom tokens should not reference permissions');
62
+ }
63
+
64
+ // Custom type tokens should always have permissions attached to them
65
+ if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM && !isArray(attributes.permissions)) {
66
+ throw new ValidationError('Missing permissions attribute for custom token');
67
+ }
68
+
69
+ // Permissions provided for a custom type token should be valid/registered permissions UID
70
+ if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM) {
71
+ const validPermissions = strapi.contentAPI.permissions.providers.action.keys();
72
+ const invalidPermissions = difference(attributes.permissions, validPermissions);
73
+
74
+ if (!isEmpty(invalidPermissions)) {
75
+ throw new ValidationError(`Unknown permissions provided: ${invalidPermissions.join(', ')}`);
76
+ }
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Assert that a token's permissions attribute is valid for its type
82
+ *
83
+ * @param {ApiToken} token
84
+ */
85
+ const assertValidLifespan = ({ lifespan }) => {
86
+ if (isNil(lifespan)) {
87
+ return;
88
+ }
89
+
90
+ if (!Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan)) {
91
+ throw new ValidationError(
92
+ `lifespan must be one of the following values:
93
+ ${Object.values(constants.API_TOKEN_LIFESPANS).join(', ')}`
94
+ );
95
+ }
96
+ };
97
+
98
+ /**
99
+ * Flatten a token's database permissions objects to an array of strings
100
+ *
101
+ * @param {ApiToken} token
102
+ *
103
+ * @returns {ApiToken}
104
+ */
105
+ const flattenTokenPermissions = (token) => {
106
+ if (!token) return token;
107
+ return {
108
+ ...token,
109
+ permissions: isArray(token.permissions) ? map('action', token.permissions) : token.permissions,
110
+ };
111
+ };
21
112
 
22
113
  /**
114
+ * Get a token
115
+ *
23
116
  * @param {Object} whereParams
24
- * @param {string|number} [whereParams.id]
25
- * @param {string} [whereParams.name]
26
- * @param {string} [whereParams.description]
27
- * @param {string} [whereParams.accessKey]
117
+ * @param {string|number} whereParams.id
118
+ * @param {string} whereParams.name
119
+ * @param {number} whereParams.lastUsedAt
120
+ * @param {string} whereParams.description
121
+ * @param {string} whereParams.accessKey
122
+ *
123
+ * @returns {Promise<Omit<ApiToken, 'accessKey'> | null>}
124
+ */
125
+ const getBy = async (whereParams = {}) => {
126
+ if (Object.keys(whereParams).length === 0) {
127
+ return null;
128
+ }
129
+
130
+ const token = await strapi
131
+ .query('admin::api-token')
132
+ .findOne({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: whereParams });
133
+
134
+ if (!token) return token;
135
+ return flattenTokenPermissions(token);
136
+ };
137
+
138
+ /**
139
+ * Check if token exists
140
+ *
141
+ * @param {Object} whereParams
142
+ * @param {string|number} whereParams.id
143
+ * @param {string} whereParams.name
144
+ * @param {number} whereParams.lastUsedAt
145
+ * @param {string} whereParams.description
146
+ * @param {string} whereParams.accessKey
28
147
  *
29
148
  * @returns {Promise<boolean>}
30
149
  */
@@ -35,6 +154,8 @@ const exists = async (whereParams = {}) => {
35
154
  };
36
155
 
37
156
  /**
157
+ * Return a secure sha512 hash of an accessKey
158
+ *
38
159
  * @param {string} accessKey
39
160
  *
40
161
  * @returns {string}
@@ -47,24 +168,103 @@ const hash = (accessKey) => {
47
168
  };
48
169
 
49
170
  /**
171
+ * @param {number} lifespan
172
+ *
173
+ * @returns { { lifespan: null | number, expiresAt: null | number } }
174
+ */
175
+ const getExpirationFields = (lifespan) => {
176
+ // it must be nil or a finite number >= 0
177
+ const isValidNumber = Number.isFinite(lifespan) && lifespan > 0;
178
+ if (!isValidNumber && !isNil(lifespan)) {
179
+ throw new ValidationError('lifespan must be a positive number or null');
180
+ }
181
+
182
+ return {
183
+ lifespan: lifespan || null,
184
+ expiresAt: lifespan ? Date.now() + lifespan : null,
185
+ };
186
+ };
187
+
188
+ /**
189
+ * Create a token and its permissions
190
+ *
50
191
  * @param {Object} attributes
51
192
  * @param {TokenType} attributes.type
52
193
  * @param {string} attributes.name
53
- * @param {string} [attributes.description]
194
+ * @param {number} attributes.lifespan
195
+ * @param {string[]} attributes.permissions
196
+ * @param {string} attributes.description
54
197
  *
55
198
  * @returns {Promise<ApiToken>}
56
199
  */
57
200
  const create = async (attributes) => {
58
201
  const accessKey = crypto.randomBytes(128).toString('hex');
59
202
 
203
+ assertCustomTokenPermissionsValidity(attributes);
204
+ assertValidLifespan(attributes);
205
+
206
+ // Create the token
60
207
  const apiToken = await strapi.query('admin::api-token').create({
61
208
  select: SELECT_FIELDS,
209
+ populate: POPULATE_FIELDS,
62
210
  data: {
63
- ...attributes,
211
+ ...omit('permissions', attributes),
64
212
  accessKey: hash(accessKey),
213
+ ...getExpirationFields(attributes.lifespan),
65
214
  },
66
215
  });
67
216
 
217
+ const result = { ...apiToken, accessKey };
218
+
219
+ // If this is a custom type token, create and the related permissions
220
+ if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM) {
221
+ // TODO: createMany doesn't seem to create relation properly, implement a better way rather than a ton of queries
222
+ // const permissionsCount = await strapi.query('admin::api-token-permission').createMany({
223
+ // populate: POPULATE_FIELDS,
224
+ // data: attributes.permissions.map(action => ({ action, token: apiToken })),
225
+ // });
226
+ await Promise.all(
227
+ uniq(attributes.permissions).map((action) =>
228
+ strapi.query('admin::api-token-permission').create({
229
+ data: { action, token: apiToken },
230
+ })
231
+ )
232
+ );
233
+
234
+ const currentPermissions = await strapi.entityService.load(
235
+ 'admin::api-token',
236
+ apiToken,
237
+ 'permissions'
238
+ );
239
+
240
+ if (currentPermissions) {
241
+ Object.assign(result, { permissions: map('action', currentPermissions) });
242
+ }
243
+ }
244
+
245
+ return result;
246
+ };
247
+
248
+ /**
249
+ * @param {string|number} id
250
+ *
251
+ * @returns {Promise<ApiToken>}
252
+ */
253
+ const regenerate = async (id) => {
254
+ const accessKey = crypto.randomBytes(128).toString('hex');
255
+
256
+ const apiToken = await strapi.query('admin::api-token').update({
257
+ select: ['id', 'accessKey'],
258
+ where: { id },
259
+ data: {
260
+ accessKey: hash(accessKey),
261
+ },
262
+ });
263
+
264
+ if (!apiToken) {
265
+ throw new NotFoundError('The provided token id does not exist');
266
+ }
267
+
68
268
  return {
69
269
  ...apiToken,
70
270
  accessKey,
@@ -92,25 +292,37 @@ For security reasons, prefer storing the secret in an environment variable and r
92
292
  };
93
293
 
94
294
  /**
295
+ * Return a list of all tokens and their permissions
296
+ *
95
297
  * @returns {Promise<Omit<ApiToken, 'accessKey'>>}
96
298
  */
97
299
  const list = async () => {
98
- return strapi.query('admin::api-token').findMany({
300
+ const tokens = await strapi.query('admin::api-token').findMany({
99
301
  select: SELECT_FIELDS,
302
+ populate: POPULATE_FIELDS,
100
303
  orderBy: { name: 'ASC' },
101
304
  });
305
+
306
+ if (!tokens) return tokens;
307
+ return tokens.map((token) => flattenTokenPermissions(token));
102
308
  };
103
309
 
104
310
  /**
311
+ * Revoke (delete) a token
312
+ *
105
313
  * @param {string|number} id
106
314
  *
107
315
  * @returns {Promise<Omit<ApiToken, 'accessKey'>>}
108
316
  */
109
317
  const revoke = async (id) => {
110
- return strapi.query('admin::api-token').delete({ select: SELECT_FIELDS, where: { id } });
318
+ return strapi
319
+ .query('admin::api-token')
320
+ .delete({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: { id } });
111
321
  };
112
322
 
113
323
  /**
324
+ * Retrieve a token by id
325
+ *
114
326
  * @param {string|number} id
115
327
  *
116
328
  * @returns {Promise<Omit<ApiToken, 'accessKey'>>}
@@ -120,6 +332,8 @@ const getById = async (id) => {
120
332
  };
121
333
 
122
334
  /**
335
+ * Retrieve a token by name
336
+ *
123
337
  * @param {string} name
124
338
  *
125
339
  * @returns {Promise<Omit<ApiToken, 'accessKey'>>}
@@ -129,39 +343,106 @@ const getByName = async (name) => {
129
343
  };
130
344
 
131
345
  /**
346
+ * Update a token and its permissions
347
+ *
132
348
  * @param {string|number} id
133
349
  * @param {Object} attributes
134
350
  * @param {TokenType} attributes.type
135
351
  * @param {string} attributes.name
136
- * @param {string} [attributes.description]
352
+ * @param {number} attributes.lastUsedAt
353
+ * @param {string[]} attributes.permissions
354
+ * @param {string} attributes.description
137
355
  *
138
356
  * @returns {Promise<Omit<ApiToken, 'accessKey'>>}
139
357
  */
140
358
  const update = async (id, attributes) => {
141
- return strapi
142
- .query('admin::api-token')
143
- .update({ where: { id }, data: attributes, select: SELECT_FIELDS });
144
- };
359
+ // retrieve token without permissions
360
+ const originalToken = await strapi.query('admin::api-token').findOne({ where: { id } });
145
361
 
146
- /**
147
- * @param {Object} whereParams
148
- * @param {string|number} [whereParams.id]
149
- * @param {string} [whereParams.name]
150
- * @param {string} [whereParams.description]
151
- * @param {string} [whereParams.accessKey]
152
- *
153
- * @returns {Promise<Omit<ApiToken, 'accessKey'> | null>}
154
- */
155
- const getBy = async (whereParams = {}) => {
156
- if (Object.keys(whereParams).length === 0) {
157
- return null;
362
+ if (!originalToken) {
363
+ throw new NotFoundError('Token not found');
158
364
  }
159
365
 
160
- return strapi.query('admin::api-token').findOne({ select: SELECT_FIELDS, where: whereParams });
366
+ const changingTypeToCustom =
367
+ attributes.type === constants.API_TOKEN_TYPE.CUSTOM &&
368
+ originalToken.type !== constants.API_TOKEN_TYPE.CUSTOM;
369
+
370
+ // if we're updating the permissions on any token type, or changing from non-custom to custom, ensure they're still valid
371
+ // if neither type nor permissions are changing, we don't need to validate again or else we can't allow partial update
372
+ if (attributes.permissions || changingTypeToCustom) {
373
+ assertCustomTokenPermissionsValidity({
374
+ ...originalToken,
375
+ ...attributes,
376
+ type: attributes.type || originalToken.type,
377
+ });
378
+ }
379
+
380
+ assertValidLifespan(attributes);
381
+
382
+ const updatedToken = await strapi.query('admin::api-token').update({
383
+ select: SELECT_FIELDS,
384
+ populate: POPULATE_FIELDS,
385
+ where: { id },
386
+ data: omit('permissions', attributes),
387
+ });
388
+
389
+ // custom tokens need to have their permissions updated as well
390
+ if (updatedToken.type === constants.API_TOKEN_TYPE.CUSTOM && attributes.permissions) {
391
+ const currentPermissionsResult = await strapi.entityService.load(
392
+ 'admin::api-token',
393
+ updatedToken,
394
+ 'permissions'
395
+ );
396
+
397
+ const currentPermissions = map('action', currentPermissionsResult || []);
398
+ const newPermissions = uniq(attributes.permissions);
399
+
400
+ const actionsToDelete = difference(currentPermissions, newPermissions);
401
+ const actionsToAdd = difference(newPermissions, currentPermissions);
402
+
403
+ // TODO: improve efficiency here
404
+ // method using a loop -- works but very inefficient
405
+ await Promise.all(
406
+ actionsToDelete.map((action) =>
407
+ strapi.query('admin::api-token-permission').delete({
408
+ where: { action, token: id },
409
+ })
410
+ )
411
+ );
412
+
413
+ // TODO: improve efficiency here
414
+ // using a loop -- works but very inefficient
415
+ await Promise.all(
416
+ actionsToAdd.map((action) =>
417
+ strapi.query('admin::api-token-permission').create({
418
+ data: { action, token: id },
419
+ })
420
+ )
421
+ );
422
+ }
423
+ // if type is not custom, make sure any old permissions get removed
424
+ else if (updatedToken.type !== constants.API_TOKEN_TYPE.CUSTOM) {
425
+ await strapi.query('admin::api-token-permission').delete({
426
+ where: { token: id },
427
+ });
428
+ }
429
+
430
+ // retrieve permissions
431
+ const permissionsFromDb = await strapi.entityService.load(
432
+ 'admin::api-token',
433
+ updatedToken,
434
+ 'permissions'
435
+ );
436
+
437
+ return {
438
+ ...updatedToken,
439
+ permissions: permissionsFromDb ? permissionsFromDb.map((p) => p.action) : undefined,
440
+ };
161
441
  };
162
442
 
163
443
  module.exports = {
164
444
  create,
445
+ regenerate,
165
446
  exists,
166
447
  checkSaltIsDefined,
167
448
  hash,
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const DAY_IN_MS = 24 * 60 * 60 * 1000;
4
+
3
5
  module.exports = {
4
6
  CONTENT_TYPE_SECTION: 'contentTypes',
5
7
  SUPER_ADMIN_CODE: 'strapi-super-admin',
@@ -13,5 +15,13 @@ module.exports = {
13
15
  API_TOKEN_TYPE: {
14
16
  READ_ONLY: 'read-only',
15
17
  FULL_ACCESS: 'full-access',
18
+ CUSTOM: 'custom',
19
+ },
20
+ // The front-end only displays these values
21
+ API_TOKEN_LIFESPANS: {
22
+ UNLIMITED: null,
23
+ DAYS_7: 7 * DAY_IN_MS,
24
+ DAYS_30: 30 * DAY_IN_MS,
25
+ DAYS_90: 90 * DAY_IN_MS,
16
26
  },
17
27
  };