@directus/api 15.0.0 → 16.0.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 (169) hide show
  1. package/dist/app.js +6 -4
  2. package/dist/auth/drivers/ldap.js +7 -4
  3. package/dist/auth/drivers/local.js +3 -2
  4. package/dist/auth/drivers/oauth2.js +9 -2
  5. package/dist/auth/drivers/openid.js +9 -2
  6. package/dist/auth/drivers/saml.js +6 -4
  7. package/dist/auth.js +7 -4
  8. package/dist/bus/index.d.ts +1 -0
  9. package/dist/bus/index.js +1 -0
  10. package/dist/bus/lib/use-bus.d.ts +9 -0
  11. package/dist/bus/lib/use-bus.js +21 -0
  12. package/dist/cache.js +9 -9
  13. package/dist/cli/commands/bootstrap/index.js +6 -2
  14. package/dist/cli/commands/count/index.js +2 -1
  15. package/dist/cli/commands/database/install.js +2 -1
  16. package/dist/cli/commands/database/migrate.js +2 -1
  17. package/dist/cli/commands/roles/create.js +2 -1
  18. package/dist/cli/commands/schema/apply.js +2 -1
  19. package/dist/cli/commands/schema/snapshot.js +6 -5
  20. package/dist/cli/commands/users/create.js +4 -3
  21. package/dist/cli/commands/users/passwd.js +5 -4
  22. package/dist/cli/load-extensions.js +4 -2
  23. package/dist/cli/utils/create-env/env-stub.liquid +1 -1
  24. package/dist/constants.d.ts +1 -1
  25. package/dist/constants.js +4 -1
  26. package/dist/controllers/assets.js +5 -3
  27. package/dist/controllers/auth.js +5 -4
  28. package/dist/controllers/extensions.js +18 -6
  29. package/dist/controllers/files.js +3 -3
  30. package/dist/controllers/schema.js +3 -2
  31. package/dist/controllers/shares.js +3 -3
  32. package/dist/database/helpers/index.d.ts +1 -1
  33. package/dist/database/index.js +9 -2
  34. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +3 -1
  35. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -1
  36. package/dist/database/migrations/20210802A-replace-groups.js +2 -1
  37. package/dist/database/migrations/20230721A-require-shares-fields.js +2 -1
  38. package/dist/database/migrations/20231215A-add-focalpoints.d.ts +3 -0
  39. package/dist/database/migrations/20231215A-add-focalpoints.js +12 -0
  40. package/dist/database/migrations/run.js +2 -1
  41. package/dist/database/run-ast.js +5 -2
  42. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -7
  43. package/dist/database/system-data/fields/files.yaml +16 -0
  44. package/dist/emitter.js +3 -1
  45. package/dist/extensions/lib/get-extensions-path.d.ts +1 -1
  46. package/dist/extensions/lib/get-extensions-path.js +2 -1
  47. package/dist/extensions/lib/get-extensions.d.ts +1 -1
  48. package/dist/extensions/lib/get-extensions.js +32 -8
  49. package/dist/extensions/lib/get-shared-deps-mapping.js +6 -4
  50. package/dist/extensions/lib/sandbox/register/call-reference.js +4 -2
  51. package/dist/extensions/lib/sandbox/sdk/generators/log.js +2 -1
  52. package/dist/extensions/lib/sync-extensions.js +6 -4
  53. package/dist/extensions/manager.js +43 -19
  54. package/dist/flows.js +13 -7
  55. package/dist/logger.d.ts +7 -7
  56. package/dist/logger.js +116 -92
  57. package/dist/mailer.js +4 -2
  58. package/dist/middleware/cache.js +4 -2
  59. package/dist/middleware/check-ip.js +25 -6
  60. package/dist/middleware/cors.js +2 -1
  61. package/dist/middleware/error-handler.js +5 -5
  62. package/dist/middleware/rate-limiter-global.js +4 -2
  63. package/dist/middleware/rate-limiter-ip.js +2 -1
  64. package/dist/middleware/respond.js +4 -2
  65. package/dist/operations/log/index.js +2 -1
  66. package/dist/rate-limiter.d.ts +2 -1
  67. package/dist/rate-limiter.js +5 -2
  68. package/dist/redis/index.d.ts +3 -2
  69. package/dist/redis/index.js +3 -2
  70. package/dist/redis/{create-redis.js → lib/create-redis.js} +2 -2
  71. package/dist/redis/utils/redis-config-available.d.ts +4 -0
  72. package/dist/redis/utils/redis-config-available.js +8 -0
  73. package/dist/request/request-interceptor.js +7 -5
  74. package/dist/request/response-interceptor.js +2 -2
  75. package/dist/request/validate-ip.d.ts +1 -1
  76. package/dist/request/validate-ip.js +23 -7
  77. package/dist/server.js +11 -7
  78. package/dist/services/activity.js +5 -4
  79. package/dist/services/assets.d.ts +2 -0
  80. package/dist/services/assets.js +9 -4
  81. package/dist/services/authentication.js +17 -9
  82. package/dist/services/collections.js +5 -4
  83. package/dist/services/extensions.d.ts +15 -9
  84. package/dist/services/extensions.js +74 -39
  85. package/dist/services/fields.js +9 -4
  86. package/dist/services/files.d.ts +2 -2
  87. package/dist/services/files.js +22 -14
  88. package/dist/services/graphql/index.js +46 -3
  89. package/dist/services/graphql/subscription.js +2 -2
  90. package/dist/services/graphql/types/bigint.js +16 -5
  91. package/dist/services/graphql/utils/process-error.d.ts +4 -1
  92. package/dist/services/graphql/utils/process-error.js +10 -8
  93. package/dist/services/import-export/index.js +5 -3
  94. package/dist/services/items.js +12 -8
  95. package/dist/services/mail/index.js +4 -2
  96. package/dist/services/notifications.js +7 -3
  97. package/dist/services/relations.js +19 -10
  98. package/dist/services/server.js +5 -4
  99. package/dist/services/shares.js +3 -2
  100. package/dist/services/specifications.js +2 -1
  101. package/dist/services/users.js +20 -9
  102. package/dist/services/versions.js +6 -5
  103. package/dist/services/webhooks.d.ts +2 -2
  104. package/dist/services/webhooks.js +2 -2
  105. package/dist/services/websocket.d.ts +1 -1
  106. package/dist/services/websocket.js +4 -3
  107. package/dist/storage/register-drivers.js +2 -1
  108. package/dist/storage/register-locations.js +2 -1
  109. package/dist/synchronization.js +3 -1
  110. package/dist/telemetry/lib/get-report.js +1 -1
  111. package/dist/telemetry/lib/init-telemetry.js +2 -2
  112. package/dist/telemetry/lib/send-report.js +1 -1
  113. package/dist/telemetry/lib/track.js +2 -3
  114. package/dist/telemetry/utils/get-user-count.js +1 -1
  115. package/dist/types/assets.d.ts +2 -0
  116. package/dist/utils/apply-diff.js +2 -1
  117. package/dist/utils/apply-query.js +0 -11
  118. package/dist/utils/delete-from-require-cache.js +2 -1
  119. package/dist/utils/get-accountability-for-token.js +3 -2
  120. package/dist/utils/get-auth-providers.js +2 -1
  121. package/dist/utils/get-cache-headers.js +5 -2
  122. package/dist/utils/get-config-from-env.js +2 -1
  123. package/dist/utils/get-default-value.js +4 -3
  124. package/dist/utils/get-ip-from-req.js +4 -2
  125. package/dist/utils/get-permissions.js +5 -3
  126. package/dist/utils/get-schema.js +5 -2
  127. package/dist/utils/get-snapshot-diff.js +7 -9
  128. package/dist/utils/get-snapshot.js +4 -4
  129. package/dist/utils/ip-in-networks.d.ts +6 -0
  130. package/dist/utils/ip-in-networks.js +13 -0
  131. package/dist/utils/is-url-allowed.js +2 -1
  132. package/dist/utils/job-queue.d.ts +1 -0
  133. package/dist/utils/job-queue.js +3 -0
  134. package/dist/utils/sanitize-query.js +7 -2
  135. package/dist/utils/sanitize-schema.d.ts +1 -1
  136. package/dist/utils/should-clear-cache.js +2 -1
  137. package/dist/utils/should-skip-cache.js +2 -1
  138. package/dist/utils/transformations.js +95 -12
  139. package/dist/utils/validate-env.js +4 -2
  140. package/dist/utils/validate-query.js +7 -3
  141. package/dist/utils/validate-storage.js +4 -2
  142. package/dist/webhooks.js +4 -3
  143. package/dist/websocket/controllers/base.js +12 -6
  144. package/dist/websocket/controllers/graphql.js +4 -2
  145. package/dist/websocket/controllers/hooks.js +3 -2
  146. package/dist/websocket/controllers/index.js +4 -2
  147. package/dist/websocket/controllers/rest.js +4 -2
  148. package/dist/websocket/errors.js +2 -1
  149. package/dist/websocket/handlers/heartbeat.js +4 -3
  150. package/dist/websocket/handlers/subscribe.d.ts +2 -2
  151. package/dist/websocket/handlers/subscribe.js +5 -4
  152. package/package.json +57 -57
  153. package/dist/__utils__/items-utils.d.ts +0 -2
  154. package/dist/__utils__/items-utils.js +0 -31
  155. package/dist/__utils__/mock-env.d.ts +0 -18
  156. package/dist/__utils__/mock-env.js +0 -41
  157. package/dist/__utils__/schemas.d.ts +0 -13
  158. package/dist/__utils__/schemas.js +0 -301
  159. package/dist/__utils__/snapshots.d.ts +0 -5
  160. package/dist/__utils__/snapshots.js +0 -903
  161. package/dist/env.d.ts +0 -14
  162. package/dist/env.js +0 -511
  163. package/dist/messenger.d.ts +0 -24
  164. package/dist/messenger.js +0 -64
  165. package/dist/utils/to-boolean.d.ts +0 -4
  166. package/dist/utils/to-boolean.js +0 -6
  167. /package/dist/redis/{create-redis.d.ts → lib/create-redis.d.ts} +0 -0
  168. /package/dist/redis/{use-redis.d.ts → lib/use-redis.d.ts} +0 -0
  169. /package/dist/redis/{use-redis.js → lib/use-redis.js} +0 -0
