directus 9.15.1 → 9.17.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 (107) hide show
  1. package/dist/__utils__/items-utils.d.ts +2 -0
  2. package/dist/__utils__/items-utils.js +36 -0
  3. package/dist/__utils__/schemas.d.ts +13 -0
  4. package/dist/__utils__/schemas.js +304 -0
  5. package/dist/__utils__/snapshots.d.ts +5 -0
  6. package/dist/__utils__/snapshots.js +897 -0
  7. package/dist/app.js +8 -7
  8. package/dist/auth/drivers/ldap.js +1 -0
  9. package/dist/auth/drivers/local.js +6 -0
  10. package/dist/auth/drivers/oauth2.js +1 -0
  11. package/dist/auth/drivers/openid.js +1 -0
  12. package/dist/cli/index.test.d.ts +1 -0
  13. package/dist/cli/index.test.js +58 -0
  14. package/dist/cli/utils/create-env/env-stub.liquid +6 -2
  15. package/dist/controllers/activity.js +1 -0
  16. package/dist/controllers/assets.js +20 -16
  17. package/dist/controllers/auth.js +6 -9
  18. package/dist/controllers/files.test.d.ts +1 -0
  19. package/dist/controllers/files.test.js +49 -0
  20. package/dist/controllers/server.js +0 -1
  21. package/dist/database/migrations/20220826A-add-origin-to-accountability.d.ts +3 -0
  22. package/dist/database/migrations/20220826A-add-origin-to-accountability.js +21 -0
  23. package/dist/database/migrations/run.test.d.ts +1 -0
  24. package/dist/database/migrations/run.test.js +92 -0
  25. package/dist/database/system-data/fields/activity.yaml +6 -0
  26. package/dist/database/system-data/fields/sessions.yaml +2 -0
  27. package/dist/env.js +15 -0
  28. package/dist/env.test.d.ts +8 -0
  29. package/dist/env.test.js +39 -0
  30. package/dist/extensions.d.ts +1 -0
  31. package/dist/extensions.js +16 -3
  32. package/dist/flows.js +28 -17
  33. package/dist/mailer.js +1 -0
  34. package/dist/middleware/authenticate.d.ts +1 -1
  35. package/dist/middleware/authenticate.js +1 -0
  36. package/dist/middleware/authenticate.test.d.ts +1 -0
  37. package/dist/middleware/authenticate.test.js +214 -0
  38. package/dist/middleware/extract-token.test.d.ts +1 -0
  39. package/dist/middleware/extract-token.test.js +60 -0
  40. package/dist/middleware/validate-batch.d.ts +1 -2
  41. package/dist/middleware/validate-batch.js +10 -13
  42. package/dist/middleware/validate-batch.test.d.ts +1 -0
  43. package/dist/middleware/validate-batch.test.js +82 -0
  44. package/dist/operations/exec/index.d.ts +5 -0
  45. package/dist/operations/exec/index.js +26 -0
  46. package/dist/operations/exec/index.test.d.ts +1 -0
  47. package/dist/operations/exec/index.test.js +95 -0
  48. package/dist/operations/notification/index.js +9 -6
  49. package/dist/operations/request/index.js +22 -3
  50. package/dist/operations/transform/index.d.ts +1 -1
  51. package/dist/operations/transform/index.js +1 -1
  52. package/dist/services/authentication.js +13 -3
  53. package/dist/services/files.js +3 -2
  54. package/dist/services/files.test.d.ts +1 -0
  55. package/dist/services/files.test.js +53 -0
  56. package/dist/services/flows.js +4 -0
  57. package/dist/services/graphql/index.d.ts +2 -2
  58. package/dist/services/graphql/index.js +78 -75
  59. package/dist/services/items.js +98 -42
  60. package/dist/services/items.test.d.ts +1 -0
  61. package/dist/services/items.test.js +765 -0
  62. package/dist/services/payload.d.ts +7 -4
  63. package/dist/services/payload.js +63 -12
  64. package/dist/services/payload.test.d.ts +1 -0
  65. package/dist/services/payload.test.js +94 -0
  66. package/dist/services/server.js +10 -7
  67. package/dist/services/shares.js +2 -1
  68. package/dist/services/specifications.test.d.ts +1 -0
  69. package/dist/services/specifications.test.js +96 -0
  70. package/dist/types/items.d.ts +11 -0
  71. package/dist/utils/apply-query.js +7 -3
  72. package/dist/utils/apply-snapshot.js +15 -0
  73. package/dist/utils/apply-snapshot.test.d.ts +1 -0
  74. package/dist/utils/apply-snapshot.test.js +305 -0
  75. package/dist/utils/async-handler.d.ts +2 -6
  76. package/dist/utils/async-handler.js +1 -13
  77. package/dist/utils/async-handler.test.d.ts +1 -0
  78. package/dist/utils/async-handler.test.js +18 -0
  79. package/dist/utils/calculate-field-depth.test.d.ts +1 -0
  80. package/dist/utils/calculate-field-depth.test.js +76 -0
  81. package/dist/utils/filter-items.test.d.ts +1 -0
  82. package/dist/utils/filter-items.test.js +60 -0
  83. package/dist/utils/get-cache-key.test.d.ts +1 -0
  84. package/dist/utils/get-cache-key.test.js +53 -0
  85. package/dist/utils/get-column-path.test.d.ts +1 -0
  86. package/dist/utils/get-column-path.test.js +136 -0
  87. package/dist/utils/get-config-from-env.test.d.ts +1 -0
  88. package/dist/utils/get-config-from-env.test.js +19 -0
  89. package/dist/utils/get-graphql-type.d.ts +1 -1
  90. package/dist/utils/get-graphql-type.js +4 -1
  91. package/dist/utils/get-os-info.d.ts +9 -0
  92. package/dist/utils/get-os-info.js +47 -0
  93. package/dist/utils/get-relation-info.test.d.ts +1 -0
  94. package/dist/utils/get-relation-info.test.js +88 -0
  95. package/dist/utils/get-relation-type.test.d.ts +1 -0
  96. package/dist/utils/get-relation-type.test.js +69 -0
  97. package/dist/utils/get-string-byte-size.test.d.ts +1 -0
  98. package/dist/utils/get-string-byte-size.test.js +8 -0
  99. package/dist/utils/is-directus-jwt.test.d.ts +1 -0
  100. package/dist/utils/is-directus-jwt.test.js +26 -0
  101. package/dist/utils/jwt.test.d.ts +1 -0
  102. package/dist/utils/jwt.test.js +36 -0
  103. package/dist/utils/merge-permissions.test.d.ts +1 -0
  104. package/dist/utils/merge-permissions.test.js +80 -0
  105. package/dist/utils/validate-keys.test.d.ts +1 -0
  106. package/dist/utils/validate-keys.test.js +97 -0
  107. package/package.json +14 -12
