directus 9.16.1 → 9.17.2

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 (46) 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 +8 -1
  7. package/dist/controllers/activity.js +1 -0
  8. package/dist/controllers/auth.js +6 -9
  9. package/dist/database/helpers/schema/dialects/sqlite.d.ts +2 -2
  10. package/dist/database/helpers/schema/dialects/sqlite.js +2 -2
  11. package/dist/database/helpers/schema/types.d.ts +2 -2
  12. package/dist/database/helpers/schema/types.js +2 -2
  13. package/dist/database/migrations/20220826A-add-origin-to-accountability.d.ts +3 -0
  14. package/dist/database/migrations/20220826A-add-origin-to-accountability.js +21 -0
  15. package/dist/database/system-data/fields/activity.yaml +6 -0
  16. package/dist/database/system-data/fields/sessions.yaml +2 -0
  17. package/dist/env.js +8 -0
  18. package/dist/extensions.d.ts +1 -0
  19. package/dist/extensions.js +16 -3
  20. package/dist/flows.js +27 -17
  21. package/dist/mailer.js +7 -0
  22. package/dist/middleware/authenticate.d.ts +1 -1
  23. package/dist/middleware/authenticate.js +1 -0
  24. package/dist/middleware/authenticate.test.js +44 -4
  25. package/dist/middleware/validate-batch.d.ts +1 -2
  26. package/dist/middleware/validate-batch.js +10 -13
  27. package/dist/middleware/validate-batch.test.d.ts +1 -0
  28. package/dist/middleware/validate-batch.test.js +82 -0
  29. package/dist/operations/request/index.js +2 -2
  30. package/dist/operations/transform/index.d.ts +1 -1
  31. package/dist/operations/transform/index.js +1 -1
  32. package/dist/services/authentication.js +13 -3
  33. package/dist/services/fields.js +11 -3
  34. package/dist/services/files.js +2 -2
  35. package/dist/services/graphql/index.js +45 -37
  36. package/dist/services/items.js +15 -3
  37. package/dist/services/payload.js +28 -4
  38. package/dist/services/relations.d.ts +2 -0
  39. package/dist/services/relations.js +14 -0
  40. package/dist/services/server.js +5 -4
  41. package/dist/services/shares.js +2 -1
  42. package/dist/utils/async-handler.d.ts +2 -6
  43. package/dist/utils/async-handler.js +1 -13
  44. package/dist/utils/async-handler.test.d.ts +1 -0
  45. package/dist/utils/async-handler.test.js +18 -0
  46. package/package.json +14 -11
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
 
@@ -288,7 +292,7 @@ EXTENSIONS_AUTO_RELOAD=false
288
292
  EMAIL_FROM="no-reply@directus.io"
289
293
 
290
294
  # What to use to send emails. One of
291
- # sendmail, smtp, mailgun, ses.
295
+ # sendmail, smtp, mailgun, sendgrid, ses.
292
296
  EMAIL_TRANSPORT="sendmail"
293
297
  EMAIL_SENDMAIL_NEW_LINE="unix"
294
298
  EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
@@ -315,3 +319,6 @@ EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
315
319
  ## Email (Mailgun Transport)
316
320
  # EMAIL_MAILGUN_API_KEY="key-1234123412341234"
317
321
  # EMAIL_MAILGUN_DOMAIN="a domain name from https://app.mailgun.com/app/sending/domains"
322
+
323
+ ## Email (SendGrid Transport)
324
+ # EMAIL_SENDGRID_API_KEY="key-1234123412341234"
@@ -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 });
@@ -1,5 +1,5 @@
1
1
  import { SchemaHelper } from '../types';
2
2
  export declare class SchemaHelperSQLite extends SchemaHelper {
3
- preColumnDelete(): Promise<boolean>;
4
- postColumnDelete(): Promise<void>;
3
+ preColumnChange(): Promise<boolean>;
4
+ postColumnChange(): Promise<void>;
5
5
  }
@@ -3,14 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SchemaHelperSQLite = void 0;
4
4
  const types_1 = require("../types");
