directus 9.16.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 (39) hide show
  1. package/dist/app.js +7 -7
  2. package/dist/auth/drivers/ldap.js +1 -0
  3. package/dist/auth/drivers/local.js +6 -0
  4. package/dist/auth/drivers/oauth2.js +1 -0
  5. package/dist/auth/drivers/openid.js +1 -0
  6. package/dist/cli/utils/create-env/env-stub.liquid +4 -0
  7. package/dist/controllers/activity.js +1 -0
  8. package/dist/controllers/auth.js +6 -9
  9. package/dist/database/migrations/20220826A-add-origin-to-accountability.d.ts +3 -0
  10. package/dist/database/migrations/20220826A-add-origin-to-accountability.js +21 -0
  11. package/dist/database/system-data/fields/activity.yaml +6 -0
  12. package/dist/database/system-data/fields/sessions.yaml +2 -0
  13. package/dist/env.js +7 -0
  14. package/dist/extensions.d.ts +1 -0
  15. package/dist/extensions.js +16 -3
  16. package/dist/flows.js +27 -17
  17. package/dist/mailer.js +1 -0
  18. package/dist/middleware/authenticate.d.ts +1 -1
  19. package/dist/middleware/authenticate.js +1 -0
  20. package/dist/middleware/authenticate.test.js +44 -4
  21. package/dist/middleware/validate-batch.d.ts +1 -2
  22. package/dist/middleware/validate-batch.js +10 -13
  23. package/dist/middleware/validate-batch.test.d.ts +1 -0
  24. package/dist/middleware/validate-batch.test.js +82 -0
  25. package/dist/operations/request/index.js +2 -2
  26. package/dist/operations/transform/index.d.ts +1 -1
  27. package/dist/operations/transform/index.js +1 -1
  28. package/dist/services/authentication.js +13 -3
  29. package/dist/services/files.js +2 -2
  30. package/dist/services/graphql/index.js +45 -37
  31. package/dist/services/items.js +15 -3
  32. package/dist/services/payload.js +28 -4
  33. package/dist/services/server.js +5 -4
  34. package/dist/services/shares.js +2 -1
  35. package/dist/utils/async-handler.d.ts +2 -6
  36. package/dist/utils/async-handler.js +1 -13
  37. package/dist/utils/async-handler.test.d.ts +1 -0
  38. package/dist/utils/async-handler.test.js +18 -0
  39. package/package.json +12 -10