@@ -1,25 +1,26 @@
1
1
  import { JAVASCRIPT_FILE_EXTS } from '@directus/constants';
2
+ import { useEnv } from '@directus/env';
2
3
  import { APP_SHARED_DEPS, HYBRID_EXTENSION_TYPES, NESTED_EXTENSION_TYPES } from '@directus/extensions';
3
4
  import { generateExtensionsEntrypoint } from '@directus/extensions/node';
4
- import { isIn, isTypeIn, pluralize } from '@directus/utils';
5
- import { pathToRelativeUrl } from '@directus/utils/node';
5
+ import { isIn, isTypeIn, pluralize, toBoolean } from '@directus/utils';
6
+ import { getNodeEnv } from '@directus/utils/node';
6
7
  import aliasDefault from '@rollup/plugin-alias';
7
8
  import nodeResolveDefault from '@rollup/plugin-node-resolve';
8
9
  import virtualDefault from '@rollup/plugin-virtual';
9
10
  import chokidar, { FSWatcher } from 'chokidar';
10
11
  import express, { Router } from 'express';
11
12
  import ivm from 'isolated-vm';
12
- import { clone } from 'lodash-es';
13
+ import { clone, debounce } from 'lodash-es';
13
14
  import { readFile, readdir } from 'node:fs/promises';