5
5
  class SchemaHelperSQLite extends types_1.SchemaHelper {
6
- async preColumnDelete() {
6
+ async preColumnChange() {
7
7
  const foreignCheckStatus = (await this.knex.raw('PRAGMA foreign_keys'))[0].foreign_keys === 1;
8
8
  if (foreignCheckStatus) {
9
9
  await this.knex.raw('PRAGMA foreign_keys = OFF');
10
10
  }
11
11
  return foreignCheckStatus;
12
12
  }
13
- async postColumnDelete() {
13
+ async postColumnChange() {
14
14
  await this.knex.raw('PRAGMA foreign_keys = ON');
15
15
  }
16
16
  }
@@ -11,7 +11,7 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
11
11
  changeNullable(table: string, column: string, nullable: boolean): Promise<void>;
12
12
  changeToType(table: string, column: string, type: typeof KNEX_TYPES[number], options?: Options): Promise<void>;
13
13
  protected changeToTypeByCopy(table: string, column: string, type: typeof KNEX_TYPES[number], options: Options): Promise<void>;
14
- preColumnDelete(): Promise<boolean>;
15
- postColumnDelete(): Promise<void>;
14
+ preColumnChange(): Promise<boolean>;
15
+ postColumnChange(): Promise<void>;
16
16
  }
17
17
  export {};
@@ -56,10 +56,10 @@ class SchemaHelper extends types_1.DatabaseHelper {
56
56
  await this.changeNullable(table, column, options.nullable);
57
57
  }
58
58
  }
59
- async preColumnDelete() {
59
+ async preColumnChange() {
60
60
  return false;
61
61
  }
62
- async postColumnDelete() {
62
+ async postColumnChange() {
63
63
  return;
64
64
  }
65
65
  }
@@ -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',
@@ -161,6 +165,7 @@ const allowedEnvironmentVars = [
161
165
  'EMAIL_MAILGUN_API_KEY',
162
166
  'EMAIL_MAILGUN_DOMAIN',
163
167
  'EMAIL_MAILGUN_HOST',
168
+ 'EMAIL_SENDGRID_API_KEY',
164
169
  'EMAIL_SES_CREDENTIALS__ACCESS_KEY_ID',
165
170
  'EMAIL_SES_CREDENTIALS__SECRET_ACCESS_KEY',
166
171
  'EMAIL_SES_REGION',
@@ -172,6 +177,8 @@ const allowedEnvironmentVars = [
172
177
  // limits & optimization
173
178
  'RELATIONAL_BATCH_SIZE',
174
179
  'EXPORT_BATCH_SIZE',
180
+ // flows
181
+ 'FLOWS_EXEC_ALLOWED_MODULES',
175
182
  ].map((name) => new RegExp(`^${name}$`));
176
183
  const acceptedEnvTypes = ['string', 'number', 'regex', 'array', 'json'];
177
184
  const defaults = {
@@ -194,6 +201,7 @@ const defaults = {
194
201
  REFRESH_TOKEN_COOKIE_SECURE: false,
195
202
  REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
196
203
  REFRESH_TOKEN_COOKIE_NAME: 'directus_refresh_token',
204
+ LOGIN_STALL_TIME: 500,
197
205
  ROOT_REDIRECT: './admin',
198
206
  CORS_ENABLED: false,
199
207
  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,
@@ -56,6 +57,12 @@ function getMailer() {
56
57
  host: env_1.default.EMAIL_MAILGUN_HOST || 'api.mailgun.net',
57
58
  }));
58
59
  }
60
+ else if (transportName === 'sendgrid') {
61
+ const sg = require('nodemailer-sendgrid');
62
+ transporter = nodemailer_1.default.createTransport(sg({
63
+ apiKey: env_1.default.EMAIL_SENDGRID_API_KEY,
64
+ }));
65
+ }
59
66
  else {
60
67
  logger_1.default.warn('Illegal transport given for email. Check the EMAIL_TRANSPORT env var.');
61
68
  }
@@ -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>;