@@ -78,19 +78,27 @@ class ExtensionManager {
78
78
  this.reloadQueue = new job_queue_1.JobQueue();
79
79
  }
80
80
  async initialize(options = {}) {
81
+ const prevOptions = this.options;
81
82
  this.options = {
82
83
  ...defaultOptions,
83
84
  ...options,
84
85
  };
85
- this.initializeWatcher();
86
+ if (!prevOptions.watch && this.options.watch) {
87
+ this.initializeWatcher();
88
+ }
89
+ else if (prevOptions.watch && !this.options.watch) {
90
+ await this.closeWatcher();
91
+ }
86
92
  if (!this.isLoaded) {
87
93
  await this.load();
88
- this.updateWatchedExtensions(this.extensions);
89
94
  const loadedExtensions = this.getExtensionsList();
90
95
  if (loadedExtensions.length > 0) {
91
96
  logger_1.default.info(`Loaded extensions: ${loadedExtensions.join(', ')}`);
92
97
  }
93
98
  }
99
+ if (!prevOptions.watch && this.options.watch) {
100
+ this.updateWatchedExtensions(this.extensions);
101
+ }
94
102
  }
95
103
  reload() {
96
104
  this.reloadQueue.enqueue(async () => {
@@ -158,7 +166,7 @@ class ExtensionManager {
158
166
  this.isLoaded = false;
159
167
  }
160
168
  initializeWatcher() {
161
- if (this.options.watch && !this.watcher) {
169
+ if (!this.watcher) {
162
170
  logger_1.default.info('Watching extensions for changes...');
163
171
  const localExtensionPaths = (env_1.default.SERVE_APP ? constants_1.EXTENSION_TYPES : constants_1.API_OR_HYBRID_EXTENSION_TYPES).flatMap((type) => {
164
172
  const typeDir = path_1.default.posix.join(path_1.default.relative('.', env_1.default.EXTENSIONS_PATH).split(path_1.default.sep).join(path_1.default.posix.sep), (0, utils_1.pluralize)(type));
@@ -175,6 +183,11 @@ class ExtensionManager {
175
183
  .on('unlink', () => this.reload());
176
184
  }
177
185
  }
186
+ async closeWatcher() {
187
+ if (this.watcher) {
188
+ await this.watcher.close();
189
+ }
190
+ }
178
191
  updateWatchedExtensions(added, removed = []) {
179
192
  if (this.watcher) {
180
193
  const toPackageExtensionPaths = (extensions) => extensions
package/dist/flows.js CHANGED
@@ -119,28 +119,33 @@ class FlowManager {
119
119
  return handler(data, context);
120
120
  }
121
121
  async load() {
122
- var _a, _b, _c, _d, _e, _f, _g;
122
+ var _a, _b, _c, _d;
123
123
  const flowsService = new services_1.FlowsService({ knex: (0, database_1.default)(), schema: await (0, get_schema_1.getSchema)() });
124
124
  const flows = await flowsService.readByQuery({
125
125
  filter: { status: { _eq: 'active' } },
126
126
  fields: ['*', 'operations.*'],
127
+ limit: -1,
127
128
  });
128
129
  const flowTrees = flows.map((flow) => (0, construct_flow_tree_1.constructFlowTree)(flow));
129
130
  for (const flow of flowTrees) {
130
131
  if (flow.trigger === 'event') {
131
- const events = (_d = (_c = (_b = (_a = flow.options) === null || _a === void 0 ? void 0 : _a.scope) === null || _b === void 0 ? void 0 : _b.map((scope) => {
132
- var _a, _b, _c;
133
- if (['items.create', 'items.update', 'items.delete'].includes(scope)) {
134
- return ((_c = (_b = (_a = flow.options) === null || _a === void 0 ? void 0 : _a.collections) === null || _b === void 0 ? void 0 : _b.map((collection) => {
135
- if (collection.startsWith('directus_')) {
136
- const action = scope.split('.')[1];
137
- return collection.substring(9) + '.' + action;
138
- }
139
- return `${collection}.${scope}`;
140
- })) !== null && _c !== void 0 ? _c : []);
141
- }
142
- return scope;
143
- })) === null || _c === void 0 ? void 0 : _c.flat()) !== null && _d !== void 0 ? _d : [];
132
+ const events = ((_a = flow.options) === null || _a === void 0 ? void 0 : _a.scope)
133
+ ? (0, utils_1.toArray)(flow.options.scope)
134
+ .map((scope) => {
135
+ var _a, _b, _c;
136
+ if (['items.create', 'items.update', 'items.delete'].includes(scope)) {
137
+ return ((_c = (_b = (_a = flow.options) === null || _a === void 0 ? void 0 : _a.collections) === null || _b === void 0 ? void 0 : _b.map((collection) => {
138
+ if (collection.startsWith('directus_')) {
139
+ const action = scope.split('.')[1];
140
+ return collection.substring(9) + '.' + action;
141
+ }
142
+ return `${collection}.${scope}`;
143
+ })) !== null && _c !== void 0 ? _c : []);
144
+ }
145
+ return scope;
146
+ })
147
+ .flat()
148
+ : [];
144
149
  if (flow.options.type === 'filter') {
145
150
  const handler = (payload, meta, context) => this.executeFlow(flow, { payload, ...meta }, {
146
151
  accountability: context.accountability,
@@ -195,9 +200,9 @@ class FlowManager {
195
200
  return this.executeFlow(flow, data, context);
196
201
  }
197
202
  };
198
- const method = (_f = (_e = flow.options) === null || _e === void 0 ? void 0 : _e.method) !== null && _f !== void 0 ? _f : 'GET';
203
+ const method = (_c = (_b = flow.options) === null || _b === void 0 ? void 0 : _b.method) !== null && _c !== void 0 ? _c : 'GET';
199
204
  // Default return to $last for webhooks
200
- flow.options.return = (_g = flow.options.return) !== null && _g !== void 0 ? _g : '$last';
205
+ flow.options.return = (_d = flow.options.return) !== null && _d !== void 0 ? _d : '$last';
201
206
  this.webhookFlowHandlers[`${method}-${flow.id}`] = handler;
202
207
  }
203
208
  else if (flow.trigger === 'manual') {
@@ -253,7 +258,7 @@ class FlowManager {
253
258
  this.isLoaded = false;
254
259
  }
255
260
  async executeFlow(flow, data = null, context = {}) {
256
- var _a, _b, _c, _d, _e, _f;
261
+ var _a, _b, _c, _d, _e, _f, _g;
257
262
  const database = (_a = context.database) !== null && _a !== void 0 ? _a : (0, database_1.default)();
258
263
  const schema = (_b = context.schema) !== null && _b !== void 0 ? _b : (await (0, get_schema_1.getSchema)({ database }));
259
264
  const keyedData = {
@@ -262,11 +267,13 @@ class FlowManager {
262
267
  [ACCOUNTABILITY_KEY]: (_c = context === null || context === void 0 ? void 0 : context.accountability) !== null && _c !== void 0 ? _c : null,
263
268
  };
264
269
  let nextOperation = flow.operation;
270
+ let lastOperationStatus = 'unknown';
265
271
  const steps = [];
266
272
  while (nextOperation !== null) {
267
273
  const { successor, data, status, options } = await this.executeOperation(nextOperation, keyedData, context);
268
274
  keyedData[nextOperation.key] = data;
269
275
  keyedData[LAST_KEY] = data;
276
+ lastOperationStatus = status;
270
277
  steps.push({ operation: nextOperation.id, key: nextOperation.key, status, options });
271
278
  nextOperation = successor;
272
279
  }
@@ -282,6 +289,7 @@ class FlowManager {
282
289
  collection: 'directus_flows',
283
290
  ip: (_e = accountability === null || accountability === void 0 ? void 0 : accountability.ip) !== null && _e !== void 0 ? _e : null,
284
291
  user_agent: (_f = accountability === null || accountability === void 0 ? void 0 : accountability.userAgent) !== null && _f !== void 0 ? _f : null,
292
+ origin: (_g = accountability === null || accountability === void 0 ? void 0 : accountability.origin) !== null && _g !== void 0 ? _g : null,
285
293
  item: flow.id,
286
294
  });
287
295
  if (flow.accountability === 'all') {
@@ -300,6 +308,9 @@ class FlowManager {
300
308
  });
301
309
  }
302
310
  }
311
+ if (flow.trigger === 'event' && flow.options.type === 'filter' && lastOperationStatus === 'reject') {
312
+ throw keyedData[LAST_KEY];
313
+ }
303
314
  if (flow.options.return === '$all') {
304
315
  return keyedData;
305
316
  }
package/dist/mailer.js CHANGED
@@ -37,6 +37,7 @@ function getMailer() {
37
37
  }
38
38
  const tls = (0, get_config_from_env_1.getConfigFromEnv)('EMAIL_SMTP_TLS_');
39
39
  transporter = nodemailer_1.default.createTransport({
40
+ name: env_1.default.EMAIL_SMTP_NAME,
40
41
  pool: env_1.default.EMAIL_SMTP_POOL,
41
42
  host: env_1.default.EMAIL_SMTP_HOST,
42
43
  port: env_1.default.EMAIL_SMTP_PORT,
@@ -3,5 +3,5 @@ import { NextFunction, Request, Response } from 'express';
3
3
  * Verify the passed JWT and assign the user ID and role to `req`
4
4
  */
5
5
  export declare const handler: (req: Request, res: Response, next: NextFunction) => Promise<void>;
6
- declare const _default: import("express").RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
6
+ declare const _default: (req: Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: NextFunction) => Promise<void>;
7
7
  export default _default;
@@ -24,6 +24,7 @@ const handler = async (req, res, next) => {
24
24
  app: false,
25
25
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
26
26
  userAgent: req.get('user-agent'),
27
+ origin: req.get('origin'),
27
28
  };
28
29
  const database = (0, database_1.default)();
29
30
  const customAccountability = await emitter_1.default.emitFilter('authenticate', defaultAccountability, {
@@ -0,0 +1 @@
1
+ import '../../src/types/express.d.ts';
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ // @ts-nocheck
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8
+ const database_1 = __importDefault(require("../database"));
9
+ const emitter_1 = __importDefault(require("../emitter"));
10
+ const env_1 = __importDefault(require("../env"));
11
+ const exceptions_1 = require("../exceptions");
12
+ const authenticate_1 = require("./authenticate");
13
+ require("../../src/types/express.d.ts");
14
+ jest.mock('../../src/database');
15
+ jest.mock('../../src/env', () => ({
16
+ SECRET: 'test',
17
+ }));
18
+ afterEach(() => {
19
+ jest.resetAllMocks();
20
+ });
21
+ test('Short-circuits when authenticate filter is used', async () => {
22
+ const req = {
23
+ ip: '127.0.0.1',
24
+ get: jest.fn(),
25
+ };
26
+ const res = {};
27
+ const next = jest.fn();
28
+ const customAccountability = { admin: true };
29
+ jest.spyOn(emitter_1.default, 'emitFilter').mockResolvedValue(customAccountability);
30
+ await (0, authenticate_1.handler)(req, res, next);
31
+ expect(req.accountability).toEqual(customAccountability);
32
+ expect(next).toHaveBeenCalledTimes(1);
33
+ });
34
+ test('Uses default public accountability when no token is given', async () => {
35
+ const req = {
36
+ ip: '127.0.0.1',
37
+ get: jest.fn((string) => {
38
+ switch (string) {
39
+ case 'user-agent':
40
+ return 'fake-user-agent';
41
+ case 'origin':
42
+ return 'fake-origin';
43
+ default:
44
+ return null;
45
+ }
46
+ }),
47
+ };
48
+ const res = {};
49
+ const next = jest.fn();
50
+ jest.spyOn(emitter_1.default, 'emitFilter').mockImplementation((_, payload) => payload);
51
+ await (0, authenticate_1.handler)(req, res, next);
52
+ expect(req.accountability).toEqual({
53
+ user: null,
54
+ role: null,
55
+ admin: false,
56
+ app: false,
57
+ ip: '127.0.0.1',
58
+ userAgent: 'fake-user-agent',
59
+ origin: 'fake-origin',
60
+ });
61
+ expect(next).toHaveBeenCalledTimes(1);
62
+ });
63
+ test('Sets accountability to payload contents if valid token is passed', async () => {
64
+ const userID = '3fac3c02-607f-4438-8d6e-6b8b25109b52';
65
+ const roleID = '38269fc6-6eb6-475a-93cb-479d97f73039';
66
+ const share = 'ca0ad005-f4ad-4bfe-b428-419ee8784790';
67
+ const shareScope = {
68
+ collection: 'articles',
69
+ item: 15,
70
+ };
71
+ const appAccess = true;
72
+ const adminAccess = false;
73
+ const token = jsonwebtoken_1.default.sign({
74
+ id: userID,
75
+ role: roleID,
76
+ app_access: appAccess,
77
+ admin_access: adminAccess,
78
+ share,
79
+ share_scope: shareScope,
80
+ }, env_1.default.SECRET, { issuer: 'directus' });
81
+ const req = {
82
+ ip: '127.0.0.1',
83
+ get: jest.fn((string) => {
84
+ switch (string) {
85
+ case 'user-agent':
86
+ return 'fake-user-agent';
87
+ case 'origin':
88
+ return 'fake-origin';
89
+ default:
90
+ return null;
91
+ }
92
+ }),
93
+ token,
94
+ };
95
+ const res = {};
96
+ const next = jest.fn();
97
+ await (0, authenticate_1.handler)(req, res, next);
98
+ expect(req.accountability).toEqual({
99
+ user: userID,
100
+ role: roleID,
101
+ app: appAccess,
102
+ admin: adminAccess,
103
+ share,
104
+ share_scope: shareScope,
105
+ ip: '127.0.0.1',
106
+ userAgent: 'fake-user-agent',
107
+ origin: 'fake-origin',
108
+ });
109
+ expect(next).toHaveBeenCalledTimes(1);
110
+ // Test with 1/0 instead or true/false
111
+ next.mockClear();
112
+ req.token = jsonwebtoken_1.default.sign({
113
+ id: userID,
114
+ role: roleID,
115
+ app_access: 1,
116
+ admin_access: 0,
117
+ share,
118
+ share_scope: shareScope,
119
+ }, env_1.default.SECRET, { issuer: 'directus' });
120
+ await (0, authenticate_1.handler)(req, res, next);
121
+ expect(req.accountability).toEqual({
122
+ user: userID,
123
+ role: roleID,
124
+ app: appAccess,
125
+ admin: adminAccess,
126
+ share,
127
+ share_scope: shareScope,
128
+ ip: '127.0.0.1',
129
+ userAgent: 'fake-user-agent',
130
+ origin: 'fake-origin',
131
+ });
132
+ expect(next).toHaveBeenCalledTimes(1);
133
+ });
134
+ test('Throws InvalidCredentialsException when static token is used, but user does not exist', async () => {
135
+ jest.mocked(database_1.default).mockReturnValue({
136
+ select: jest.fn().mockReturnThis(),
137
+ from: jest.fn().mockReturnThis(),
138
+ leftJoin: jest.fn().mockReturnThis(),
139
+ where: jest.fn().mockReturnThis(),
140
+ first: jest.fn().mockResolvedValue(undefined),
141
+ });
142
+ const req = {
143
+ ip: '127.0.0.1',
144
+ get: jest.fn((string) => {
145
+ switch (string) {
146
+ case 'user-agent':
147
+ return 'fake-user-agent';
148
+ case 'origin':
149
+ return 'fake-origin';
150
+ default:
151
+ return null;
152
+ }
153
+ }),
154
+ token: 'static-token',
155
+ };
156
+ const res = {};
157
+ const next = jest.fn();
158
+ expect((0, authenticate_1.handler)(req, res, next)).rejects.toEqual(new exceptions_1.InvalidCredentialsException());
159
+ expect(next).toHaveBeenCalledTimes(0);
160
+ });
161
+ test('Sets accountability to user information when static token is used', async () => {
162
+ const req = {
163
+ ip: '127.0.0.1',
164
+ get: jest.fn((string) => {
165
+ switch (string) {
166
+ case 'user-agent':
167
+ return 'fake-user-agent';
168
+ case 'origin':
169
+ return 'fake-origin';
170
+ default:
171
+ return null;
172
+ }
173
+ }),
174
+ token: 'static-token',
175
+ };
176
+ const res = {};
177
+ const next = jest.fn();
178
+ const testUser = { id: 'test-id', role: 'test-role', admin_access: true, app_access: false };
179
+ const expectedAccountability = {
180
+ user: testUser.id,
181
+ role: testUser.role,
182
+ app: testUser.app_access,
183
+ admin: testUser.admin_access,
184
+ ip: '127.0.0.1',
185
+ userAgent: 'fake-user-agent',
186
+ origin: 'fake-origin',
187
+ };
188
+ jest.mocked(database_1.default).mockReturnValue({
189
+ select: jest.fn().mockReturnThis(),
190
+ from: jest.fn().mockReturnThis(),
191
+ leftJoin: jest.fn().mockReturnThis(),
192
+ where: jest.fn().mockReturnThis(),
193
+ first: jest.fn().mockResolvedValue(testUser),
194
+ });
195
+ await (0, authenticate_1.handler)(req, res, next);
196
+ expect(req.accountability).toEqual(expectedAccountability);
197
+ expect(next).toHaveBeenCalledTimes(1);
198
+ // Test for 0 / 1 instead of false / true
199
+ next.mockClear();
200
+ testUser.admin_access = 1;
201
+ testUser.app_access = 0;
202
+ await (0, authenticate_1.handler)(req, res, next);
203
+ expect(req.accountability).toEqual(expectedAccountability);
204
+ expect(next).toHaveBeenCalledTimes(1);
205
+ // Test for "1" / "0" instead of true / false
206
+ next.mockClear();
207
+ testUser.admin_access = '0';
208
+ testUser.app_access = '1';
209
+ expectedAccountability.admin = false;
210
+ expectedAccountability.app = true;
211
+ await (0, authenticate_1.handler)(req, res, next);
212
+ expect(req.accountability).toEqual(expectedAccountability);
213
+ expect(next).toHaveBeenCalledTimes(1);
214
+ });
@@ -0,0 +1 @@
1
+ import '../../src/types/express.d.ts';
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const extract_token_1 = __importDefault(require("../../src/middleware/extract-token"));
7
+ require("../../src/types/express.d.ts");
8
+ let mockRequest;
9
+ let mockResponse;
10
+ const nextFunction = jest.fn();
11
+ beforeEach(() => {
12
+ mockRequest = {};
13
+ mockResponse = {};
14
+ jest.clearAllMocks();
15
+ });
16
+ test('Token from query', () => {
17
+ mockRequest = {
18
+ query: {
19
+ access_token: 'test',
20
+ },
21
+ };
22
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
23
+ expect(mockRequest.token).toBe('test');
24
+ expect(nextFunction).toBeCalledTimes(1);
25
+ });
26
+ test('Token from Authorization header (capitalized)', () => {
27
+ mockRequest = {
28
+ headers: {
29
+ authorization: 'Bearer test',
30
+ },
31
+ };
32
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
33
+ expect(mockRequest.token).toBe('test');
34
+ expect(nextFunction).toBeCalledTimes(1);
35
+ });
36
+ test('Token from Authorization header (lowercase)', () => {
37
+ mockRequest = {
38
+ headers: {
39
+ authorization: 'bearer test',
40
+ },
41
+ };
42
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
43
+ expect(mockRequest.token).toBe('test');
44
+ expect(nextFunction).toBeCalledTimes(1);
45
+ });
46
+ test('Ignore the token if authorization header is too many parts', () => {
47
+ mockRequest = {
48
+ headers: {
49
+ authorization: 'bearer test what another one',
50
+ },
51
+ };
52
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
53
+ expect(mockRequest.token).toBeNull();
54
+ expect(nextFunction).toBeCalledTimes(1);
55
+ });
56
+ test('Null if no token passed', () => {
57
+ (0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
58
+ expect(mockRequest.token).toBeNull();
59
+ expect(nextFunction).toBeCalledTimes(1);
60
+ });
@@ -1,2 +1 @@
1
- import { RequestHandler } from 'express';
2
- export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => RequestHandler;
1
+ export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => (req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express").Response<any, Record<string, any>>, next: import("express").NextFunction) => Promise<void>;
@@ -14,39 +14,36 @@ const validateBatch = (scope) => (0, async_handler_1.default)(async (req, res, n
14
14
  req.body = {};
15
15
  return next();
16
16
  }
17
+ if (req.method.toLowerCase() !== 'search' && scope !== 'read' && req.singleton) {
18
+ return next();
19
+ }
17
20
  if (!req.body)
18
21
  throw new exceptions_1.InvalidPayloadException('Payload in body is required');
19
- if (req.singleton)
22
+ if (['update', 'delete'].includes(scope) && Array.isArray(req.body)) {
20
23
  return next();
24
+ }
25
+ // In reads, the query in the body should override the query params for searching
26
+ if (scope === 'read' && req.body.query) {
27
+ req.sanitizedQuery = (0, sanitize_query_1.sanitizeQuery)(req.body.query, req.accountability);
28
+ }
21
29
  // Every cRUD action has either keys or query
22
30
  let batchSchema = joi_1.default.object().keys({
23
31
  keys: joi_1.default.array().items(joi_1.default.alternatives(joi_1.default.string(), joi_1.default.number())),
24
32
  query: joi_1.default.object().unknown(),
25
33
  });
26
- // In reads, you can't combine the two, and 1 of the two at least is required
27
- if (scope !== 'read') {
34
+ if (['update', 'delete'].includes(scope)) {
28
35
  batchSchema = batchSchema.xor('query', 'keys');
29
36
  }
30
37
  // In updates, we add a required `data` that holds the update payload if an array isn't used
31
38
  if (scope === 'update') {
32
- if (Array.isArray(req.body))
33
- return next();
34
39
  batchSchema = batchSchema.keys({
35
40
  data: joi_1.default.object().unknown().required(),
36
41
  });
37
42
  }
38
- // In deletes, we want to keep supporting an array of just primary keys
39
- if (scope === 'delete' && Array.isArray(req.body)) {
40
- return next();
41
- }
42
43
  const { error } = batchSchema.validate(req.body);
43
44
  if (error) {
44
45
  throw new exceptions_2.FailedValidationException(error.details[0]);
45
46
  }
46
- // In reads, the query in the body should override the query params for searching
47
- if (scope === 'read' && req.body.query) {
48
- req.sanitizedQuery = (0, sanitize_query_1.sanitizeQuery)(req.body.query, req.accountability);
49
- }
50
47
  return next();
51
48
  });
52
49
  exports.validateBatch = validateBatch;
@@ -0,0 +1 @@
1
+ import '../../src/types/express.d.ts';
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const validate_batch_1 = require("./validate-batch");
4
+ require("../../src/types/express.d.ts");
5
+ const exceptions_1 = require("../exceptions");
6
+ const exceptions_2 = require("@directus/shared/exceptions");
7
+ let mockRequest;
8
+ let mockResponse;
9
+ const nextFunction = jest.fn();
10
+ beforeEach(() => {
11
+ mockRequest = {};
12
+ mockResponse = {};
13
+ jest.clearAllMocks();
14
+ });
15
+ test('Sets body to empty, calls next on GET requests', async () => {
16
+ mockRequest.method = 'GET';
17
+ await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
18
+ expect(mockRequest.body).toEqual({});
19
+ expect(nextFunction).toHaveBeenCalledTimes(1);
20
+ });
21
+ test(`Short circuits on singletons that aren't queried through SEARCH`, async () => {
22
+ mockRequest.method = 'PATCH';
23
+ mockRequest.singleton = true;
24
+ mockRequest.body = { title: 'test' };
25
+ await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
26
+ expect(nextFunction).toHaveBeenCalledTimes(1);
27
+ });
28
+ test('Throws InvalidPayloadException on missing body', async () => {
29
+ mockRequest.method = 'SEARCH';
30
+ await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
31
+ expect(nextFunction).toHaveBeenCalledTimes(1);
32
+ expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_1.InvalidPayloadException);
33
+ });
34
+ test(`Short circuits on Array body in update/delete use`, async () => {
35
+ mockRequest.method = 'PATCH';
36
+ mockRequest.body = [1, 2, 3];
37
+ await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
38
+ expect(mockRequest.sanitizedQuery).toBe(undefined);
39
+ expect(nextFunction).toHaveBeenCalled();
40
+ });
41
+ test(`Sets sanitizedQuery based on body.query in read operations`, async () => {
42
+ mockRequest.method = 'SEARCH';
43
+ mockRequest.body = {
44
+ query: {
45
+ sort: 'id',
46
+ },
47
+ };
48
+ await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
49
+ expect(mockRequest.sanitizedQuery).toEqual({
50
+ sort: ['id'],
51
+ });
52
+ });
53
+ test(`Doesn't allow both query and keys in a batch delete`, async () => {
54
+ mockRequest.method = 'DELETE';
55
+ mockRequest.body = {
56
+ keys: [1, 2, 3],
57
+ query: { filter: {} },
58
+ };
59
+ await (0, validate_batch_1.validateBatch)('delete')(mockRequest, mockResponse, nextFunction);
60
+ expect(nextFunction).toHaveBeenCalledTimes(1);
61
+ expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_2.FailedValidationException);
62
+ });
63
+ test(`Requires 'data' on batch update`, async () => {
64
+ mockRequest.method = 'PATCH';
65
+ mockRequest.body = {
66
+ keys: [1, 2, 3],
67
+ query: { filter: {} },
68
+ };
69
+ await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
70
+ expect(nextFunction).toHaveBeenCalledTimes(1);
71
+ expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_2.FailedValidationException);
72
+ });
73
+ test(`Calls next when all is well`, async () => {
74
+ mockRequest.method = 'PATCH';
75
+ mockRequest.body = {
76
+ query: { filter: {} },
77
+ data: {},
78
+ };
79
+ await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
80
+ expect(nextFunction).toHaveBeenCalledTimes(1);
81
+ expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeUndefined();
82
+ });
@@ -0,0 +1,5 @@
1
+ declare type Options = {
2
+ code: string;
3
+ };
4
+ declare const _default: import("@directus/shared/types").OperationApiConfig<Options>;
5
+ export default _default;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@directus/shared/utils");
4
+ const vm2_1 = require("vm2");
5
+ exports.default = (0, utils_1.defineOperationApi)({
6
+ id: 'exec',
7
+ handler: async ({ code }, { data, env }) => {
8
+ const allowedModules = env.FLOWS_EXEC_ALLOWED_MODULES ? (0, utils_1.toArray)(env.FLOWS_EXEC_ALLOWED_MODULES) : [];
9
+ const opts = {
10
+ eval: false,
11
+ wasm: false,
12
+ };
13
+ if (allowedModules.length > 0) {
14
+ opts.require = {
15
+ external: {
16
+ modules: allowedModules,
17
+ transitive: false,
18
+ },
19
+ };
20
+ }
21
+ const vm = new vm2_1.NodeVM(opts);
22
+ const script = new vm2_1.VMScript(code).compile();
23
+ const fn = await vm.run(script);
24
+ return await fn(data);
25
+ },
26
+ });
@@ -0,0 +1 @@
1
+ export {};