15
+ import os from 'node:os';
14
16
  import { dirname } from 'node:path';
15
17
  import { fileURLToPath } from 'node:url';
16
18
  import path from 'path';
17
19
  import { rollup } from 'rollup';
18
20
  import getDatabase from '../database/index.js';
19
21
  import emitter, { Emitter } from '../emitter.js';
20
- import env from '../env.js';
21
22
  import { getFlowManager } from '../flows.js';
22
- import logger from '../logger.js';
23
+ import { useLogger } from '../logger.js';
23
24
  import * as services from '../services/index.js';
24
25
  import { deleteFromRequireCache } from '../utils/delete-from-require-cache.js';
25
26
  import getModuleDefault from '../utils/get-module-default.js';
@@ -27,7 +28,6 @@ import { getSchema } from '../utils/get-schema.js';
27
28
  import { importFileUrl } from '../utils/import-file-url.js';
28
29
  import { JobQueue } from '../utils/job-queue.js';
29
30
  import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
30
- import { toBoolean } from '../utils/to-boolean.js';
31
31
  import { getExtensionsPath } from './lib/get-extensions-path.js';
32
32
  import { getExtensionsSettings } from './lib/get-extensions-settings.js';
33
33
  import { getExtensions } from './lib/get-extensions.js';
@@ -41,9 +41,10 @@ const virtual = virtualDefault;
41
41
  const alias = aliasDefault;
42
42
  const nodeResolve = nodeResolveDefault;
43
43
  const __dirname = dirname(fileURLToPath(import.meta.url));
44
+ const env = useEnv();
44
45
  const defaultOptions = {
45
46
  schedule: true,
46
- watch: env['EXTENSIONS_AUTO_RELOAD'] && env['NODE_ENV'] !== 'development',
47
+ watch: env['EXTENSIONS_AUTO_RELOAD'] && getNodeEnv() !== 'development',
47
48
  };