package/dist/app.js CHANGED
@@ -135,6 +135,13 @@ async function createApp() {
135
135
  await emitter_1.default.emitInit('app.before', { app });
136
136
  await emitter_1.default.emitInit('middlewares.before', { app });
137
137
  app.use(logger_1.expressLogger);
138
+ app.use((_req, res, next) => {
139
+ res.setHeader('X-Powered-By', 'Directus');
140
+ next();
141
+ });
142
+ if (env_1.default.CORS_ENABLED === true) {
143
+ app.use(cors_1.default);
144
+ }
138
145
  app.use((req, res, next) => {
139
146
  express_1.default.json({
140
147
  limit: env_1.default.MAX_PAYLOAD_SIZE,
@@ -147,13 +154,6 @@ async function createApp() {
147
154
  });
148
155
  app.use((0, cookie_parser_1.default)());
149
156
  app.use(extract_token_1.default);
150
- app.use((_req, res, next) => {
151
- res.setHeader('X-Powered-By', 'Directus');
152
- next();
153
- });
154
- if (env_1.default.CORS_ENABLED === true) {
155
- app.use(cors_1.default);
156
- }
157
157
  app.get('/', (_req, res, next) => {
158
158
  if (env_1.default.ROOT_REDIRECT) {
159
159
  res.redirect(env_1.default.ROOT_REDIRECT);
@@ -302,6 +302,7 @@ function createLDAPAuthRouter(provider) {
302
302
  const accountability = {
303
303
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
304
304
  userAgent: req.get('user-agent'),
305
+ origin: req.get('origin'),
305
306
  role: null,
306
307
  };
307
308
  const authenticationService = new services_1.AuthenticationService({
@@ -15,6 +15,8 @@ const env_1 = __importDefault(require("../../env"));
15
15
  const respond_1 = require("../../middleware/respond");
16
16
  const constants_1 = require("../../constants");
17
17
  const get_ip_from_req_1 = require("../../utils/get-ip-from-req");
18
+ const perf_hooks_1 = require("perf_hooks");
19
+ const stall_1 = require("../../utils/stall");
18
20
  class LocalAuthDriver extends auth_1.AuthDriver {
19
21
  async getUserID(payload) {
20
22
  if (!payload.email) {
@@ -50,9 +52,12 @@ function createLocalAuthRouter(provider) {
50
52
  }).unknown();
51
53
  router.post('/', (0, async_handler_1.default)(async (req, res, next) => {
52
54
  var _a;
55
+ const STALL_TIME = env_1.default.LOGIN_STALL_TIME;
56
+ const timeStart = perf_hooks_1.performance.now();
53
57
  const accountability = {
54
58
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
55
59
  userAgent: req.get('user-agent'),
60
+ origin: req.get('origin'),
56
61
  role: null,
57
62
  };
58
63
  const authenticationService = new services_1.AuthenticationService({
@@ -61,6 +66,7 @@ function createLocalAuthRouter(provider) {
61
66
  });
62
67
  const { error } = userLoginSchema.validate(req.body);
63
68
  if (error) {
69
+ await (0, stall_1.stall)(STALL_TIME, timeStart);
64
70
  throw new exceptions_1.InvalidPayloadException(error.message);
65
71
  }
66
72
  const mode = req.body.mode || 'json';
@@ -222,6 +222,7 @@ function createOAuth2AuthRouter(providerName) {
222
222
  accountability: {
223
223
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
224
224
  userAgent: req.get('user-agent'),
225
+ origin: req.get('origin'),
225
226
  role: null,
226
227
  },
227
228
  schema: req.schema,
@@ -241,6 +241,7 @@ function createOpenIDAuthRouter(providerName) {
241
241
  accountability: {
242
242
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
243
243
  userAgent: req.get('user-agent'),
244
+ origin: req.get('origin'),
244
245
  role: null,
245
246
  },
246
247
  schema: req.schema,
@@ -211,6 +211,10 @@ REFRESH_TOKEN_COOKIE_NAME="directus_refresh_token"
211
211
  # Which domain to use for the refresh cookie. Useful for development mode.
212
212
  # REFRESH_TOKEN_COOKIE_DOMAIN
213
213
 
214
+ # The duration in milliseconds that a login request will be stalled for,
215
+ # and it should be greater than the time taken for a login request with an invalid password [500]
216
+ # LOGIN_STALL_TIME=500
217
+
214
218
  # Whether or not to enable the CORS headers [false]
215
219
  CORS_ENABLED=true
216
220
 
@@ -75,6 +75,7 @@ router.post('/comment', (0, async_handler_1.default)(async (req, res, next) => {
75
75
  user: (_a = req.accountability) === null || _a === void 0 ? void 0 : _a.user,
76
76
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
77
77
  user_agent: req.get('user-agent'),
78
+ origin: req.get('origin'),
78
79
  });
79
80
  try {
80
81
  const record = await service.readOne(primaryKey, req.sanitizedQuery);
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = require("express");
7
- const ms_1 = __importDefault(require("ms"));
8
7
  const env_1 = __importDefault(require("../env"));
9
8
  const exceptions_1 = require("../exceptions");
10
9
  const respond_1 = require("../middleware/respond");
@@ -15,6 +14,7 @@ const logger_1 = __importDefault(require("../logger"));
15
14
  const drivers_1 = require("../auth/drivers");
16
15
  const constants_1 = require("../constants");
17
16
  const get_ip_from_req_1 = require("../utils/get-ip-from-req");
17
+ const constants_2 = require("../constants");
18
18
  const router = (0, express_1.Router)();
19
19
  const authProviders = (0, get_auth_providers_1.getAuthProviders)();
20
20
  for (const authProvider of authProviders) {
@@ -43,10 +43,10 @@ if (!env_1.default.AUTH_DISABLE_DEFAULT) {
43
43
  router.use('/login', (0, drivers_1.createLocalAuthRouter)(constants_1.DEFAULT_AUTH_PROVIDER));
44
44
  }
45
45
  router.post('/refresh', (0, async_handler_1.default)(async (req, res, next) => {
46
- var _a;
47
46
  const accountability = {
48
47
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
49
48
  userAgent: req.get('user-agent'),
49
+ origin: req.get('origin'),
50
50
  role: null,
51
51
  };
52
52
  const authenticationService = new services_1.AuthenticationService({
@@ -66,13 +66,7 @@ router.post('/refresh', (0, async_handler_1.default)(async (req, res, next) => {
66
66
  payload.data.refresh_token = refreshToken;
67
67
  }
68
68
  if (mode === 'cookie') {
69
- res.cookie(env_1.default.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
70
- httpOnly: true,
71
- domain: env_1.default.REFRESH_TOKEN_COOKIE_DOMAIN,
72
- maxAge: (0, ms_1.default)(env_1.default.REFRESH_TOKEN_TTL),
73
- secure: (_a = env_1.default.REFRESH_TOKEN_COOKIE_SECURE) !== null && _a !== void 0 ? _a : false,
74
- sameSite: env_1.default.REFRESH_TOKEN_COOKIE_SAME_SITE || 'strict',
75
- });
69
+ res.cookie(env_1.default.REFRESH_TOKEN_COOKIE_NAME, refreshToken, constants_2.COOKIE_OPTIONS);
76
70
  }
77
71
  res.locals.payload = payload;
78
72
  return next();
@@ -82,6 +76,7 @@ router.post('/logout', (0, async_handler_1.default)(async (req, res, next) => {
82
76
  const accountability = {
83
77
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
84
78
  userAgent: req.get('user-agent'),
79
+ origin: req.get('origin'),
85
80
  role: null,
86
81
  };
87
82
  const authenticationService = new services_1.AuthenticationService({
@@ -110,6 +105,7 @@ router.post('/password/request', (0, async_handler_1.default)(async (req, res, n
110
105
  const accountability = {
111
106
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
112
107
  userAgent: req.get('user-agent'),
108
+ origin: req.get('origin'),
113
109
  role: null,
114
110
  };
115
111
  const service = new services_1.UsersService({ accountability, schema: req.schema });
@@ -137,6 +133,7 @@ router.post('/password/reset', (0, async_handler_1.default)(async (req, res, nex
137
133
  const accountability = {
138
134
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
139
135
  userAgent: req.get('user-agent'),
136
+ origin: req.get('origin'),
140
137
  role: null,
141
138
  };
142
139
  const service = new services_1.UsersService({ accountability, schema: req.schema });
@@ -0,0 +1,3 @@
1
+ import { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.down = exports.up = void 0;
4
+ async function up(knex) {
5
+ await knex.schema.alterTable('directus_activity', (table) => {
6
+ table.string('origin').nullable();
7
+ });
8
+ await knex.schema.alterTable('directus_sessions', (table) => {
9
+ table.string('origin').nullable();
10
+ });
11
+ }
12
+ exports.up = up;
13
+ async function down(knex) {
14
+ await knex.schema.alterTable('directus_activity', (table) => {
15
+ table.dropColumn('origin');
16
+ });
17
+ await knex.schema.alterTable('directus_sessions', (table) => {
18
+ table.dropColumn('origin');
19
+ });
20
+ }
21
+ exports.down = down;
@@ -60,6 +60,12 @@ fields:
60
60
  font: monospace
61
61
  width: half
62
62
 
63
+ - field: origin
64
+ display: formatted-value
65
+ display_options:
66
+ font: monospace
67
+ width: half
68
+
63
69
  - field: ip
64
70
  display: formatted-value
65
71
  display_options:
@@ -11,4 +11,6 @@ fields:
11
11
  width: half
12
12
  - field: user_agent
13
13
  width: half
14
+ - field: origin
15
+ width: half
14
16
  - field: share
package/dist/env.js CHANGED
@@ -42,6 +42,7 @@ const allowedEnvironmentVars = [
42
42
  'REFRESH_TOKEN_COOKIE_SECURE',
43
43
  'REFRESH_TOKEN_COOKIE_SAME_SITE',
44
44
  'REFRESH_TOKEN_COOKIE_NAME',
45
+ 'LOGIN_STALL_TIME',
45
46
  'PASSWORD_RESET_URL_ALLOW_LIST',
46
47
  'USER_INVITE_URL_ALLOW_LIST',
47
48
  'IP_TRUST_PROXY',
@@ -79,6 +80,7 @@ const allowedEnvironmentVars = [
79
80
  'CACHE_REDIS_PASSWORD',
80
81
  'CACHE_MEMCACHE',
81
82
  'CACHE_VALUE_MAX_SIZE',
83
+ 'CACHE_HEALTHCHECK_THRESHOLD',
82
84
  // storage
83
85
  'STORAGE_LOCATIONS',
84
86
  'STORAGE_.+_DRIVER',
@@ -95,6 +97,7 @@ const allowedEnvironmentVars = [
95
97
  'STORAGE_.+_ENDPOINT',
96
98
  'STORAGE_.+_KEY_FILENAME',
97
99
  'STORAGE_.+_BUCKET',
100
+ 'STORAGE_.+_HEALTHCHECK_THRESHOLD',
98
101
  // metadata
99
102
  'FILE_METADATA_ALLOW_LIST',
100
103
  // assets
@@ -151,6 +154,7 @@ const allowedEnvironmentVars = [
151
154
  'EMAIL_VERIFY_SETUP',
152
155
  'EMAIL_SENDMAIL_NEW_LINE',
153
156
  'EMAIL_SENDMAIL_PATH',
157
+ 'EMAIL_SMTP_NAME',
154
158
  'EMAIL_SMTP_HOST',
155
159
  'EMAIL_SMTP_PORT',
156
160
  'EMAIL_SMTP_USER',
@@ -172,6 +176,8 @@ const allowedEnvironmentVars = [
172
176
  // limits & optimization
173
177
  'RELATIONAL_BATCH_SIZE',
174
178
  'EXPORT_BATCH_SIZE',
179
+ // flows
180
+ 'FLOWS_EXEC_ALLOWED_MODULES',
175
181
  ].map((name) => new RegExp(`^${name}$`));
176
182
  const acceptedEnvTypes = ['string', 'number', 'regex', 'array', 'json'];
177
183
  const defaults = {
@@ -194,6 +200,7 @@ const defaults = {
194
200
  REFRESH_TOKEN_COOKIE_SECURE: false,
195
201
  REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
196
202
  REFRESH_TOKEN_COOKIE_NAME: 'directus_refresh_token',
203
+ LOGIN_STALL_TIME: 500,
197
204
  ROOT_REDIRECT: './admin',
198
205
  CORS_ENABLED: false,
199
206
  CORS_ORIGIN: false,
@@ -24,6 +24,7 @@ declare class ExtensionManager {
24
24
  private load;
25
25
  private unload;
26
26
  private initializeWatcher;
27
+ private closeWatcher;
27
28
  private updateWatchedExtensions;
28
29
  private getExtensions;
29
30
  private generateExtensionBundles;
@@ -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,7 +119,7 @@ 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' } },
@@ -129,19 +129,23 @@ class FlowManager {
129
129
  const flowTrees = flows.map((flow) => (0, construct_flow_tree_1.constructFlowTree)(flow));
130
130
  for (const flow of flowTrees) {
131
131
  if (flow.trigger === 'event') {
132
- const events = (_d = (_c = (_b = (0, utils_1.toArray)((_a = flow.options) === null || _a === void 0 ? void 0 : _a.scope)) === null || _b === void 0 ? void 0 : _b.map((scope) => {
133
- var _a, _b, _c;
134
- if (['items.create', 'items.update', 'items.delete'].includes(scope)) {
135
- return ((_c = (_b = (_a = flow.options) === null || _a === void 0 ? void 0 : _a.collections) === null || _b === void 0 ? void 0 : _b.map((collection) => {
136
- if (collection.startsWith('directus_')) {
137
- const action = scope.split('.')[1];
138
- return collection.substring(9) + '.' + action;
139
- }
140
- return `${collection}.${scope}`;
141
- })) !== null && _c !== void 0 ? _c : []);
142
- }
143
- return scope;
144
- })) === 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
+ : [];
145
149
  if (flow.options.type === 'filter') {
146
150
  const handler = (payload, meta, context) => this.executeFlow(flow, { payload, ...meta }, {
147
151
  accountability: context.accountability,
@@ -196,9 +200,9 @@ class FlowManager {
196
200
  return this.executeFlow(flow, data, context);
197
201
  }
198
202
  };
199
- 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';
200
204
  // Default return to $last for webhooks
201
- 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';
202
206
  this.webhookFlowHandlers[`${method}-${flow.id}`] = handler;
203
207
  }
204
208
  else if (flow.trigger === 'manual') {
@@ -254,7 +258,7 @@ class FlowManager {
254
258
  this.isLoaded = false;
255
259
  }
256
260
  async executeFlow(flow, data = null, context = {}) {
257
- var _a, _b, _c, _d, _e, _f;
261
+ var _a, _b, _c, _d, _e, _f, _g;
258
262
  const database = (_a = context.database) !== null && _a !== void 0 ? _a : (0, database_1.default)();
259
263
  const schema = (_b = context.schema) !== null && _b !== void 0 ? _b : (await (0, get_schema_1.getSchema)({ database }));
260
264
  const keyedData = {
@@ -263,11 +267,13 @@ class FlowManager {
263
267
  [ACCOUNTABILITY_KEY]: (_c = context === null || context === void 0 ? void 0 : context.accountability) !== null && _c !== void 0 ? _c : null,
264
268
  };
265
269
  let nextOperation = flow.operation;
270
+ let lastOperationStatus = 'unknown';
266
271
  const steps = [];
267
272
  while (nextOperation !== null) {
268
273
  const { successor, data, status, options } = await this.executeOperation(nextOperation, keyedData, context);
269
274
  keyedData[nextOperation.key] = data;
270
275
  keyedData[LAST_KEY] = data;
276
+ lastOperationStatus = status;
271
277
  steps.push({ operation: nextOperation.id, key: nextOperation.key, status, options });
272
278
  nextOperation = successor;
273
279
  }
@@ -283,6 +289,7 @@ class FlowManager {
283
289
  collection: 'directus_flows',
284
290
  ip: (_e = accountability === null || accountability === void 0 ? void 0 : accountability.ip) !== null && _e !== void 0 ? _e : null,
285
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,
286
293
  item: flow.id,
287
294
  });
288
295
  if (flow.accountability === 'all') {
@@ -301,6 +308,9 @@ class FlowManager {
301
308
  });
302
309
  }
303
310
  }
311
+ if (flow.trigger === 'event' && flow.options.type === 'filter' && lastOperationStatus === 'reject') {
312
+ throw keyedData[LAST_KEY];
313
+ }
304
314
  if (flow.options.return === '$all') {
305
315
  return keyedData;
306
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, {
@@ -34,7 +34,16 @@ test('Short-circuits when authenticate filter is used', async () => {
34
34
  test('Uses default public accountability when no token is given', async () => {
35
35
  const req = {
36
36
  ip: '127.0.0.1',
37
- get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
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
+ }),
38
47
  };
39
48
  const res = {};
40
49
  const next = jest.fn();
@@ -47,6 +56,7 @@ test('Uses default public accountability when no token is given', async () => {
47
56
  app: false,
48
57
  ip: '127.0.0.1',
49
58
  userAgent: 'fake-user-agent',
59
+ origin: 'fake-origin',
50
60
  });
51
61
  expect(next).toHaveBeenCalledTimes(1);
52
62
  });
@@ -70,7 +80,16 @@ test('Sets accountability to payload contents if valid token is passed', async (
70
80
  }, env_1.default.SECRET, { issuer: 'directus' });
71
81
  const req = {
72
82
  ip: '127.0.0.1',
73
- get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
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
+ }),
74
93
  token,
75
94
  };
76
95
  const res = {};
@@ -85,6 +104,7 @@ test('Sets accountability to payload contents if valid token is passed', async (
85
104
  share_scope: shareScope,
86
105
  ip: '127.0.0.1',
87
106
  userAgent: 'fake-user-agent',
107
+ origin: 'fake-origin',
88
108
  });
89
109
  expect(next).toHaveBeenCalledTimes(1);
90
110
  // Test with 1/0 instead or true/false
@@ -107,6 +127,7 @@ test('Sets accountability to payload contents if valid token is passed', async (
107
127
  share_scope: shareScope,
108
128
  ip: '127.0.0.1',
109
129
  userAgent: 'fake-user-agent',
130
+ origin: 'fake-origin',
110
131
  });
111
132
  expect(next).toHaveBeenCalledTimes(1);
112
133
  });
@@ -120,7 +141,16 @@ test('Throws InvalidCredentialsException when static token is used, but user doe
120
141
  });
121
142
  const req = {
122
143
  ip: '127.0.0.1',
123
- get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
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
+ }),
124
154
  token: 'static-token',
125
155
  };
126
156
  const res = {};
@@ -131,7 +161,16 @@ test('Throws InvalidCredentialsException when static token is used, but user doe
131
161
  test('Sets accountability to user information when static token is used', async () => {
132
162
  const req = {
133
163
  ip: '127.0.0.1',
134
- get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
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
+ }),
135
174
  token: 'static-token',
136
175
  };
137
176
  const res = {};
@@ -144,6 +183,7 @@ test('Sets accountability to user information when static token is used', async
144
183
  admin: testUser.admin_access,
145
184
  ip: '127.0.0.1',
146
185
  userAgent: 'fake-user-agent',
186
+ origin: 'fake-origin',
147
187
  };
148
188
  jest.mocked(database_1.default).mockReturnValue({
149
189
  select: jest.fn().mockReturnThis(),
@@ -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
+ });
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const utils_1 = require("@directus/shared/utils");
7
7
  const axios_1 = __importDefault(require("axios"));
8
+ const encodeurl_1 = __importDefault(require("encodeurl"));
8
9
  exports.default = (0, utils_1.defineOperationApi)({
9
10
  id: 'request',
10
11
  handler: async ({ url, method, body, headers }) => {
@@ -16,9 +17,8 @@ exports.default = (0, utils_1.defineOperationApi)({
16
17
  if (!customHeaders['Content-Type'] && isValidJSON(body)) {
17
18
  customHeaders['Content-Type'] = 'application/json';
18
19
  }
19
- const shouldEncode = decodeURI(url) === url;
20
20
  const result = await (0, axios_1.default)({
21
- url: shouldEncode ? encodeURI(url) : url,
21
+ url: (0, encodeurl_1.default)(url),
22
22
  method,
23
23
  data: body,
24
24
  headers: customHeaders,
@@ -1,5 +1,5 @@
1
1
  declare type Options = {
2
- json: string;
2
+ json: string | Record<string, any>;
3
3
  };
4
4
  declare const _default: import("@directus/shared/types").OperationApiConfig<Options>;
5
5
  export default _default;
@@ -4,6 +4,6 @@ const utils_1 = require("@directus/shared/utils");
4
4
  exports.default = (0, utils_1.defineOperationApi)({
5
5
  id: 'transform',
6
6
  handler: ({ json }) => {
7
- return (0, utils_1.parseJSON)(json);
7
+ return (0, utils_1.optionToObject)(json);
8
8
  },
9
9
  });
@@ -36,15 +36,23 @@ class AuthenticationService {
36
36
  * to handle password existence checks elsewhere
37
37
  */
38
38
  async login(providerName = constants_1.DEFAULT_AUTH_PROVIDER, payload, otp) {
39
- var _a, _b;
40
- const STALL_TIME = 100;
39
+ var _a, _b, _c;
40
+ const STALL_TIME = env_1.default.LOGIN_STALL_TIME;
41
41
  const timeStart = perf_hooks_1.performance.now();
42
42
  const provider = (0, auth_1.getAuthProvider)(providerName);
43
+ let userId;
44
+ try {
45
+ userId = await provider.getUserID((0, lodash_1.cloneDeep)(payload));
46
+ }
47
+ catch (err) {
48
+ await (0, stall_1.stall)(STALL_TIME, timeStart);
49
+ throw err;
50
+ }
43
51
  const user = await this.knex
44
52
  .select('u.id', 'u.first_name', 'u.last_name', 'u.email', 'u.password', 'u.status', 'u.role', 'r.admin_access', 'r.app_access', 'u.tfa_secret', 'u.provider', 'u.external_identifier', 'u.auth_data')
45
53
  .from('directus_users as u')
46
54
  .leftJoin('directus_roles as r', 'u.role', 'r.id')
47
- .where('u.id', await provider.getUserID((0, lodash_1.cloneDeep)(payload)))
55
+ .where('u.id', userId)
48
56
  .first();
49
57
  const updatedPayload = await emitter_1.default.emitFilter('auth.login', payload, {
50
58
  status: 'pending',
@@ -151,6 +159,7 @@ class AuthenticationService {
151
159
  expires: refreshTokenExpiration,
152
160
  ip: (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.ip,
153
161
  user_agent: (_b = this.accountability) === null || _b === void 0 ? void 0 : _b.userAgent,
162
+ origin: (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.origin,
154
163
  });
155
164
  await this.knex('directus_sessions').delete().where('expires', '<', new Date());
156
165
  if (this.accountability) {
@@ -159,6 +168,7 @@ class AuthenticationService {
159
168
  user: user.id,
160
169
  ip: this.accountability.ip,
161
170
  user_agent: this.accountability.userAgent,
171
+ origin: this.accountability.origin,
162
172
  collection: 'directus_users',
163
173
  item: user.id,
164
174
  });
@@ -46,6 +46,7 @@ const utils_1 = require("@directus/shared/utils");
46
46
  const items_1 = require("./items");
47
47
  const net_1 = __importDefault(require("net"));
48
48
  const os_1 = __importDefault(require("os"));
49
+ const encodeurl_1 = __importDefault(require("encodeurl"));
49
50
  const lookupDNS = (0, util_1.promisify)(dns_1.lookup);
50
51
  class FilesService extends items_1.ItemsService {
51
52
  constructor(options) {
@@ -229,8 +230,7 @@ class FilesService extends items_1.ItemsService {
229
230
  }
230
231
  let fileResponse;
231
232
  try {
232
- const shouldEncode = decodeURI(importURL) === importURL;
233
- fileResponse = await axios_1.default.get(shouldEncode ? encodeURI(importURL) : importURL, {
233
+ fileResponse = await axios_1.default.get((0, encodeurl_1.default)(importURL), {
234
234
  responseType: 'stream',
235
235
  });
236
236
  }
@@ -293,41 +293,43 @@ class GraphQLService {
293
293
  return obj[field.field];
294
294
  },
295
295
  };
296
- if (field.type === 'date') {
297
- acc[`${field.field}_func`] = {
298
- type: DateFunctions,
299
- resolve: (obj) => {
300
- const funcFields = Object.keys(DateFunctions.getFields()).map((key) => `${field.field}_${key}`);
301
- return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
302
- },
303
- };
304
- }
305
- if (field.type === 'time') {
306
- acc[`${field.field}_func`] = {
307
- type: TimeFunctions,
308
- resolve: (obj) => {
309
- const funcFields = Object.keys(TimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
310
- return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
311
- },
312
- };
313
- }
314
- if (field.type === 'dateTime' || field.type === 'timestamp') {
315
- acc[`${field.field}_func`] = {
316
- type: DateTimeFunctions,
317
- resolve: (obj) => {
318
- const funcFields = Object.keys(DateTimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
319
- return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
320
- },
321
- };
322
- }
323
- if (field.type === 'json' || field.type === 'alias') {
324
- acc[`${field.field}_func`] = {
325
- type: CountFunctions,
326
- resolve: (obj) => {
327
- const funcFields = Object.keys(CountFunctions.getFields()).map((key) => `${field.field}_${key}`);
328
- return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
329
- },
330
- };
296
+ if (action === 'read') {
297
+ if (field.type === 'date') {
298
+ acc[`${field.field}_func`] = {
299
+ type: DateFunctions,
300
+ resolve: (obj) => {
301
+ const funcFields = Object.keys(DateFunctions.getFields()).map((key) => `${field.field}_${key}`);
302
+ return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
303
+ },
304
+ };
305
+ }
306
+ if (field.type === 'time') {
307
+ acc[`${field.field}_func`] = {
308
+ type: TimeFunctions,
309
+ resolve: (obj) => {
310
+ const funcFields = Object.keys(TimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
311
+ return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
312
+ },
313
+ };
314
+ }
315
+ if (field.type === 'dateTime' || field.type === 'timestamp') {
316
+ acc[`${field.field}_func`] = {
317
+ type: DateTimeFunctions,
318
+ resolve: (obj) => {
319
+ const funcFields = Object.keys(DateTimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
320
+ return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
321
+ },
322
+ };
323
+ }
324
+ if (field.type === 'json' || field.type === 'alias') {
325
+ acc[`${field.field}_func`] = {
326
+ type: CountFunctions,
327
+ resolve: (obj) => {
328
+ const funcFields = Object.keys(CountFunctions.getFields()).map((key) => `${field.field}_${key}`);
329
+ return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
330
+ },
331
+ };
332
+ }
331
333
  }
332
334
  return acc;
333
335
  }, {}),
@@ -1615,6 +1617,7 @@ class GraphQLService {
1615
1617
  const accountability = {
1616
1618
  ip: req === null || req === void 0 ? void 0 : req.ip,
1617
1619
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1620
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1618
1621
  role: null,
1619
1622
  };
1620
1623
  const authenticationService = new authentication_1.AuthenticationService({
@@ -1649,6 +1652,7 @@ class GraphQLService {
1649
1652
  const accountability = {
1650
1653
  ip: req === null || req === void 0 ? void 0 : req.ip,
1651
1654
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1655
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1652
1656
  role: null,
1653
1657
  };
1654
1658
  const authenticationService = new authentication_1.AuthenticationService({
@@ -1685,6 +1689,7 @@ class GraphQLService {
1685
1689
  const accountability = {
1686
1690
  ip: req === null || req === void 0 ? void 0 : req.ip,
1687
1691
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1692
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1688
1693
  role: null,
1689
1694
  };
1690
1695
  const authenticationService = new authentication_1.AuthenticationService({
@@ -1709,6 +1714,7 @@ class GraphQLService {
1709
1714
  const accountability = {
1710
1715
  ip: req === null || req === void 0 ? void 0 : req.ip,
1711
1716
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1717
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1712
1718
  role: null,
1713
1719
  };
1714
1720
  const service = new users_1.UsersService({ accountability, schema: this.schema });
@@ -1733,6 +1739,7 @@ class GraphQLService {
1733
1739
  const accountability = {
1734
1740
  ip: req === null || req === void 0 ? void 0 : req.ip,
1735
1741
  userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
1742
+ origin: req === null || req === void 0 ? void 0 : req.get('origin'),
1736
1743
  role: null,
1737
1744
  };
1738
1745
  const service = new users_1.UsersService({ accountability, schema: this.schema });
@@ -2303,7 +2310,7 @@ class GraphQLService {
2303
2310
  comment: (0, graphql_1.GraphQLNonNull)(graphql_1.GraphQLString),
2304
2311
  },
2305
2312
  resolve: async (_, args, __, info) => {
2306
- var _a, _b, _c, _d, _e;
2313
+ var _a, _b, _c, _d, _e, _f;
2307
2314
  const service = new activity_1.ActivityService({
2308
2315
  accountability: this.accountability,
2309
2316
  schema: this.schema,
@@ -2314,9 +2321,10 @@ class GraphQLService {
2314
2321
  user: (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.user,
2315
2322
  ip: (_b = this.accountability) === null || _b === void 0 ? void 0 : _b.ip,
2316
2323
  user_agent: (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.userAgent,
2324
+ origin: (_d = this.accountability) === null || _d === void 0 ? void 0 : _d.origin,
2317
2325
  });
2318
2326
  if ('directus_activity' in ReadCollectionTypes) {
2319
- const selections = this.replaceFragmentsInSelections((_e = (_d = info.fieldNodes[0]) === null || _d === void 0 ? void 0 : _d.selectionSet) === null || _e === void 0 ? void 0 : _e.selections, info.fragments);
2327
+ const selections = this.replaceFragmentsInSelections((_f = (_e = info.fieldNodes[0]) === null || _e === void 0 ? void 0 : _e.selectionSet) === null || _f === void 0 ? void 0 : _f.selections, info.fragments);
2320
2328
  const query = this.getQuery(args, selections || [], info.variableValues);
2321
2329
  return await service.readOne(primaryKey, query);
2322
2330
  }
@@ -130,6 +130,7 @@ class ItemsService {
130
130
  collection: this.collection,
131
131
  ip: this.accountability.ip,
132
132
  user_agent: this.accountability.userAgent,
133
+ origin: this.accountability.origin,
133
134
  item: primaryKey,
134
135
  });
135
136
  // If revisions are tracked, create revisions record
@@ -214,7 +215,16 @@ class ItemsService {
214
215
  * Get items by query
215
216
  */
216
217
  async readByQuery(query, opts) {
217
- let ast = await (0, get_ast_from_query_1.default)(this.collection, query, this.schema, {
218
+ const updatedQuery = (opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false
219
+ ? await emitter_1.default.emitFilter(`${this.eventScope}.query`, query, {
220
+ collection: this.collection,
221
+ }, {
222
+ database: this.knex,
223
+ schema: this.schema,
224
+ accountability: this.accountability,
225
+ })
226
+ : query;
227
+ let ast = await (0, get_ast_from_query_1.default)(this.collection, updatedQuery, this.schema, {
218
228
  accountability: this.accountability,
219
229
  // By setting the permissions action, you can read items using the permissions for another
220
230
  // operation's permissions. This is used to dynamically check if you have update/delete
@@ -240,7 +250,7 @@ class ItemsService {
240
250
  }
241
251
  const filteredRecords = (opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false
242
252
  ? await emitter_1.default.emitFilter(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, records, {
243
- query,
253
+ query: updatedQuery,
244
254
  collection: this.collection,
245
255
  }, {
246
256
  database: this.knex,
@@ -251,7 +261,7 @@ class ItemsService {
251
261
  if ((opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false) {
252
262
  emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, {
253
263
  payload: filteredRecords,
254
- query,
264
+ query: updatedQuery,
255
265
  collection: this.collection,
256
266
  }, {
257
267
  database: this.knex || (0, database_1.default)(),
@@ -406,6 +416,7 @@ class ItemsService {
406
416
  collection: this.collection,
407
417
  ip: this.accountability.ip,
408
418
  user_agent: this.accountability.userAgent,
419
+ origin: this.accountability.origin,
409
420
  item: key,
410
421
  })));
411
422
  if (this.schema.collections[this.collection].accountability === 'all') {
@@ -573,6 +584,7 @@ class ItemsService {
573
584
  collection: this.collection,
574
585
  ip: this.accountability.ip,
575
586
  user_agent: this.accountability.userAgent,
587
+ origin: this.accountability.origin,
576
588
  item: key,
577
589
  })));
578
590
  }
@@ -538,10 +538,34 @@ class PayloadService {
538
538
  if (error)
539
539
  throw new exceptions_1.InvalidPayloadException(`Invalid one-to-many update structure: ${error.message}`);
540
540
  if (alterations.create) {
541
- await itemsService.createMany(alterations.create.map((item) => ({
542
- ...item,
543
- [relation.field]: parent || payload[currentPrimaryKeyField],
544
- })), {
541
+ const sortField = relation.meta.sort_field;
542
+ let createPayload;
543
+ if (sortField !== null) {
544
+ const highestOrderNumber = await this.knex
545
+ .from(relation.collection)
546
+ .where({ [relation.field]: parent })
547
+ .whereNotNull(sortField)
548
+ .max(sortField)
549
+ .first();
550
+ createPayload = alterations.create.map((item, index) => {
551
+ const record = (0, lodash_1.cloneDeep)(item);
552
+ // add sort field value if it is not supplied in the item
553
+ if (parent !== null && record[sortField] === undefined) {
554
+ record[sortField] = (highestOrderNumber === null || highestOrderNumber === void 0 ? void 0 : highestOrderNumber.max) ? highestOrderNumber.max + index + 1 : index + 1;
555
+ }
556
+ return {
557
+ ...record,
558
+ [relation.field]: parent || payload[currentPrimaryKeyField],
559
+ };
560
+ });
561
+ }
562
+ else {
563
+ createPayload = alterations.create.map((item) => ({
564
+ ...item,
565
+ [relation.field]: parent || payload[currentPrimaryKeyField],
566
+ }));
567
+ }
568
+ await itemsService.createMany(createPayload, {
545
569
  onRevisionCreate: (pk) => revisions.push(pk),
546
570
  bypassEmitAction: (params) => nestedActionEvents.push(params),
547
571
  emitEvents: opts === null || opts === void 0 ? void 0 : opts.emitEvents,
@@ -143,7 +143,7 @@ class ServerService {
143
143
  componentType: 'datastore',
144
144
  observedUnit: 'ms',
145
145
  observedValue: 0,
146
- threshold: 150,
146
+ threshold: env_1.default.DB_HEALTHCHECK_THRESHOLD ? +env_1.default.DB_HEALTHCHECK_THRESHOLD : 150,
147
147
  },
148
148
  ];
149
149
  const startTime = perf_hooks_1.performance.now();
@@ -188,7 +188,7 @@ class ServerService {
188
188
  componentType: 'cache',
189
189
  observedValue: 0,
190
190
  observedUnit: 'ms',
191
- threshold: 150,
191
+ threshold: env_1.default.CACHE_HEALTHCHECK_THRESHOLD ? +env_1.default.CACHE_HEALTHCHECK_THRESHOLD : 150,
192
192
  },
193
193
  ],
194
194
  };
@@ -222,7 +222,7 @@ class ServerService {
222
222
  componentType: 'ratelimiter',
223
223
  observedValue: 0,
224
224
  observedUnit: 'ms',
225
- threshold: 150,
225
+ threshold: env_1.default.RATE_LIMITER_HEALTHCHECK_THRESHOLD ? +env_1.default.RATE_LIMITER_HEALTHCHECK_THRESHOLD : 150,
226
226
  },
227
227
  ],
228
228
  };
@@ -249,13 +249,14 @@ class ServerService {
249
249
  const checks = {};
250
250
  for (const location of (0, utils_1.toArray)(env_1.default.STORAGE_LOCATIONS)) {
251
251
  const disk = storage_1.default.disk(location);
252
+ const envThresholdKey = `STORAGE_${location}_HEALTHCHECK_THRESHOLD`.toUpperCase();
252
253
  checks[`storage:${location}:responseTime`] = [
253
254
  {
254
255
  status: 'ok',
255
256
  componentType: 'objectstore',
256
257
  observedValue: 0,
257
258
  observedUnit: 'ms',
258
- threshold: 750,
259
+ threshold: env_1.default[envThresholdKey] ? +env_1.default[envThresholdKey] : 750,
259
260
  },
260
261
  ];
261
262
  const startTime = perf_hooks_1.performance.now();
@@ -31,7 +31,7 @@ class SharesService extends items_1.ItemsService {
31
31
  return super.createOne(data, opts);
32
32
  }
33
33
  async login(payload) {
34
- var _a, _b;
34
+ var _a, _b, _c;
35
35
  const record = await this.knex
36
36
  .select({
37
37
  share_id: 'id',
@@ -86,6 +86,7 @@ class SharesService extends items_1.ItemsService {
86
86
  expires: refreshTokenExpiration,
87
87
  ip: (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.ip,
88
88
  user_agent: (_b = this.accountability) === null || _b === void 0 ? void 0 : _b.userAgent,
89
+ origin: (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.origin,
89
90
  share: record.share_id,
90
91
  });
91
92
  await this.knex('directus_sessions').delete().where('expires', '<', new Date());
@@ -1,7 +1,3 @@
1
- import { ErrorRequestHandler, RequestHandler } from 'express';
2
- /**
3
- * Handles promises in routes.
4
- */
5
- declare function asyncHandler(handler: RequestHandler): RequestHandler;
6
- declare function asyncHandler(handler: ErrorRequestHandler): ErrorRequestHandler;
1
+ import type { RequestHandler, Request, Response, NextFunction } from 'express';
2
+ declare const asyncHandler: (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
7
3
  export default asyncHandler;
@@ -1,16 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- function asyncHandler(handler) {
4
- if (handler.length === 2 || handler.length === 3) {
5
- const scoped = (req, res, next) => Promise.resolve(handler(req, res, next)).catch(next);
6
- return scoped;
7
- }
8
- else if (handler.length === 4) {
9
- const scoped = (err, req, res, next) => Promise.resolve(handler(err, req, res, next)).catch(next);
10
- return scoped;
11
- }
12
- else {
13
- throw new Error(`Failed to asyncHandle() function "${handler.name}"`);
14
- }
15
- }
3
+ const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
16
4
  exports.default = asyncHandler;
@@ -0,0 +1 @@
1
+ import '../../src/types/express.d.ts';
@@ -0,0 +1,18 @@
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
+ require("../../src/types/express.d.ts");
7
+ const async_handler_1 = __importDefault(require("./async-handler"));
8
+ let mockRequest;
9
+ let mockResponse;
10
+ const nextFunction = jest.fn();
11
+ test('Wraps async middleware in Promise resolve that will catch rejects and pass them to the nextFn', async () => {
12
+ const err = new Error('testing');
13
+ const middleware = async (_req, _res, _next) => {
14
+ throw err;
15
+ };
16
+ await (0, async_handler_1.default)(middleware)(mockRequest, mockResponse, nextFunction);
17
+ expect(nextFunction).toHaveBeenCalledWith(err);
18
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directus",
3
- "version": "9.16.1",
3
+ "version": "9.17.0",
4
4
  "license": "GPL-3.0-only",
5
5
  "homepage": "https://github.com/directus/directus#readme",
6
6
  "description": "Directus is a real-time API and App dashboard for managing SQL database content.",
@@ -66,16 +66,16 @@
66
66
  ],
67
67
  "dependencies": {
68
68
  "@aws-sdk/client-ses": "^3.107.0",
69
- "@directus/app": "9.16.1",
70
- "@directus/drive": "9.16.1",
71
- "@directus/drive-azure": "9.16.1",
72
- "@directus/drive-gcs": "9.16.1",
73
- "@directus/drive-s3": "9.16.1",
69
+ "@directus/app": "9.17.0",
70
+ "@directus/drive": "9.17.0",
71
+ "@directus/drive-azure": "9.17.0",
72
+ "@directus/drive-gcs": "9.17.0",
73
+ "@directus/drive-s3": "9.17.0",
74
74
  "@directus/extensions-sdk": "^9.14.1",
75
75
  "@directus/format-title": "^9.15.0",
76
- "@directus/schema": "9.16.1",
77
- "@directus/shared": "9.16.1",
78
- "@directus/specs": "9.16.1",
76
+ "@directus/schema": "9.17.0",
77
+ "@directus/shared": "9.17.0",
78
+ "@directus/specs": "9.17.0",
79
79
  "@godaddy/terminus": "^4.10.2",
80
80
  "@rollup/plugin-alias": "^3.1.9",
81
81
  "@rollup/plugin-virtual": "^2.1.0",
@@ -97,6 +97,7 @@
97
97
  "deep-map": "^2.0.0",
98
98
  "destroy": "^1.2.0",
99
99
  "dotenv": "^10.0.0",
100
+ "encodeurl": "^1.0.2",
100
101
  "eventemitter2": "^6.4.5",
101
102
  "execa": "^5.1.1",
102
103
  "exifr": "^7.1.3",
@@ -126,7 +127,7 @@
126
127
  "mime-types": "^2.1.35",
127
128
  "ms": "^2.1.3",
128
129
  "nanoid": "^3.1.23",
129
- "node-cron": "^3.0.1",
130
+ "node-cron": "^3.0.2",
130
131
  "node-machine-id": "^1.1.12",
131
132
  "nodemailer": "^6.7.5",
132
133
  "object-hash": "^2.2.0",
@@ -175,6 +176,7 @@
175
176
  "@types/cors": "2.8.12",
176
177
  "@types/deep-diff": "1.0.1",
177
178
  "@types/destroy": "1.0.0",
179
+ "@types/encodeurl": "^1.0.0",
178
180
  "@types/express": "4.17.13",
179
181
  "@types/express-pino-logger": "4.0.3",
180
182
  "@types/express-serve-static-core": "^4.17.29",