@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,8 +1,8 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { parse as parseBytesConfiguration } from 'bytes';
2
3
  import { assign } from 'lodash-es';
3
4
  import { getCache, setCacheValue } from '../cache.js';
4
- import env from '../env.js';
5
- import logger from '../logger.js';
5
+ import { useLogger } from '../logger.js';
6
6
  import { ExportService } from '../services/import-export/index.js';
7
7
  import { VersionsService } from '../services/versions.js';
8
8
  import asyncHandler from '../utils/async-handler.js';
@@ -12,6 +12,8 @@ import { getDateFormatted } from '../utils/get-date-formatted.js';
12
12
  import { getMilliseconds } from '../utils/get-milliseconds.js';
13
13
  import { stringByteSize } from '../utils/get-string-byte-size.js';
14
14
  export const respond = asyncHandler(async (req, res) => {
15
+ const env = useEnv();
16
+ const logger = useLogger();
15
17
  const { cache } = getCache();
16
18
  let exceedsMaxSize = false;
17
19
  if (env['CACHE_VALUE_MAX_SIZE'] !== false) {
@@ -1,9 +1,10 @@
1
1
  import { defineOperationApi } from '@directus/extensions';
2
2
  import { optionToString } from '@directus/utils';
3
- import logger from '../../logger.js';
3
+ import { useLogger } from '../../logger.js';
4
4
  export default defineOperationApi({
5
5
  id: 'log',
6
6
  handler: ({ message }) => {
7
+ const logger = useLogger();
7
8
  logger.info(optionToString(message));
8
9
  },
9
10
  });
@@ -1,4 +1,5 @@
1
1
  import type { IRateLimiterOptions, IRateLimiterStoreOptions, RateLimiterAbstract } from 'rate-limiter-flexible';
2
+ import { RateLimiterRes } from 'rate-limiter-flexible';
2
3
  type IRateLimiterOptionsOverrides = Partial<IRateLimiterOptions> | Partial<IRateLimiterStoreOptions>;
3
4
  export declare function createRateLimiter(configPrefix?: string, configOverrides?: IRateLimiterOptionsOverrides): RateLimiterAbstract;
4
- export {};
5
+ export { RateLimiterRes };
@@ -1,10 +1,11 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { merge } from 'lodash-es';
2
- import { RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';
3
- import env from './env.js';
3
+ import { RateLimiterMemory, RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible';
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);
7
7
  export function createRateLimiter(configPrefix = 'RATE_LIMITER', configOverrides) {
8
+ const env = useEnv();
8
9
  switch (env['RATE_LIMITER_STORE']) {
9
10
  case 'redis':
10
11
  return new RateLimiterRedis(getConfig('redis', configPrefix, configOverrides));
@@ -13,10 +14,12 @@ export function createRateLimiter(configPrefix = 'RATE_LIMITER', configOverrides
13
14
  return new RateLimiterMemory(getConfig('memory', configPrefix, configOverrides));
14
15
  }
15
16
  }
17
+ export { RateLimiterRes };
16
18
  function getConfig(store = 'memory', configPrefix = 'RATE_LIMITER', overrides) {
17
19
  const config = getConfigFromEnv(`${configPrefix}_`, `${configPrefix}_${store}_`);
18
20
  if (store === 'redis') {
19
21
  const Redis = require('ioredis');
22
+ const env = useEnv();
20
23
  config.storeClient = new Redis(env[`REDIS`] || getConfigFromEnv(`REDIS_`));
21
24
  }
22
25
  delete config.enabled;
@@ -1,2 +1,3 @@
1
- export { useRedis } from './use-redis.js';
2
- export { createRedis } from './create-redis.js';
1
+ export { createRedis } from './lib/create-redis.js';
2
+ export { useRedis } from './lib/use-redis.js';
3
+ export { redisConfigAvailable } from './utils/redis-config-available.js';
@@ -1,2 +1,3 @@
1
- export { useRedis } from './use-redis.js';
2
- export { createRedis } from './create-redis.js';
1
+ export { createRedis } from './lib/create-redis.js';
2
+ export { useRedis } from './lib/use-redis.js';
3
+ export { redisConfigAvailable } from './utils/redis-config-available.js';
@@ -1,6 +1,6 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { Redis } from 'ioredis';
2
- import { useEnv } from '../env.js';
3
- import { getConfigFromEnv } from '../utils/get-config-from-env.js';
3
+ import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
4
4
  /**
5
5
  * Create a new Redis instance based on the global env configuration
6
6
  *
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Check if Redis configuration exists in the current project's environment configuration
3
+ */
4
+ export declare const redisConfigAvailable: () => boolean;
@@ -0,0 +1,8 @@
1
+ import { useEnv } from '@directus/env';
2
+ /**
3
+ * Check if Redis configuration exists in the current project's environment configuration
4
+ */
5
+ export const redisConfigAvailable = () => {
6
+ const env = useEnv();
7
+ return 'REDIS' in env || Object.keys(env).some((key) => key.startsWith('REDIS_'));
8
+ };
@@ -2,9 +2,10 @@ import axios from 'axios';
2
2
  import { lookup } from 'node:dns/promises';
3
3
  import { isIP } from 'node:net';
4
4
  import { URL } from 'node:url';
5
- import logger from '../logger.js';
6
- import { validateIP } from './validate-ip.js';
5
+ import { useLogger } from '../logger.js';
6
+ import { validateIp } from './validate-ip.js';
7
7
  export const requestInterceptor = async (config) => {
8
+ const logger = useLogger();
8
9
  const uri = axios.getUri(config);
9
10
  const { hostname } = new URL(uri);
10
11
  let ip;
@@ -13,14 +14,15 @@ export const requestInterceptor = async (config) => {
13
14
  const dns = await lookup(hostname);
14
15
  ip = dns.address;
15
16
  }
16
- catch (err) {
17
- logger.warn(err, `Couldn't lookup the DNS for url "${uri}"`);
17
+ catch (error) {
18
+ logger.warn(`Couldn't lookup the DNS for URL "${uri}"`);
19
+ logger.warn(error);
18
20
  throw new Error(`Requested URL "${uri}" resolves to a denied IP address`);
19
21
  }
20
22
  }
21
23
  else {
22
24
  ip = hostname;
23
25
  }
24
- await validateIP(ip, uri);
26
+ validateIp(ip, uri);
25
27
  return config;
26
28
  };
@@ -1,5 +1,5 @@
1
- import { validateIP } from './validate-ip.js';
1
+ import { validateIp } from './validate-ip.js';
2
2
  export const responseInterceptor = async (config) => {
3
- await validateIP(config.request.socket.remoteAddress, config.request.url);
3
+ validateIp(config.request.socket.remoteAddress, config.request.url);
4
4
  return config;
5
5
  };
@@ -1 +1 @@
1
- export declare const validateIP: (ip: string, url: string) => Promise<void>;
1
+ export declare function validateIp(ip: string, url: string): void;
@@ -1,19 +1,35 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import os from 'node:os';
2
- import env from '../env.js';
3
- export const validateIP = async (ip, url) => {
4
- if (env['IMPORT_IP_DENY_LIST'].includes(ip)) {
5
- throw new Error(`Requested URL "${url}" resolves to a denied IP address`);
3
+ import { useLogger } from '../logger.js';
4
+ import { ipInNetworks } from '../utils/ip-in-networks.js';
5
+ const deniedError = (url) => new Error(`Requested URL "${url}" resolves to a denied IP address`);
6
+ export function validateIp(ip, url) {
7
+ const env = useEnv();
8
+ const logger = useLogger();
9
+ const ipDenyList = env['IMPORT_IP_DENY_LIST'];
10
+ if (ipDenyList.length === 0)
11
+ return;
12
+ let denied;
13
+ try {
14
+ denied = ipInNetworks(ip, ipDenyList);
6
15
  }
7
- if (env['IMPORT_IP_DENY_LIST'].includes('0.0.0.0')) {
16
+ catch (error) {
17
+ logger.warn(`Invalid "IMPORT_IP_DENY_LIST" configuration`);
18
+ logger.warn(error);
19
+ throw deniedError(url);
20
+ }
21
+ if (denied)
22
+ throw deniedError(url);
23
+ if (ipDenyList.includes('0.0.0.0')) {
8
24
  const networkInterfaces = os.networkInterfaces();
9
25
  for (const networkInfo of Object.values(networkInterfaces)) {
10
26
  if (!networkInfo)
11
27
  continue;
12
28
  for (const info of networkInfo) {
13
29
  if (info.address === ip) {
14
- throw new Error(`Requested URL "${url}" resolves to a denied IP address`);
30
+ throw deniedError(url);
15
31
  }
16
32
  }
17
33
  }
18
34
  }
19
- };
35
+ }
package/dist/server.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { toBoolean } from '@directus/utils';
3
+ import { getNodeEnv } from '@directus/utils/node';
1
4
  import { createTerminus } from '@godaddy/terminus';
2
5
  import * as http from 'http';
3
6
  import * as https from 'https';
@@ -7,13 +10,14 @@ import url from 'url';
7
10
  import createApp from './app.js';
8
11
  import getDatabase from './database/index.js';
9
12
  import emitter from './emitter.js';
10
- import env from './env.js';
11
- import logger from './logger.js';
13
+ import { useLogger } from './logger.js';
12
14
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
13
- import { toBoolean } from './utils/to-boolean.js';
15
+ import { getIPFromReq } from './utils/get-ip-from-req.js';
14
16
  import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
15
17
  import { startWebSocketHandlers } from './websocket/handlers/index.js';
16
18
  export let SERVER_ONLINE = true;
19
+ const env = useEnv();
20
+ const logger = useLogger();
17
21
  export async function createServer() {
18
22
  const server = http.createServer(await createApp());
19
23
  Object.assign(server, getConfigFromEnv('SERVER_'));
@@ -57,7 +61,7 @@ export async function createServer() {
57
61
  size: metrics.out,
58
62
  headers: res.getHeaders(),
59
63
  },
60
- ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress,
64
+ ip: getIPFromReq(req),
61
65
  duration: elapsedMilliseconds.toFixed(),
62
66
  };
63
67
  emitter.emitAction('response', info, {
@@ -86,7 +90,7 @@ export async function createServer() {
86
90
  createTerminus(server, terminusOptions);
87
91
  return server;
88
92
  async function beforeShutdown() {
89
- if (env['NODE_ENV'] !== 'development') {
93
+ if (getNodeEnv() !== 'development') {
90
94
  logger.info('Shutting down...');
91
95
  }
92
96
  SERVER_ONLINE = false;
@@ -104,7 +108,7 @@ export async function createServer() {
104
108
  schema: null,
105
109
  accountability: null,
106
110
  });
107
- if (env['NODE_ENV'] !== 'development') {
111
+ if (getNodeEnv() !== 'development') {
108
112
  logger.info('Directus shut down OK. Bye bye!');
109
113
  }
110
114
  }
@@ -112,7 +116,7 @@ export async function createServer() {
112
116
  export async function startServer() {
113
117
  const server = await createServer();
114
118
  const host = env['HOST'];
115
- const port = env['PORT'];
119
+ const port = parseInt(env['PORT']);
116
120
  server
117
121
  .listen(port, host, () => {
118
122
  logger.info(`Server started at http://${host}:${port}`);
@@ -1,10 +1,9 @@
1
1
  import { Action } from '@directus/constants';
2
- import { isDirectusError } from '@directus/errors';
2
+ import { useEnv } from '@directus/env';
3
+ import { ErrorCode, isDirectusError } from '@directus/errors';
3
4
  import { uniq } from 'lodash-es';
4
5
  import validateUUID from 'uuid-validate';
5
- import env from '../env.js';
6
- import { ErrorCode } from '@directus/errors';
7
- import logger from '../logger.js';
6
+ import { useLogger } from '../logger.js';
8
7
  import { getPermissions } from '../utils/get-permissions.js';
9
8
  import { Url } from '../utils/url.js';
10
9
  import { userName } from '../utils/user-name.js';
@@ -12,6 +11,8 @@ import { AuthorizationService } from './authorization.js';
12
11
  import { ItemsService } from './items.js';
13
12
  import { NotificationsService } from './notifications.js';
14
13
  import { UsersService } from './users.js';
14
+ const env = useEnv();
15
+ const logger = useLogger();
15
16
  export class ActivityService extends ItemsService {
16
17
  notificationsService;
17
18
  usersService;
@@ -5,10 +5,12 @@ import type { Knex } from 'knex';
5
5
  import type { Readable } from 'node:stream';
6
6
  import type { AbstractServiceOptions, TransformationSet } from '../types/index.js';
7
7
  import { AuthorizationService } from './authorization.js';
8
+ import { FilesService } from './files.js';
8
9
  export declare class AssetsService {
9
10
  knex: Knex;
10
11
  accountability: Accountability | null;
11
12
  authorizationService: AuthorizationService;
13
+ filesService: FilesService;
12
14
  constructor(options: AbstractServiceOptions);
13
15
  getAsset(id: string, transformation?: TransformationSet, range?: Range): Promise<{
14
16
  stream: Readable;
@@ -1,3 +1,5 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ForbiddenError, IllegalAssetTransformationError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
1
3
  import { clamp } from 'lodash-es';
2
4
  import { contentType } from 'mime-types';
3
5
  import hash from 'object-hash';
@@ -6,20 +8,23 @@ import sharp from 'sharp';
6
8
  import validateUUID from 'uuid-validate';
7
9
  import { SUPPORTED_IMAGE_TRANSFORM_FORMATS } from '../constants.js';
8
10
  import getDatabase from '../database/index.js';
9
- import env from '../env.js';
10
- import { ForbiddenError, IllegalAssetTransformationError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
11
- import logger from '../logger.js';
11
+ import { useLogger } from '../logger.js';
12
12
  import { getStorage } from '../storage/index.js';
13
13
  import { getMilliseconds } from '../utils/get-milliseconds.js';
14
14
  import * as TransformationUtils from '../utils/transformations.js';
15
15
  import { AuthorizationService } from './authorization.js';
16
+ import { FilesService } from './files.js';
17
+ const env = useEnv();
18
+ const logger = useLogger();
16
19
  export class AssetsService {
17
20
  knex;
18
21
  accountability;
19
22
  authorizationService;
23
+ filesService;
20
24
  constructor(options) {
21
25
  this.knex = options.knex || getDatabase();
22
26
  this.accountability = options.accountability || null;
27
+ this.filesService = new FilesService(options);
23
28
  this.authorizationService = new AuthorizationService(options);
24
29
  }
25
30
  async getAsset(id, transformation, range) {
@@ -40,7 +45,7 @@ export class AssetsService {
40
45
  if (systemPublicKeys.includes(id) === false && this.accountability?.admin !== true) {
41
46
  await this.authorizationService.checkAccess('read', 'directus_files', id);
42
47
  }
43
- const file = (await this.knex.select('*').from('directus_files').where({ id }).first());
48
+ const file = (await this.filesService.readOne(id, { limit: 1 }));
44
49
  if (!file)
45
50
  throw new ForbiddenError();
46
51
  const exists = await storage.location(file.storage).exists(file.filename_disk);
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@directus/constants';
2
+ import { useEnv } from '@directus/env';
3
+ import { InvalidCredentialsError, InvalidOtpError, InvalidProviderError, ServiceUnavailableError, UserSuspendedError, } from '@directus/errors';
2
4
  import jwt from 'jsonwebtoken';
3
5
  import { clone, cloneDeep } from 'lodash-es';
4
6
  import { performance } from 'perf_hooks';
@@ -6,15 +8,13 @@ import { getAuthProvider } from '../auth.js';
6
8
  import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
7
9
  import getDatabase from '../database/index.js';
8
10
  import emitter from '../emitter.js';
9
- import env from '../env.js';
10
- import { InvalidCredentialsError, InvalidProviderError, UserSuspendedError } from '@directus/errors';
11
- import { InvalidOtpError } from '@directus/errors';
12
- import { createRateLimiter } from '../rate-limiter.js';
11
+ import { RateLimiterRes, createRateLimiter } from '../rate-limiter.js';
13
12
  import { getMilliseconds } from '../utils/get-milliseconds.js';
14
13
  import { stall } from '../utils/stall.js';
15
14
  import { ActivityService } from './activity.js';
16
15
  import { SettingsService } from './settings.js';
17
16
  import { TFAService } from './tfa.js';
17
+ const env = useEnv();
18
18
  const loginAttemptsLimiter = createRateLimiter('RATE_LIMITER', { duration: 0 });
19
19
  export class AuthenticationService {
20
20
  knex;
@@ -100,11 +100,19 @@ export class AuthenticationService {
100
100
  try {
101
101
  await loginAttemptsLimiter.consume(user.id);
102
102
  }
103
- catch {
104
- await this.knex('directus_users').update({ status: 'suspended' }).where({ id: user.id });
105
- user.status = 'suspended';
106
- // This means that new attempts after the user has been re-activated will be accepted
107
- await loginAttemptsLimiter.set(user.id, 0, 0);
103
+ catch (error) {
104
+ if (error instanceof RateLimiterRes && error.remainingPoints === 0) {
105
+ await this.knex('directus_users').update({ status: 'suspended' }).where({ id: user.id });
106
+ user.status = 'suspended';
107
+ // This means that new attempts after the user has been re-activated will be accepted
108
+ await loginAttemptsLimiter.set(user.id, 0, 0);
109
+ }
110
+ else {
111
+ throw new ServiceUnavailableError({
112
+ service: 'authentication',
113
+ reason: 'Rate limiter unreachable',
114
+ });
115
+ }
108
116
  }
109
117
  }
110
118
  try {
@@ -1,3 +1,5 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
1
3
  import { createInspector } from '@directus/schema';
2
4
  import { addFieldFlag } from '@directus/utils';
3
5
  import { chunk, groupBy, merge, omit } from 'lodash-es';
@@ -7,12 +9,10 @@ import { getHelpers } from '../database/helpers/index.js';
7
9
  import getDatabase, { getSchemaInspector } from '../database/index.js';
8
10
  import { systemCollectionRows } from '../database/system-data/collections/index.js';
9
11
  import emitter from '../emitter.js';
10
- import env from '../env.js';
11
- import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
12
- import { FieldsService } from './fields.js';
13
- import { ItemsService } from './items.js';
14
12
  import { getSchema } from '../utils/get-schema.js';
15
13
  import { shouldClearCache } from '../utils/should-clear-cache.js';
14
+ import { FieldsService } from './fields.js';
15
+ import { ItemsService } from './items.js';
16
16
  export class CollectionsService {
17
17
  knex;
18
18
  helpers;
@@ -206,6 +206,7 @@ export class CollectionsService {
206
206
  * Read all collections. Currently doesn't support any query.
207
207
  */
208
208
  async readByQuery() {
209
+ const env = useEnv();
209
210
  const collectionItemsService = new ItemsService('directus_collections', {
210
211
  knex: this.knex,
211
212
  schema: this.schema,
@@ -1,28 +1,34 @@
1
1
  import type { ApiOutput, ExtensionSettings } from '@directus/extensions';
2
- import type { SchemaInspector } from '@directus/schema';
3
2
  import type { Accountability, DeepPartial, SchemaOverview } from '@directus/types';
4
- import type Keyv from 'keyv';
5
3
  import type { Knex } from 'knex';
6
- import type { Helpers } from '../database/helpers/index.js';
7
4
  import type { ExtensionManager } from '../extensions/manager.js';
8
5
  import type { AbstractServiceOptions } from '../types/index.js';
9
6
  import { ItemsService } from './items.js';
10
- import { PermissionsService } from './permissions.js';
7
+ export declare class ExtensionReadError extends Error {
8
+ originalError: unknown;
9
+ constructor(originalError: unknown);
10
+ }
11
11
  export declare class ExtensionsService {
12
12
  knex: Knex;
13
- permissionsService: PermissionsService;
14
- schemaInspector: SchemaInspector;
15
13
  accountability: Accountability | null;
16
14
  schema: SchemaOverview;
17
15
  extensionsItemService: ItemsService<ExtensionSettings>;
18
- systemCache: Keyv<any>;
19
- helpers: Helpers;
20
16
  extensionsManager: ExtensionManager;
21
17
  constructor(options: AbstractServiceOptions);
22
18
  readAll(): Promise<ApiOutput[]>;
23
19
  readOne(bundle: string | null, name: string): Promise<ApiOutput>;
24
- updateOne(bundle: string | null, name: string, data: DeepPartial<ApiOutput>): Promise<void>;
20
+ updateOne(bundle: string | null, name: string, data: DeepPartial<ApiOutput>): Promise<ApiOutput>;
25
21
  private getKey;
22
+ /**
23
+ * Sync a bundles enabled status
24
+ * - If the extension or extensions parent is not a bundle changes are skipped
25
+ * - If a bundles status is toggled, all children are set to that status
26
+ * - If an entries status is toggled, then if the:
27
+ * - Parent bundle is non-partial throws UnprocessableContentError
28
+ * - Entry status change resulted in all children being disabled then the parent bundle is disabled
29
+ * - Entry status change resulted in at least one child being enabled then the parent bundle is enabled
30
+ */
31
+ private checkBundleAndSyncStatus;
26
32
  /**
27
33
  * Combine the settings stored in the database with the information available from the installed
28
34
  * extensions into the standardized extensions api output
@@ -1,50 +1,39 @@
1
- import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
- import { createInspector } from '@directus/schema';
3
- import Joi from 'joi';
1
+ import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
2
+ import { isObject } from '@directus/utils';
4
3
  import { omit, pick } from 'lodash-es';
5
- import { getCache } from '../cache.js';
6
- import { getHelpers } from '../database/helpers/index.js';
7
- import getDatabase, { getSchemaInspector } from '../database/index.js';
4
+ import getDatabase from '../database/index.js';
8
5
  import { getExtensionManager } from '../extensions/index.js';
9
6
  import { ItemsService } from './items.js';
10
- import { PermissionsService } from './permissions.js';
7
+ export class ExtensionReadError extends Error {
8
+ originalError;
9
+ constructor(originalError) {
10
+ super();
11
+ this.originalError = originalError;
12
+ }
13
+ }
11
14
  export class ExtensionsService {
12
15
  knex;
13
- permissionsService;
14
- schemaInspector;
15
16
  accountability;
16
17
  schema;
17
18
  extensionsItemService;
18
- systemCache;
19
- helpers;
20
19
  extensionsManager;
21
20
  constructor(options) {
22
21
  this.knex = options.knex || getDatabase();
23
- this.permissionsService = new PermissionsService(options);
24
- this.schemaInspector = options.knex ? createInspector(options.knex) : getSchemaInspector();
25
22
  this.schema = options.schema;
26
23
  this.accountability = options.accountability || null;
27
24
  this.extensionsManager = getExtensionManager();
28
25
  this.extensionsItemService = new ItemsService('directus_extensions', {
29
26
  knex: this.knex,
30
27
  schema: this.schema,
31
- // No accountability here, as every other method is hardcoded to be admin only
28
+ accountability: this.accountability,
32
29
  });
33
- this.systemCache = getCache().systemCache;
34
- this.helpers = getHelpers(this.knex);
35
30
  }
36
31
  async readAll() {
37
- if (this.accountability?.admin !== true) {
38
- throw new ForbiddenError();
39
- }
40
32
  const installedExtensions = this.extensionsManager.getExtensions();
41
33
  const configuredExtensions = await this.extensionsItemService.readByQuery({ limit: -1 });
42
34
  return this.stitch(installedExtensions, configuredExtensions);
43
35
  }
44
36
  async readOne(bundle, name) {
45
- if (this.accountability?.admin !== true) {
46
- throw new ForbiddenError();
47
- }
48
37
  const key = this.getKey(bundle, name);
49
38
  const schema = this.extensionsManager.getExtensions().find((extension) => extension.name === (bundle ?? name));
50
39
  const meta = await this.extensionsItemService.readOne(key);
@@ -54,27 +43,73 @@ export class ExtensionsService {
54
43
  throw new ForbiddenError();
55
44
  }
56
45
  async updateOne(bundle, name, data) {
57
- if (this.accountability?.admin !== true) {
58
- throw new ForbiddenError();
59
- }
60
- const key = this.getKey(bundle, name);
61
- const updateExtensionSchema = Joi.object({
62
- meta: Joi.object({
63
- enabled: Joi.boolean(),
64
- }),
46
+ const result = await this.knex.transaction(async (trx) => {
47
+ if (!isObject(data.meta)) {
48
+ throw new InvalidPayloadError({ reason: `"meta" is required` });
49
+ }
50
+ const service = new ExtensionsService({
51
+ knex: trx,
52
+ accountability: this.accountability,
53
+ schema: this.schema,
54
+ });
55
+ const key = this.getKey(bundle, name);
56
+ await service.extensionsItemService.updateOne(key, data.meta);
57
+ let extension;
58
+ try {
59
+ extension = await service.readOne(bundle, name);
60
+ }
61
+ catch (error) {
62
+ throw new ExtensionReadError(error);
63
+ }
64
+ if ('enabled' in data.meta) {
65
+ await service.checkBundleAndSyncStatus(trx, extension);
66
+ }
67
+ return extension;
65
68
  });
66
- const { error } = updateExtensionSchema.validate(data);
67
- if (error) {
68
- throw new InvalidPayloadError({ reason: error.message });
69
- }
70
- if ('meta' in data && 'enabled' in data.meta) {
71
- await this.knex('directus_extensions').update({ enabled: data.meta.enabled }).where({ name: key });
72
- this.extensionsManager.reload();
73
- }
69
+ this.extensionsManager.reload();
70
+ return result;
74
71
  }
75
72
  getKey(bundle, name) {
76
73
  return bundle ? `${bundle}/${name}` : name;
77
74
  }
75
+ /**
76
+ * Sync a bundles enabled status
77
+ * - If the extension or extensions parent is not a bundle changes are skipped
78
+ * - If a bundles status is toggled, all children are set to that status
79
+ * - If an entries status is toggled, then if the:
80
+ * - Parent bundle is non-partial throws UnprocessableContentError
81
+ * - Entry status change resulted in all children being disabled then the parent bundle is disabled
82
+ * - Entry status change resulted in at least one child being enabled then the parent bundle is enabled
83
+ */
84
+ async checkBundleAndSyncStatus(trx, extension) {
85
+ if (extension.bundle === null) {
86
+ if (extension.schema?.type === 'bundle') {
87
+ await trx('directus_extensions')
88
+ .update({ enabled: extension.meta.enabled })
89
+ .where('name', 'LIKE', this.getKey(extension.name, '%'));
90
+ }
91
+ return;
92
+ }
93
+ const parent = await this.readOne(null, extension.bundle);
94
+ if (parent.schema?.type !== 'bundle') {
95
+ return;
96
+ }
97
+ if (parent.schema.partial === false) {
98
+ throw new UnprocessableContentError({
99
+ reason: 'Unable to toggle status of an entry for a bundle marked as non partial',
100
+ });
101
+ }
102
+ const child = await trx('directus_extensions')
103
+ .where('name', 'LIKE', this.getKey(extension.bundle, '%'))
104
+ .where({ enabled: true })
105
+ .first();
106
+ if (!child && parent.meta.enabled) {
107
+ await trx('directus_extensions').update({ enabled: false }).where({ name: parent.name });
108
+ }
109
+ else if (child && !parent.meta.enabled) {
110
+ await trx('directus_extensions').update({ enabled: true }).where({ name: parent.name });
111
+ }
112
+ }
78
113
  /**
79
114
  * Combine the settings stored in the database with the information available from the installed
80
115
  * extensions into the standardized extensions api output
@@ -128,7 +163,7 @@ export class ExtensionsService {
128
163
  return {
129
164
  name,
130
165
  bundle: bundleName,
131
- schema: schema ? pick(schema, 'type', 'local', 'version') : null,
166
+ schema: schema ? pick(schema, 'type', 'local', 'version', 'partial') : null,
132
167
  meta: omit(meta, 'name'),
133
168
  };
134
169
  });
@@ -319,14 +319,19 @@ export class FieldsService {
319
319
  }
320
320
  if (hookAdjustedField.schema) {
321
321
  const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field);
322
+ if (hookAdjustedField.schema?.is_nullable === true && existingColumn.is_primary_key) {
323
+ throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
324
+ }
322
325
  // Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts
323
326
  const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
324
327
  if (!isEqual(columnToCompare, hookAdjustedField.schema)) {
325
328
  try {
326
- await this.knex.schema.alterTable(collection, (table) => {
327
- if (!hookAdjustedField.schema)
328
- return;
329
- this.addColumnToTable(table, field, existingColumn);
329
+ await this.knex.transaction(async (trx) => {
330
+ await trx.schema.alterTable(collection, async (table) => {
331
+ if (!hookAdjustedField.schema)
332
+ return;
333
+ this.addColumnToTable(table, field, existingColumn);
334
+ });
330
335
  });
331
336
  }
332
337
  catch (err) {