48
49
  export class ExtensionManager {
49
50
  options = defaultOptions;
@@ -109,6 +110,7 @@ export class ExtensionManager {
109
110
  * @param {boolean} options.watch - Whether or not to watch the local extensions folder for changes
110
111
  */
111
112
  async initialize(options = {}) {
113
+ const logger = useLogger();
112
114
  this.options = {
113
115
  ...defaultOptions,
114
116
  ...options,
@@ -134,6 +136,7 @@ export class ExtensionManager {
134
136
  * Load all extensions from disk and register them in their respective places
135
137
  */
136
138
  async load() {
139
+ const logger = useLogger();
137
140
  try {
138
141
  await syncExtensions();
139
142
  }
@@ -171,12 +174,17 @@ export class ExtensionManager {
171
174
  * Reload all the extensions. Will unload if extensions have already been loaded
172
175
  */
173
176
  reload() {
177
+ if (this.reloadQueue.size > 0) {
178
+ // The pending job in the queue will already handle the additional changes
179
+ return;
180
+ }
181
+ const logger = useLogger();
174
182
  this.reloadQueue.enqueue(async () => {
175
183
  if (this.isLoaded) {
176
- logger.info('Reloading extensions');
177
184
  const prevExtensions = clone(this.extensions);
178
185
  await this.unload();
179
186
  await this.load();
187
+ logger.info('Extensions reloaded');
180
188
  const added = this.extensions.filter((extension) => !prevExtensions.some((prevExtension) => extension.path === prevExtension.path));
181
189
  const removed = prevExtensions.filter((prevExtension) => !this.extensions.some((extension) => prevExtension.path === extension.path));
182
190
  this.updateWatchedExtensions(added, removed);
@@ -231,27 +239,34 @@ export class ExtensionManager {
231
239
  * Start the chokidar watcher for extensions on the local filesystem
232
240
  */
233
241
  initializeWatcher() {
242
+ const logger = useLogger();
234
243
  logger.info('Watching extensions for changes...');
235
- const extensionDirUrl = pathToRelativeUrl(getExtensionsPath());
236
- const localExtensionUrls = NESTED_EXTENSION_TYPES.flatMap((type) => {
237
- const typeDir = path.posix.join(extensionDirUrl, pluralize(type));
244
+ const extensionsDir = path.resolve(getExtensionsPath());
245
+ const rootPackageJson = path.resolve(env['PACKAGE_FILE_LOCATION'], 'package.json');
246
+ const localExtensions = path.join(extensionsDir, '*', 'package.json');
247
+ const nestedExtensions = NESTED_EXTENSION_TYPES.flatMap((type) => {
248
+ const typeDir = path.join(extensionsDir, pluralize(type));
238
249
  if (isIn(type, HYBRID_EXTENSION_TYPES)) {
239
250
  return [
240
- path.posix.join(typeDir, '*', `app.{${JAVASCRIPT_FILE_EXTS.join()}}`),
241
- path.posix.join(typeDir, '*', `api.{${JAVASCRIPT_FILE_EXTS.join()}}`),
251
+ path.join(typeDir, '*', `app.{${JAVASCRIPT_FILE_EXTS.join()}}`),
252
+ path.join(typeDir, '*', `api.{${JAVASCRIPT_FILE_EXTS.join()}}`),
242
253
  ];
243
254
  }
244
255
  else {
245
- return path.posix.join(typeDir, '*', `index.{${JAVASCRIPT_FILE_EXTS.join()}}`);
256
+ return path.join(typeDir, '*', `index.{${JAVASCRIPT_FILE_EXTS.join()}}`);
246
257
  }
247
258
  });
248
- this.watcher = chokidar.watch([path.resolve('package.json'), path.posix.join(extensionDirUrl, '*', 'package.json'), ...localExtensionUrls], {
259
+ this.watcher = chokidar.watch([rootPackageJson, localExtensions, ...nestedExtensions], {
249
260
  ignoreInitial: true,
261
+ // dotdirs are watched by default and frequently found in 'node_modules'
262
+ ignored: `${extensionsDir}/**/node_modules/**`,
263
+ // on macOS dotdirs in linked extensions are watched too
264
+ followSymlinks: os.platform() === 'darwin' ? false : true,
250
265
  });
251
266
  this.watcher
252
- .on('add', () => this.reload())
253
- .on('change', () => this.reload())
254
- .on('unlink', () => this.reload());
267
+ .on('add', debounce(() => this.reload(), 500))
268
+ .on('change', debounce(() => this.reload(), 650))
269
+ .on('unlink', debounce(() => this.reload(), 2000));
255
270
  }
256
271
  /**
257
272
  * Close and destroy the local filesystem watcher if enabled
@@ -268,8 +283,12 @@ export class ExtensionManager {
268
283
  */
269
284
  updateWatchedExtensions(added, removed = []) {
270
285
  if (this.watcher) {
286
+ const extensionDir = path.resolve(getExtensionsPath());
287
+ const nestedExtensionDirs = NESTED_EXTENSION_TYPES.map((type) => {
288
+ return path.join(extensionDir, pluralize(type));
289
+ });
271
290
  const toPackageExtensionPaths = (extensions) => extensions
272
- .filter((extension) => !extension.local || extension.type === 'bundle')
291
+ .filter((extension) => !nestedExtensionDirs.some((path) => extension.path.startsWith(path)))
273
292
  .flatMap((extension) => isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle'
274
293
  ? [
275
294
  path.resolve(extension.path, extension.entrypoint.app),
@@ -287,6 +306,7 @@ export class ExtensionManager {
287
306
  * run.
288
307
  */
289
308
  async generateExtensionBundle() {
309
+ const logger = useLogger();
290
310
  const sharedDepsMapping = await getSharedDepsMapping(APP_SHARED_DEPS);
291
311
  const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
292
312
  find: name,
@@ -316,6 +336,7 @@ export class ExtensionManager {
316
336
  return null;
317
337
  }
318
338
  async registerSandboxedApiExtension(extension) {
339
+ const logger = useLogger();
319
340
  const sandboxMemory = Number(env['EXTENSIONS_SANDBOX_MEMORY']);
320
341
  const sandboxTimeout = Number(env['EXTENSIONS_SANDBOX_TIMEOUT']);
321
342
  const entrypointPath = path.resolve(extension.path, isTypeIn(extension, HYBRID_EXTENSION_TYPES) ? extension.entrypoint.api : extension.entrypoint);
@@ -503,6 +524,7 @@ export class ExtensionManager {
503
524
  * Register a single hook
504
525
  */
505
526
  registerHook(hookRegistrationCallback, name) {
527
+ const logger = useLogger();
506
528
  let scheduleIndex = 0;
507
529
  const unregisterFunctions = [];
508
530
  const hookRegistrationContext = {
@@ -582,6 +604,7 @@ export class ExtensionManager {
582
604
  * Register an individual endpoint
583
605
  */
584
606
  registerEndpoint(config, name) {
607
+ const logger = useLogger();
585
608
  const endpointRegistrationCallback = typeof config === 'function' ? config : config.handler;
586
609
  const nameWithoutType = name.includes(':') ? name.split(':')[0] : name;
587
610
  const routeName = typeof config === 'function' ? nameWithoutType : config.id;
@@ -623,6 +646,7 @@ export class ExtensionManager {
623
646
  * Otherwise, the error will only be logged as a warning.
624
647
  */
625
648
  handleExtensionError({ error, reason }) {
649
+ const logger = useLogger();
626
650
  if (toBoolean(env['EXTENSIONS_MUST_LOAD'])) {
627
651
  logger.error('EXTENSION_MUST_LOAD is enabled and an extension failed to load.');
628
652
  logger.error(reason);
package/dist/flows.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { Action } from '@directus/constants';
2
+ import { useEnv } from '@directus/env';
3
+ import { ForbiddenError } from '@directus/errors';
2
4
  import { applyOptionsData, getRedactedString, isValidJSON, parseJSON, toArray } from '@directus/utils';
3
5
  import { omit, pick } from 'lodash-es';
4
6
  import { get } from 'micromustache';
7
+ import { useBus } from './bus/index.js';
5
8
  import getDatabase from './database/index.js';
6
9
  import emitter from './emitter.js';
7
- import env from './env.js';
8
- import { ForbiddenError } from '@directus/errors';
9
- import logger from './logger.js';
10
- import { getMessenger } from './messenger.js';
10
+ import { useLogger } from './logger.js';
11
11
  import { ActivityService } from './services/activity.js';
12
12
  import { FlowsService } from './services/flows.js';
13
13
  import * as services from './services/index.js';
@@ -40,9 +40,11 @@ class FlowManager {
40
40
  reloadQueue;
41
41
  envs;
42
42
  constructor() {
43
+ const env = useEnv();
44
+ const logger = useLogger();
43
45
  this.reloadQueue = new JobQueue();
44
46
  this.envs = env['FLOWS_ENV_ALLOW_LIST'] ? pick(env, toArray(env['FLOWS_ENV_ALLOW_LIST'])) : {};
45
- const messenger = getMessenger();
47
+ const messenger = useBus();
46
48
  messenger.subscribe('flows', (event) => {
47
49
  if (event['type'] === 'reload') {
48
50
  this.reloadQueue.enqueue(async () => {
@@ -63,7 +65,7 @@ class FlowManager {
63
65
  }
64
66
  }
65
67
  async reload() {
66
- const messenger = getMessenger();
68
+ const messenger = useBus();
67
69
  messenger.publish('flows', { type: 'reload' });
68
70
  }
69
71
  addOperation(id, operation) {
@@ -73,6 +75,7 @@ class FlowManager {
73
75
  this.operations.delete(id);
74
76
  }
75
77
  async runOperationFlow(id, data, context) {
78
+ const logger = useLogger();
76
79
  if (!(id in this.operationFlowHandlers)) {
77
80
  logger.warn(`Couldn't find operation triggered flow with id "${id}"`);
78
81
  return null;
@@ -81,6 +84,7 @@ class FlowManager {
81
84
  return handler(data, context);
82
85
  }
83
86
  async runWebhookFlow(id, data, context) {
87
+ const logger = useLogger();
84
88
  if (!(id in this.webhookFlowHandlers)) {
85
89
  logger.warn(`Couldn't find webhook or manual triggered flow with id "${id}"`);
86
90
  throw new ForbiddenError();
@@ -89,6 +93,7 @@ class FlowManager {
89
93
  return handler(data, context);
90
94
  }
91
95
  async load() {
96
+ const logger = useLogger();
92
97
  const flowsService = new FlowsService({ knex: getDatabase(), schema: await getSchema() });
93
98
  const flows = await flowsService.readByQuery({
94
99
  filter: { status: { _eq: 'active' } },
@@ -305,6 +310,7 @@ class FlowManager {
305
310
  return undefined;
306
311
  }
307
312
  async executeOperation(operation, keyedData, context = {}) {
313
+ const logger = useLogger();
308
314
  if (!this.operations.has(operation.type)) {
309
315
  logger.warn(`Couldn't find operation ${operation.type}`);
310
316
  return { successor: null, status: 'unknown', data: null, options: null };
@@ -314,7 +320,7 @@ class FlowManager {
314
320
  try {
315
321
  let result = await handler(options, {
316
322
  services,
317
- env,
323
+ env: useEnv(),
318
324
  database: getDatabase(),
319
325
  logger,
320
326
  getSchema,
package/dist/logger.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /// <reference types="qs" />
2
2
  import type { RequestHandler } from 'express';
3
- import { type LoggerOptions } from 'pino';
4
- export declare const httpLoggerOptions: LoggerOptions;
5
- declare const logger: import("pino").Logger<never>;
6
- export declare const httpLoggerEnvConfig: Record<string, any>;
7
- export declare const expressLogger: RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
8
- export declare const useLogger: () => import("pino").Logger<never>;
9
- export default logger;
3
+ import { type Logger } from 'pino';
4
+ export declare const _cache: {
5
+ logger: Logger<never> | undefined;
6
+ };
7
+ export declare const useLogger: () => Logger<never>;
8
+ export declare const createLogger: () => Logger<never>;
9
+ export declare const createExpressLogger: () => RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
package/dist/logger.js CHANGED
@@ -1,112 +1,136 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { REDACTED_TEXT, toArray } from '@directus/utils';
2
3
  import { merge } from 'lodash-es';
3
4
  import { URL } from 'node:url';
4
5
  import { pino } from 'pino';
5
6
  import { pinoHttp, stdSerializers } from 'pino-http';
6
- import env from './env.js';
7
7
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
8
- const pinoOptions = {
9
- level: env['LOG_LEVEL'] || 'info',
10
- redact: {
11
- paths: ['req.headers.authorization', 'req.headers.cookie'],
12
- censor: REDACTED_TEXT,
13
- },
14
- };
15
- export const httpLoggerOptions = {
16
- level: env['LOG_LEVEL'] || 'info',
17
- redact: {
18
- paths: ['req.headers.authorization', 'req.headers.cookie'],
19
- censor: REDACTED_TEXT,
20
- },
8
+ export const _cache = { logger: undefined };
9
+ export const useLogger = () => {
10
+ if (_cache.logger) {
11
+ return _cache.logger;
12
+ }
13
+ _cache.logger = createLogger();
14
+ return _cache.logger;
21
15
  };
22
- if (env['LOG_STYLE'] !== 'raw') {
23
- pinoOptions.transport = {
24
- target: 'pino-pretty',
25
- options: {
26
- ignore: 'hostname,pid',
27
- sync: true,
16
+ export const createLogger = () => {
17
+ const env = useEnv();
18
+ const pinoOptions = {
19
+ level: env['LOG_LEVEL'] || 'info',
20
+ redact: {
21
+ paths: ['req.headers.authorization', 'req.headers.cookie'],
22
+ censor: REDACTED_TEXT,
28
23
  },
29
24
  };
30
- httpLoggerOptions.transport = {
31
- target: 'pino-http-print',
32
- options: {
33
- all: true,
34
- translateTime: 'SYS:HH:MM:ss',
35
- relativeUrl: true,
36
- prettyOptions: {
25
+ if (env['LOG_STYLE'] !== 'raw') {
26
+ pinoOptions.transport = {
27
+ target: 'pino-pretty',
28
+ options: {
37
29
  ignore: 'hostname,pid',
38
30
  sync: true,
39
31
  },
32
+ };
33
+ }
34
+ const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
35
+ // Expose custom log levels into formatter function
36
+ if (loggerEnvConfig['levels']) {
37
+ const customLogLevels = {};
38
+ for (const el of toArray(loggerEnvConfig['levels'])) {
39
+ const key_val = el.split(':');
40
+ customLogLevels[key_val[0].trim()] = key_val[1].trim();
41
+ }
42
+ pinoOptions.formatters = {
43
+ level(label, number) {
44
+ return {
45
+ severity: customLogLevels[label] || 'info',
46
+ level: number,
47
+ };
48
+ },
49
+ };
50
+ delete loggerEnvConfig['levels'];
51
+ }
52
+ return pino(merge(pinoOptions, loggerEnvConfig));
53
+ };
54
+ export const createExpressLogger = () => {
55
+ const env = useEnv();
56
+ const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', ['LOGGER_HTTP_LOGGER']);
57
+ const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
58
+ const httpLoggerOptions = {
59
+ level: env['LOG_LEVEL'] || 'info',
60
+ redact: {
61
+ paths: ['req.headers.authorization', 'req.headers.cookie'],
62
+ censor: REDACTED_TEXT,
40
63
  },
41
64
  };
42
- }
43
- if (env['LOG_STYLE'] === 'raw') {
44
- httpLoggerOptions.redact = {
45
- paths: ['req.headers.authorization', 'req.headers.cookie', 'res.headers'],
46
- censor: (value, pathParts) => {
47
- const path = pathParts.join('.');
48
- if (path === 'res.headers') {
49
- if ('set-cookie' in value) {
50
- value['set-cookie'] = REDACTED_TEXT;
65
+ if (env['LOG_STYLE'] !== 'raw') {
66
+ httpLoggerOptions.transport = {
67
+ target: 'pino-http-print',
68
+ options: {
69
+ all: true,
70
+ translateTime: 'SYS:HH:MM:ss',
71
+ relativeUrl: true,
72
+ prettyOptions: {
73
+ ignore: 'hostname,pid',
74
+ sync: true,
75
+ },
76
+ },
77
+ };
78
+ }
79
+ if (env['LOG_STYLE'] === 'raw') {
80
+ httpLoggerOptions.redact = {
81
+ paths: ['req.headers.authorization', 'req.headers.cookie', 'res.headers'],
82
+ censor: (value, pathParts) => {
83
+ const path = pathParts.join('.');
84
+ if (path === 'res.headers') {
85
+ if ('set-cookie' in value) {
86
+ value['set-cookie'] = REDACTED_TEXT;
87
+ }
88
+ return value;
51
89
  }
52
- return value;
53
- }
54
- return REDACTED_TEXT;
55
- },
56
- };
57
- }
58
- const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
59
- // Expose custom log levels into formatter function
60
- if (loggerEnvConfig['levels']) {
61
- const customLogLevels = {};
62
- for (const el of toArray(loggerEnvConfig['levels'])) {
63
- const key_val = el.split(':');
64
- customLogLevels[key_val[0].trim()] = key_val[1].trim();
90
+ return REDACTED_TEXT;
91
+ },
92
+ };
65
93
  }
66
- pinoOptions.formatters = {
67
- level(label, number) {
68
- return {
69
- severity: customLogLevels[label] || 'info',
70
- level: number,
71
- };
72
- },
73
- };
74
- httpLoggerOptions.formatters = {
75
- level(label, number) {
76
- return {
77
- severity: customLogLevels[label] || 'info',
78
- level: number,
79
- };
80
- },
81
- };
82
- delete loggerEnvConfig['levels'];
83
- }
84
- const logger = pino(merge(pinoOptions, loggerEnvConfig));
85
- export const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', ['LOGGER_HTTP_LOGGER']);
86
- if (env['LOG_HTTP_IGNORE_PATHS']) {
87
- const ignorePathsSet = new Set(env['LOG_HTTP_IGNORE_PATHS']);
88
- httpLoggerEnvConfig['autoLogging'] = {
89
- ignore: (req) => {
90
- if (!req.url)
91
- return false;
92
- const { pathname } = new URL(req.url, 'http://example.com/');
93
- return ignorePathsSet.has(pathname);
94
- },
95
- };
96
- }
97
- export const expressLogger = pinoHttp({
98
- logger: pino(merge(httpLoggerOptions, loggerEnvConfig)),
99
- ...httpLoggerEnvConfig,
100
- serializers: {
101
- req(request) {
102
- const output = stdSerializers.req(request);
103
- output.url = redactQuery(output.url);
104
- return output;
94
+ // Expose custom log levels into formatter function
95
+ if (loggerEnvConfig['levels']) {
96
+ const customLogLevels = {};
97
+ for (const el of toArray(loggerEnvConfig['levels'])) {
98
+ const key_val = el.split(':');
99
+ customLogLevels[key_val[0].trim()] = key_val[1].trim();
100
+ }
101
+ httpLoggerOptions.formatters = {
102
+ level(label, number) {
103
+ return {
104
+ severity: customLogLevels[label] || 'info',
105
+ level: number,
106
+ };
107
+ },
108
+ };
109
+ delete loggerEnvConfig['levels'];
110
+ }
111
+ if (env['LOG_HTTP_IGNORE_PATHS']) {
112
+ const ignorePathsSet = new Set(env['LOG_HTTP_IGNORE_PATHS']);
113
+ httpLoggerEnvConfig['autoLogging'] = {
114
+ ignore: (req) => {
115
+ if (!req.url)
116
+ return false;
117
+ const { pathname } = new URL(req.url, 'http://example.com/');
118
+ return ignorePathsSet.has(pathname);
119
+ },
120
+ };
121
+ }
122
+ return pinoHttp({
123
+ logger: pino(merge(httpLoggerOptions, loggerEnvConfig)),
124
+ ...httpLoggerEnvConfig,
125
+ serializers: {
126
+ req(request) {
127
+ const output = stdSerializers.req(request);
128
+ output.url = redactQuery(output.url);
129
+ return output;
130
+ },
105
131
  },
106
- },
107
- });
108
- export const useLogger = () => logger;
109
- export default logger;
132
+ });
133
+ };
110
134
  function redactQuery(originalPath) {
111
135
  const url = new URL(originalPath, 'http://example.com/');
112
136
  if (url.searchParams.has('access_token')) {
package/dist/mailer.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import nodemailer from 'nodemailer';
2
- import env from './env.js';
3
- import logger from './logger.js';
3
+ import { useLogger } from './logger.js';
4
4
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
5
5
  import { createRequire } from 'node:module';
6
6
  const require = createRequire(import.meta.url);
@@ -8,6 +8,8 @@ let transporter;
8
8
  export default function getMailer() {
9
9
  if (transporter)
10
10
  return transporter;
11
+ const env = useEnv();
12
+ const logger = useLogger();
11
13
  const transportName = env['EMAIL_TRANSPORT'].toLowerCase();
12
14
  if (transportName === 'sendmail') {
13
15
  transporter = nodemailer.createTransport({
@@ -1,12 +1,14 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { getCache, getCacheValue } from '../cache.js';
2
- import env from '../env.js';
3
- import logger from '../logger.js';
3
+ import { useLogger } from '../logger.js';
4
4
  import asyncHandler from '../utils/async-handler.js';
5
5
  import { getCacheControlHeader } from '../utils/get-cache-headers.js';
6
6
  import { getCacheKey } from '../utils/get-cache-key.js';
7
7
  import { shouldSkipCache } from '../utils/should-skip-cache.js';
8
8
  const checkCacheMiddleware = asyncHandler(async (req, res, next) => {
9
+ const env = useEnv();
9
10
  const { cache } = getCache();
11
+ const logger = useLogger();
10
12
  if (req.method.toLowerCase() !== 'get' && req.originalUrl?.startsWith('/graphql') === false)
11
13
  return next();
12
14
  if (env['CACHE_ENABLED'] !== true)
@@ -1,18 +1,37 @@
1
- import getDatabase from '../database/index.js';
2
1
  import { InvalidIpError } from '@directus/errors';
2
+ import getDatabase from '../database/index.js';
3
+ import { useLogger } from '../logger.js';
3
4
  import asyncHandler from '../utils/async-handler.js';
5
+ import { ipInNetworks } from '../utils/ip-in-networks.js';
4
6
  export const checkIP = asyncHandler(async (req, _res, next) => {
5
7
  const database = getDatabase();
8
+ const logger = useLogger();
9
+ const { role: roleId, ip } = req.accountability;
6
10
  const query = database.select('ip_access').from('directus_roles');
7
- if (req.accountability.role) {
8
- query.where({ id: req.accountability.role });
11
+ if (roleId) {
12
+ query.where({ id: roleId });
9
13
  }
10
14
  else {
11
15
  query.whereNull('id');
12
16
  }
13
17
  const role = await query.first();
14
- const ipAllowlist = (role?.ip_access || '').split(',').filter((ip) => ip);
15
- if (ipAllowlist.length > 0 && ipAllowlist.includes(req.accountability.ip) === false)
16
- throw new InvalidIpError();
18
+ if (!role?.ip_access)
19
+ return next();
20
+ const ipAllowList = role.ip_access.split(',').filter((ip) => ip);
21
+ if (ipAllowList.length > 0) {
22
+ if (!ip)
23
+ throw new InvalidIpError();
24
+ let allowed;
25
+ try {
26
+ allowed = ipInNetworks(ip, ipAllowList);
27
+ }
28
+ catch (error) {
29
+ logger.warn(`Invalid IP access configuration for role "${roleId}"`);
30
+ logger.warn(error);
31
+ throw new InvalidIpError();
32
+ }
33
+ if (!allowed)
34
+ throw new InvalidIpError();
35
+ }
17
36
  return next();
18
37
  });
@@ -1,6 +1,7 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import cors from 'cors';
2
- import env from '../env.js';
3
3
  let corsMiddleware = (_req, _res, next) => next();
4
+ const env = useEnv();
4
5
  if (env['CORS_ENABLED'] === true) {
5
6
  corsMiddleware = cors({
6
7
  origin: env['CORS_ORIGIN'] || true,
@@ -1,20 +1,20 @@
1
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, MethodNotAllowedError, isDirectusError } from '@directus/errors';
2
2
  import { toArray } from '@directus/utils';
3
+ import { getNodeEnv } from '@directus/utils/node';
3
4
  import getDatabase from '../database/index.js';
4
5
  import emitter from '../emitter.js';
5
- import env from '../env.js';
6
- import { ErrorCode, MethodNotAllowedError } from '@directus/errors';
7
- import logger from '../logger.js';
6
+ import { useLogger } from '../logger.js';
8
7
  // Note: keep all 4 parameters here. That's how Express recognizes it's the error handler, even if
9
8
  // we don't use next
10
9
  const errorHandler = (err, req, res, _next) => {
10
+ const logger = useLogger();
11
11
  let payload = {
12
12
  errors: [],
13
13
  };
14
14
  const errors = toArray(err);
15
15
  let status = null;
16
16
  for (const err of errors) {
17
- if (env['NODE_ENV'] === 'development') {
17
+ if (getNodeEnv() === 'development') {
18
18
  err.extensions = {
19
19
  ...(err.extensions || {}),
20
20
  stack: err.stack,
@@ -1,10 +1,12 @@
1
- import env from '../env.js';
1
+ import { useEnv } from '@directus/env';
2
2
  import { HitRateLimitError } from '@directus/errors';
3
- import logger from '../logger.js';
3
+ import { useLogger } from '../logger.js';
4
4
  import { createRateLimiter } from '../rate-limiter.js';
5
5
  import asyncHandler from '../utils/async-handler.js';
6
6
  import { validateEnv } from '../utils/validate-env.js';
7
7
  const RATE_LIMITER_GLOBAL_KEY = 'global-rate-limit';
8
+ const env = useEnv();
9
+ const logger = useLogger();
8
10
  let checkRateLimit = (_req, _res, next) => next();
9
11
  export let rateLimiterGlobal;
10
12
  if (env['RATE_LIMITER_GLOBAL_ENABLED'] === true) {
@@ -1,4 +1,4 @@
1
- import env from '../env.js';
1
+ import { useEnv } from '@directus/env';
2
2
  import { HitRateLimitError } from '@directus/errors';
3
3
  import { createRateLimiter } from '../rate-limiter.js';
4
4
  import asyncHandler from '../utils/async-handler.js';
@@ -6,6 +6,7 @@ import { getIPFromReq } from '../utils/get-ip-from-req.js';
6
6
  import { validateEnv } from '../utils/validate-env.js';
7
7
  let checkRateLimit = (_req, _res, next) => next();
8
8
  export let rateLimiter;
9
+ const env = useEnv();
9
10
  if (env['RATE_LIMITER_ENABLED'] === true) {
10
11
  validateEnv(['RATE_LIMITER_STORE', 'RATE_LIMITER_DURATION', 'RATE_LIMITER_POINTS']);
11
12
  rateLimiter = createRateLimiter('RATE_LIMITER');