@directus/api 30.0.0 → 32.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 (197) hide show
  1. package/dist/app.js +7 -0
  2. package/dist/auth/auth.d.ts +2 -1
  3. package/dist/auth/auth.js +7 -2
  4. package/dist/auth/drivers/ldap.d.ts +0 -2
  5. package/dist/auth/drivers/ldap.js +9 -7
  6. package/dist/auth/drivers/oauth2.d.ts +0 -2
  7. package/dist/auth/drivers/oauth2.js +28 -11
  8. package/dist/auth/drivers/openid.d.ts +0 -2
  9. package/dist/auth/drivers/openid.js +28 -11
  10. package/dist/auth/drivers/saml.d.ts +0 -2
  11. package/dist/auth/drivers/saml.js +5 -5
  12. package/dist/auth.js +1 -2
  13. package/dist/cli/commands/bootstrap/index.js +12 -33
  14. package/dist/cli/commands/init/index.js +1 -1
  15. package/dist/cli/commands/schema/apply.d.ts +4 -0
  16. package/dist/cli/commands/schema/apply.js +26 -3
  17. package/dist/controllers/collections.js +7 -2
  18. package/dist/controllers/fields.js +31 -8
  19. package/dist/controllers/mcp.d.ts +2 -0
  20. package/dist/controllers/mcp.js +33 -0
  21. package/dist/controllers/server.js +26 -1
  22. package/dist/controllers/settings.js +9 -2
  23. package/dist/controllers/users.js +17 -7
  24. package/dist/controllers/versions.js +3 -2
  25. package/dist/database/errors/dialects/mssql.d.ts +1 -1
  26. package/dist/database/errors/dialects/mssql.js +18 -10
  27. package/dist/database/helpers/fn/types.js +3 -3
  28. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  29. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  30. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  31. package/dist/database/helpers/schema/dialects/mssql.js +23 -0
  32. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  33. package/dist/database/helpers/schema/dialects/mysql.js +25 -0
  34. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  35. package/dist/database/helpers/schema/dialects/oracle.js +13 -0
  36. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  37. package/dist/database/helpers/schema/dialects/postgres.js +13 -0
  38. package/dist/database/helpers/schema/types.d.ts +5 -0
  39. package/dist/database/helpers/schema/types.js +6 -0
  40. package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
  41. package/dist/database/migrations/20250813A-add-mcp.js +18 -0
  42. package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
  43. package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
  44. package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
  45. package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
  46. package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
  47. package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
  48. package/dist/database/run-ast/README.md +46 -0
  49. package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
  50. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  51. package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
  52. package/dist/database/run-ast/lib/apply-query/index.js +4 -6
  53. package/dist/database/run-ast/lib/apply-query/search.js +2 -0
  54. package/dist/database/run-ast/lib/get-db-query.js +7 -6
  55. package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
  56. package/dist/database/run-ast/utils/generate-alias.js +57 -0
  57. package/dist/flows.js +1 -0
  58. package/dist/mcp/define.d.ts +2 -0
  59. package/dist/mcp/define.js +3 -0
  60. package/dist/mcp/index.d.ts +1 -0
  61. package/dist/mcp/index.js +1 -0
  62. package/dist/mcp/schema.d.ts +485 -0
  63. package/dist/mcp/schema.js +219 -0
  64. package/dist/mcp/server.d.ts +103 -0
  65. package/dist/mcp/server.js +310 -0
  66. package/dist/mcp/tools/assets.d.ts +3 -0
  67. package/dist/mcp/tools/assets.js +54 -0
  68. package/dist/mcp/tools/collections.d.ts +84 -0
  69. package/dist/mcp/tools/collections.js +90 -0
  70. package/dist/mcp/tools/fields.d.ts +101 -0
  71. package/dist/mcp/tools/fields.js +157 -0
  72. package/dist/mcp/tools/files.d.ts +235 -0
  73. package/dist/mcp/tools/files.js +103 -0
  74. package/dist/mcp/tools/flows.d.ts +323 -0
  75. package/dist/mcp/tools/flows.js +85 -0
  76. package/dist/mcp/tools/folders.d.ts +95 -0
  77. package/dist/mcp/tools/folders.js +96 -0
  78. package/dist/mcp/tools/index.d.ts +15 -0
  79. package/dist/mcp/tools/index.js +29 -0
  80. package/dist/mcp/tools/items.d.ts +87 -0
  81. package/dist/mcp/tools/items.js +141 -0
  82. package/dist/mcp/tools/operations.d.ts +171 -0
  83. package/dist/mcp/tools/operations.js +77 -0
  84. package/dist/mcp/tools/prompts/assets.md +8 -0
  85. package/dist/mcp/tools/prompts/collections.md +336 -0
  86. package/dist/mcp/tools/prompts/fields.md +521 -0
  87. package/dist/mcp/tools/prompts/files.md +180 -0
  88. package/dist/mcp/tools/prompts/flows.md +495 -0
  89. package/dist/mcp/tools/prompts/folders.md +34 -0
  90. package/dist/mcp/tools/prompts/index.d.ts +16 -0
  91. package/dist/mcp/tools/prompts/index.js +19 -0
  92. package/dist/mcp/tools/prompts/items.md +317 -0
  93. package/dist/mcp/tools/prompts/operations.md +721 -0
  94. package/dist/mcp/tools/prompts/relations.md +386 -0
  95. package/dist/mcp/tools/prompts/schema.md +130 -0
  96. package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
  97. package/dist/mcp/tools/prompts/system-prompt.md +44 -0
  98. package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
  99. package/dist/mcp/tools/relations.d.ts +73 -0
  100. package/dist/mcp/tools/relations.js +93 -0
  101. package/dist/mcp/tools/schema.d.ts +54 -0
  102. package/dist/mcp/tools/schema.js +317 -0
  103. package/dist/mcp/tools/system.d.ts +3 -0
  104. package/dist/mcp/tools/system.js +22 -0
  105. package/dist/mcp/tools/trigger-flow.d.ts +8 -0
  106. package/dist/mcp/tools/trigger-flow.js +48 -0
  107. package/dist/mcp/transport.d.ts +13 -0
  108. package/dist/mcp/transport.js +18 -0
  109. package/dist/mcp/types.d.ts +56 -0
  110. package/dist/mcp/types.js +1 -0
  111. package/dist/metrics/lib/create-metrics.js +16 -25
  112. package/dist/middleware/collection-exists.js +2 -2
  113. package/dist/operations/mail/index.js +3 -1
  114. package/dist/operations/mail/rate-limiter.d.ts +1 -0
  115. package/dist/operations/mail/rate-limiter.js +29 -0
  116. package/dist/permissions/modules/process-payload/process-payload.js +3 -10
  117. package/dist/permissions/modules/validate-access/validate-access.js +2 -3
  118. package/dist/schedules/metrics.js +6 -2
  119. package/dist/schedules/project.d.ts +4 -0
  120. package/dist/schedules/project.js +27 -0
  121. package/dist/services/authentication.js +36 -0
  122. package/dist/services/collections.d.ts +3 -3
  123. package/dist/services/collections.js +16 -1
  124. package/dist/services/fields.d.ts +21 -5
  125. package/dist/services/fields.js +109 -32
  126. package/dist/services/graphql/resolvers/query.js +1 -1
  127. package/dist/services/graphql/resolvers/system-admin.js +49 -5
  128. package/dist/services/graphql/schema/parse-query.js +8 -8
  129. package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
  130. package/dist/services/graphql/utils/aggregate-query.js +5 -1
  131. package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
  132. package/dist/services/import-export.d.ts +9 -1
  133. package/dist/services/import-export.js +287 -101
  134. package/dist/services/items.d.ts +1 -1
  135. package/dist/services/items.js +50 -24
  136. package/dist/services/mail/index.js +2 -0
  137. package/dist/services/mail/rate-limiter.d.ts +1 -0
  138. package/dist/services/mail/rate-limiter.js +29 -0
  139. package/dist/services/meta.js +28 -24
  140. package/dist/services/payload.d.ts +7 -3
  141. package/dist/services/payload.js +26 -12
  142. package/dist/services/schema.js +4 -1
  143. package/dist/services/server.d.ts +1 -0
  144. package/dist/services/server.js +15 -18
  145. package/dist/services/settings.d.ts +2 -1
  146. package/dist/services/settings.js +15 -0
  147. package/dist/services/tfa.d.ts +1 -1
  148. package/dist/services/tfa.js +20 -5
  149. package/dist/services/tus/server.js +14 -9
  150. package/dist/services/versions.d.ts +6 -4
  151. package/dist/services/versions.js +84 -25
  152. package/dist/telemetry/lib/get-report.js +4 -4
  153. package/dist/telemetry/lib/send-report.d.ts +6 -1
  154. package/dist/telemetry/lib/send-report.js +3 -1
  155. package/dist/telemetry/types/report.d.ts +17 -1
  156. package/dist/telemetry/utils/get-settings.d.ts +9 -0
  157. package/dist/telemetry/utils/get-settings.js +14 -0
  158. package/dist/test-utils/README.md +760 -0
  159. package/dist/test-utils/cache.d.ts +51 -0
  160. package/dist/test-utils/cache.js +59 -0
  161. package/dist/test-utils/database.d.ts +48 -0
  162. package/dist/test-utils/database.js +52 -0
  163. package/dist/test-utils/emitter.d.ts +35 -0
  164. package/dist/test-utils/emitter.js +38 -0
  165. package/dist/test-utils/fields-service.d.ts +28 -0
  166. package/dist/test-utils/fields-service.js +36 -0
  167. package/dist/test-utils/items-service.d.ts +23 -0
  168. package/dist/test-utils/items-service.js +37 -0
  169. package/dist/test-utils/knex.d.ts +164 -0
  170. package/dist/test-utils/knex.js +268 -0
  171. package/dist/test-utils/schema.d.ts +26 -0
  172. package/dist/test-utils/schema.js +35 -0
  173. package/dist/types/auth.d.ts +2 -3
  174. package/dist/utils/apply-diff.js +15 -0
  175. package/dist/utils/create-admin.d.ts +11 -0
  176. package/dist/utils/create-admin.js +50 -0
  177. package/dist/utils/get-schema.js +5 -3
  178. package/dist/utils/get-snapshot-diff.js +49 -5
  179. package/dist/utils/get-snapshot.js +13 -7
  180. package/dist/utils/sanitize-schema.d.ts +11 -4
  181. package/dist/utils/sanitize-schema.js +9 -6
  182. package/dist/utils/schedule.js +15 -19
  183. package/dist/utils/validate-diff.js +31 -0
  184. package/dist/utils/validate-snapshot.js +7 -0
  185. package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
  186. package/dist/utils/versioning/deep-map-with-schema.js +81 -0
  187. package/dist/utils/versioning/handle-version.d.ts +2 -2
  188. package/dist/utils/versioning/handle-version.js +47 -43
  189. package/dist/utils/versioning/split-recursive.d.ts +4 -0
  190. package/dist/utils/versioning/split-recursive.js +27 -0
  191. package/dist/websocket/controllers/hooks.js +12 -20
  192. package/dist/websocket/messages.d.ts +3 -3
  193. package/package.json +65 -66
  194. package/dist/cli/utils/defaults.d.ts +0 -4
  195. package/dist/cli/utils/defaults.js +0 -17
  196. package/dist/telemetry/utils/get-project-id.d.ts +0 -2
  197. package/dist/telemetry/utils/get-project-id.js +0 -4
package/dist/app.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
3
3
  import { handlePressure } from '@directus/pressure';
4
+ import { toBoolean } from '@directus/utils';
4
5
  import cookieParser from 'cookie-parser';
5
6
  import express from 'express';
6
7
  import { merge } from 'lodash-es';
@@ -23,6 +24,7 @@ import flowsRouter from './controllers/flows.js';
23
24
  import foldersRouter from './controllers/folders.js';
24
25
  import graphqlRouter from './controllers/graphql.js';
25
26
  import itemsRouter from './controllers/items.js';
27
+ import mcpRouter from './controllers/mcp.js';
26
28
  import metricsRouter from './controllers/metrics.js';
27
29
  import notFoundHandler from './controllers/not-found.js';
28
30
  import notificationsRouter from './controllers/notifications.js';
@@ -62,6 +64,7 @@ import metricsSchedule from './schedules/metrics.js';
62
64
  import retentionSchedule from './schedules/retention.js';
63
65
  import telemetrySchedule from './schedules/telemetry.js';
64
66
  import tusSchedule from './schedules/tus.js';
67
+ import projectSchedule from './schedules/project.js';
65
68
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
66
69
  import { Url } from './utils/url.js';
67
70
  import { validateStorage } from './utils/validate-storage.js';
@@ -225,6 +228,9 @@ export default async function createApp() {
225
228
  app.use('/flows', flowsRouter);
226
229
  app.use('/folders', foldersRouter);
227
230
  app.use('/items', itemsRouter);
231
+ if (toBoolean(env['MCP_ENABLED']) === true) {
232
+ app.use('/mcp', mcpRouter);
233
+ }
228
234
  if (env['METRICS_ENABLED'] === true) {
229
235
  app.use('/metrics', metricsRouter);
230
236
  }
@@ -257,6 +263,7 @@ export default async function createApp() {
257
263
  await telemetrySchedule();
258
264
  await tusSchedule();
259
265
  await metricsSchedule();
266
+ await projectSchedule();
260
267
  await emitter.emitInit('app.after', { app });
261
268
  return app;
262
269
  }
@@ -1,10 +1,11 @@
1
1
  import type { SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
+ import { UsersService } from '../services/users.js';
3
4
  import type { AuthDriverOptions, User } from '../types/index.js';
4
5
  export declare abstract class AuthDriver {
5
6
  knex: Knex;
6
- schema: SchemaOverview;
7
7
  constructor(options: AuthDriverOptions, _config: Record<string, any>);
8
+ protected getUsersService(schema: SchemaOverview): UsersService;
8
9
  /**
9
10
  * Get user id for a given provider payload
10
11
  *
package/dist/auth/auth.js CHANGED
@@ -1,9 +1,14 @@
1
+ import { UsersService } from '../services/users.js';
1
2
  export class AuthDriver {
2
3
  knex;
3
- schema;
4
4
  constructor(options, _config) {
5
5
  this.knex = options.knex;
6
- this.schema = options.schema;
6
+ }
7
+ getUsersService(schema) {
8
+ return new UsersService({
9
+ knex: this.knex,
10
+ schema,
11
+ });
7
12
  }
8
13
  /**
9
14
  * Check with the (external) provider if the user is allowed entry to Directus
@@ -1,11 +1,9 @@
1
1
  import { Router } from 'express';
2
2
  import type { Client } from 'ldapjs';
3
- import { UsersService } from '../../services/users.js';
4
3
  import type { AuthDriverOptions, User } from '../../types/index.js';
5
4
  import { AuthDriver } from '../auth.js';
6
5
  export declare class LDAPAuthDriver extends AuthDriver {
7
6
  bindClient: Client;
8
- usersService: UsersService;
9
7
  config: Record<string, any>;
10
8
  constructor(options: AuthDriverOptions, config: Record<string, any>);
11
9
  private validateBindClient;
@@ -10,17 +10,16 @@ import { useLogger } from '../../logger/index.js';
10
10
  import { respond } from '../../middleware/respond.js';
11
11
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
12
12
  import { AuthenticationService } from '../../services/authentication.js';
13
- import { UsersService } from '../../services/users.js';
14
13
  import asyncHandler from '../../utils/async-handler.js';
15
14
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
16
15
  import { AuthDriver } from '../auth.js';
16
+ import { getSchema } from '../../utils/get-schema.js';
17
17
  // 0x2: ACCOUNTDISABLE
18
18
  // 0x10: LOCKOUT
19
19
  // 0x800000: PASSWORD_EXPIRED
20
20
  const INVALID_ACCOUNT_FLAGS = 0x800012;
21
21
  export class LDAPAuthDriver extends AuthDriver {
22
22
  bindClient;
23
- usersService;
24
23
  config;
25
24
  constructor(options, config) {
26
25
  super(options, config);
@@ -39,7 +38,6 @@ export class LDAPAuthDriver extends AuthDriver {
39
38
  this.bindClient.on('error', (err) => {
40
39
  logger.warn(err);
41
40
  });
42
- this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
43
41
  this.config = config;
44
42
  }
45
43
  async validateBindClient() {
@@ -216,9 +214,11 @@ export class LDAPAuthDriver extends AuthDriver {
216
214
  email: userInfo.email,
217
215
  };
218
216
  }
219
- const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema: this.schema, accountability: null });
217
+ const schema = await getSchema();
218
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema, accountability: null });
220
219
  // Update user to update properties that might have changed
221
- await this.usersService.updateOne(userId, updatedUserPayload);
220
+ const usersService = this.getUsersService(schema);
221
+ await usersService.updateOne(userId, updatedUserPayload);
222
222
  return userId;
223
223
  }
224
224
  if (!userInfo) {
@@ -232,11 +232,13 @@ export class LDAPAuthDriver extends AuthDriver {
232
232
  external_identifier: userInfo.dn,
233
233
  role: userRole?.id ?? defaultRoleId,
234
234
  };
235
+ const schema = await getSchema();
235
236
  // Run hook so the end user has the chance to augment the
236
237
  // user that is about to be created
237
- const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema: this.schema, accountability: null });
238
+ const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema, accountability: null });
238
239
  try {
239
- await this.usersService.createOne(updatedUserPayload);
240
+ const usersService = this.getUsersService(schema);
241
+ await usersService.createOne(updatedUserPayload);
240
242
  }
241
243
  catch (e) {
242
244
  if (isDirectusError(e, ErrorCode.RecordNotUnique)) {
@@ -1,13 +1,11 @@
1
1
  import { Router } from 'express';
2
2
  import type { Client } from 'openid-client';
3
- import { UsersService } from '../../services/users.js';
4
3
  import type { AuthDriverOptions, User } from '../../types/index.js';
5
4
  import type { RoleMap } from '../../types/rolemap.js';
6
5
  import { LocalAuthDriver } from './local.js';
7
6
  export declare class OAuth2AuthDriver extends LocalAuthDriver {
8
7
  client: Client;
9
8
  redirectUrl: string;
10
- usersService: UsersService;
11
9
  config: Record<string, any>;
12
10
  roleMap: RoleMap;
13
11
  constructor(options: AuthDriverOptions, config: Record<string, any>);
@@ -13,7 +13,6 @@ import { useLogger } from '../../logger/index.js';
13
13
  import { respond } from '../../middleware/respond.js';
14
14
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
15
15
  import { AuthenticationService } from '../../services/authentication.js';
16
- import { UsersService } from '../../services/users.js';
17
16
  import asyncHandler from '../../utils/async-handler.js';
18
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
19
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
@@ -22,10 +21,10 @@ import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js
22
21
  import { verifyJWT } from '../../utils/jwt.js';
23
22
  import { Url } from '../../utils/url.js';
24
23
  import { LocalAuthDriver } from './local.js';
24
+ import { getSchema } from '../../utils/get-schema.js';
25
25
  export class OAuth2AuthDriver extends LocalAuthDriver {
26
26
  client;
27
27
  redirectUrl;
28
- usersService;
29
28
  config;
30
29
  roleMap;
31
30
  constructor(options, config) {
@@ -39,7 +38,6 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
39
38
  }
40
39
  const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', additionalConfig['provider'], 'callback');
41
40
  this.redirectUrl = redirectUrl.toString();
42
- this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
43
41
  this.config = additionalConfig;
44
42
  this.roleMap = {};
45
43
  const roleMapping = this.config['roleMapping'];
@@ -177,14 +175,16 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
177
175
  email: userPayload.email,
178
176
  };
179
177
  }
178
+ const schema = await getSchema();
180
179
  const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
181
180
  identifier,
182
181
  provider: this.config['provider'],
183
182
  providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
184
- }, { database: getDatabase(), schema: this.schema, accountability: null });
183
+ }, { database: getDatabase(), schema, accountability: null });
185
184
  // Update user to update refresh_token and other properties that might have changed
186
185
  if (Object.values(updatedUserPayload).some((value) => value !== undefined)) {
187
- await this.usersService.updateOne(userId, updatedUserPayload);
186
+ const usersService = this.getUsersService(schema);
187
+ await usersService.updateOne(userId, updatedUserPayload);
188
188
  }
189
189
  return userId;
190
190
  }
@@ -193,15 +193,17 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
193
193
  logger.warn(`[OAuth2] User doesn't exist, and public registration not allowed for provider "${provider}"`);
194
194
  throw new InvalidCredentialsError();
195
195
  }
196
+ const schema = await getSchema();
196
197
  // Run hook so the end user has the chance to augment the
197
198
  // user that is about to be created
198
199
  const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
199
200
  identifier,
200
201
  provider: this.config['provider'],
201
202
  providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
202
- }, { database: getDatabase(), schema: this.schema, accountability: null });
203
+ }, { database: getDatabase(), schema, accountability: null });
203
204
  try {
204
- await this.usersService.createOne(updatedUserPayload);
205
+ const usersService = this.getUsersService(schema);
206
+ await usersService.createOne(updatedUserPayload);
205
207
  }
206
208
  catch (e) {
207
209
  if (isDirectusError(e, ErrorCode.RecordNotUnique)) {
@@ -231,7 +233,8 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
231
233
  const tokenSet = await this.client.refresh(authData['refreshToken']);
232
234
  // Update user refreshToken if provided
233
235
  if (tokenSet.refresh_token) {
234
- await this.usersService.updateOne(user.id, {
236
+ const usersService = this.getUsersService(await getSchema());
237
+ await usersService.updateOne(user.id, {
235
238
  auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
236
239
  });
237
240
  }
@@ -273,10 +276,11 @@ export function createOAuth2AuthRouter(providerName) {
273
276
  const codeVerifier = provider.generateCodeVerifier();
274
277
  const prompt = !!req.query['prompt'];
275
278
  const redirect = req.query['redirect'];
279
+ const otp = req.query['otp'];
276
280
  if (isLoginRedirectAllowed(redirect, providerName) === false) {
277
281
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
278
282
  }
279
- const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
283
+ const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
280
284
  expiresIn: '5m',
281
285
  issuer: 'directus',
282
286
  });
@@ -299,7 +303,8 @@ export function createOAuth2AuthRouter(providerName) {
299
303
  logger.warn(e, `[OAuth2] Couldn't verify OAuth2 cookie`);
300
304
  throw new InvalidCredentialsError();
301
305
  }
302
- const { verifier, redirect, prompt } = tokenData;
306
+ const { verifier, prompt, otp } = tokenData;
307
+ let { redirect } = tokenData;
303
308
  const accountability = createDefaultAccountability({
304
309
  ip: getIPFromReq(req),
305
310
  });
@@ -321,7 +326,7 @@ export function createOAuth2AuthRouter(providerName) {
321
326
  code: req.query['code'],
322
327
  codeVerifier: verifier,
323
328
  state: req.query['state'],
324
- }, { session: authMode === 'session' });
329
+ }, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
325
330
  }
326
331
  catch (error) {
327
332
  // Prompt user for a new refresh_token if invalidated
@@ -342,6 +347,18 @@ export function createOAuth2AuthRouter(providerName) {
342
347
  throw error;
343
348
  }
344
349
  const { accessToken, refreshToken, expires } = authResponse;
350
+ try {
351
+ const claims = verifyJWT(accessToken, getSecret());
352
+ if (claims?.enforce_tfa === true) {
353
+ const url = new Url(env['PUBLIC_URL']).addPath('admin', 'tfa-setup');
354
+ if (redirect)
355
+ url.setQuery('redirect', redirect);
356
+ redirect = url.toString();
357
+ }
358
+ }
359
+ catch (e) {
360
+ logger.warn(e, `[OAuth2] Unexpected error during OAuth2 login`);
361
+ }
345
362
  if (redirect) {
346
363
  if (authMode === 'session') {
347
364
  res.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
@@ -1,13 +1,11 @@
1
1
  import { Router } from 'express';
2
2
  import type { Client } from 'openid-client';
3
- import { UsersService } from '../../services/users.js';
4
3
  import type { AuthDriverOptions, User } from '../../types/index.js';
5
4
  import type { RoleMap } from '../../types/rolemap.js';
6
5
  import { LocalAuthDriver } from './local.js';
7
6
  export declare class OpenIDAuthDriver extends LocalAuthDriver {
8
7
  client: null | Client;
9
8
  redirectUrl: string;
10
- usersService: UsersService;
11
9
  config: Record<string, any>;
12
10
  roleMap: RoleMap;
13
11
  constructor(options: AuthDriverOptions, config: Record<string, any>);
@@ -13,7 +13,6 @@ import { useLogger } from '../../logger/index.js';
13
13
  import { respond } from '../../middleware/respond.js';
14
14
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
15
15
  import { AuthenticationService } from '../../services/authentication.js';
16
- import { UsersService } from '../../services/users.js';
17
16
  import asyncHandler from '../../utils/async-handler.js';
18
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
19
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
@@ -22,10 +21,10 @@ import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js
22
21
  import { verifyJWT } from '../../utils/jwt.js';
23
22
  import { Url } from '../../utils/url.js';
24
23
  import { LocalAuthDriver } from './local.js';
24
+ import { getSchema } from '../../utils/get-schema.js';
25
25
  export class OpenIDAuthDriver extends LocalAuthDriver {
26
26
  client;
27
27
  redirectUrl;
28
- usersService;
29
28
  config;
30
29
  roleMap;
31
30
  constructor(options, config) {
@@ -40,7 +39,6 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
40
39
  }
41
40
  const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', provider, 'callback');
42
41
  this.redirectUrl = redirectUrl.toString();
43
- this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
44
42
  this.config = config;
45
43
  this.roleMap = {};
46
44
  const roleMapping = this.config['roleMapping'];
@@ -227,14 +225,16 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
227
225
  email: userPayload.email,
228
226
  };
229
227
  }
228
+ const schema = await getSchema();
230
229
  const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
231
230
  identifier,
232
231
  provider: this.config['provider'],
233
232
  providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
234
- }, { database: getDatabase(), schema: this.schema, accountability: null });
233
+ }, { database: getDatabase(), schema, accountability: null });
235
234
  // Update user to update refresh_token and other properties that might have changed
236
235
  if (Object.values(updatedUserPayload).some((value) => value !== undefined)) {
237
- await this.usersService.updateOne(userId, updatedUserPayload);
236
+ const usersService = this.getUsersService(schema);
237
+ await usersService.updateOne(userId, updatedUserPayload);
238
238
  }
239
239
  return userId;
240
240
  }
@@ -244,15 +244,17 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
244
244
  logger.warn(`[OpenID] User doesn't exist, and public registration not allowed for provider "${provider}"`);
245
245
  throw new InvalidCredentialsError();
246
246
  }
247
+ const schema = await getSchema();
247
248
  // Run hook so the end user has the chance to augment the
248
249
  // user that is about to be created
249
250
  const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
250
251
  identifier,
251
252
  provider: this.config['provider'],
252
253
  providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
253
- }, { database: getDatabase(), schema: this.schema, accountability: null });
254
+ }, { database: getDatabase(), schema, accountability: null });
254
255
  try {
255
- await this.usersService.createOne(updatedUserPayload);
256
+ const usersService = this.getUsersService(schema);
257
+ await usersService.createOne(updatedUserPayload);
256
258
  }
257
259
  catch (e) {
258
260
  if (isDirectusError(e, ErrorCode.RecordNotUnique)) {
@@ -283,7 +285,8 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
283
285
  const tokenSet = await client.refresh(authData['refreshToken']);
284
286
  // Update user refreshToken if provided
285
287
  if (tokenSet.refresh_token) {
286
- await this.usersService.updateOne(user.id, {
288
+ const usersService = this.getUsersService(await getSchema());
289
+ await usersService.updateOne(user.id, {
287
290
  auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
288
291
  });
289
292
  }
@@ -325,10 +328,11 @@ export function createOpenIDAuthRouter(providerName) {
325
328
  const codeVerifier = provider.generateCodeVerifier();
326
329
  const prompt = !!req.query['prompt'];
327
330
  const redirect = req.query['redirect'];
331
+ const otp = req.query['otp'];
328
332
  if (isLoginRedirectAllowed(redirect, providerName) === false) {
329
333
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
330
334
  }
331
- const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
335
+ const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
332
336
  expiresIn: (env[`AUTH_${providerName.toUpperCase()}_LOGIN_TIMEOUT`] ?? '5m'),
333
337
  issuer: 'directus',
334
338
  });
@@ -361,7 +365,8 @@ export function createOpenIDAuthRouter(providerName) {
361
365
  const url = new Url(env['PUBLIC_URL']).addPath('admin', 'login');
362
366
  return res.redirect(`${url.toString()}?reason=${ErrorCode.InvalidCredentials}`);
363
367
  }
364
- const { verifier, redirect, prompt } = tokenData;
368
+ const { verifier, prompt, otp } = tokenData;
369
+ let { redirect } = tokenData;
365
370
  const accountability = createDefaultAccountability({ ip: getIPFromReq(req) });
366
371
  const userAgent = req.get('user-agent')?.substring(0, 1024);
367
372
  if (userAgent)
@@ -382,7 +387,7 @@ export function createOpenIDAuthRouter(providerName) {
382
387
  codeVerifier: verifier,
383
388
  state: req.query['state'],
384
389
  iss: req.query['iss'],
385
- }, { session: authMode === 'session' });
390
+ }, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
386
391
  }
387
392
  catch (error) {
388
393
  // Prompt user for a new refresh_token if invalidated
@@ -404,6 +409,18 @@ export function createOpenIDAuthRouter(providerName) {
404
409
  throw error;
405
410
  }
406
411
  const { accessToken, refreshToken, expires } = authResponse;
412
+ try {
413
+ const claims = verifyJWT(accessToken, getSecret());
414
+ if (claims?.enforce_tfa === true) {
415
+ const url = new Url(env['PUBLIC_URL']).addPath('admin', 'tfa-setup');
416
+ if (redirect)
417
+ url.setQuery('redirect', redirect);
418
+ redirect = url.toString();
419
+ }
420
+ }
421
+ catch (e) {
422
+ logger.warn(e, `[OpenID] Unexpected error during OpenID login`);
423
+ }
407
424
  if (redirect) {
408
425
  if (authMode === 'session') {
409
426
  res.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
@@ -1,11 +1,9 @@
1
1
  import * as samlify from 'samlify';
2
- import { UsersService } from '../../services/users.js';
3
2
  import type { AuthDriverOptions, User } from '../../types/index.js';
4
3
  import { LocalAuthDriver } from './local.js';
5
4
  export declare class SAMLAuthDriver extends LocalAuthDriver {
6
5
  sp: samlify.ServiceProviderInstance;
7
6
  idp: samlify.IdentityProviderInstance;
8
- usersService: UsersService;
9
7
  config: Record<string, any>;
10
8
  constructor(options: AuthDriverOptions, config: Record<string, any>);
11
9
  fetchUserID(identifier: string): Promise<any>;
@@ -10,22 +10,20 @@ import emitter from '../../emitter.js';
10
10
  import { useLogger } from '../../logger/index.js';
11
11
  import { respond } from '../../middleware/respond.js';
12
12
  import { AuthenticationService } from '../../services/authentication.js';
13
- import { UsersService } from '../../services/users.js';
14
13
  import asyncHandler from '../../utils/async-handler.js';
15
14
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
16
15
  import { LocalAuthDriver } from './local.js';
17
16
  import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
17
+ import { getSchema } from '../../utils/get-schema.js';
18
18
  // Register the samlify schema validator
19
19
  samlify.setSchemaValidator(validator);
20
20
  export class SAMLAuthDriver extends LocalAuthDriver {
21
21
  sp;
22
22
  idp;
23
- usersService;
24
23
  config;
25
24
  constructor(options, config) {
26
25
  super(options, config);
27
26
  this.config = config;
28
- this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
29
27
  this.sp = samlify.ServiceProvider(getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_SP`));
30
28
  this.idp = samlify.IdentityProvider(getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_IDP`));
31
29
  }
@@ -63,11 +61,13 @@ export class SAMLAuthDriver extends LocalAuthDriver {
63
61
  external_identifier: identifier.toLowerCase(),
64
62
  role: this.config['defaultRoleId'],
65
63
  };
64
+ const schema = await getSchema();
66
65
  // Run hook so the end user has the chance to augment the
67
66
  // user that is about to be created
68
- const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, { identifier: identifier.toLowerCase(), provider: this.config['provider'], providerPayload: { ...payload } }, { database: getDatabase(), schema: this.schema, accountability: null });
67
+ const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, { identifier: identifier.toLowerCase(), provider: this.config['provider'], providerPayload: { ...payload } }, { database: getDatabase(), schema, accountability: null });
69
68
  try {
70
- return await this.usersService.createOne(updatedUserPayload);
69
+ const usersService = this.getUsersService(schema);
70
+ return await usersService.createOne(updatedUserPayload);
71
71
  }
72
72
  catch (error) {
73
73
  if (isDirectusError(error, ErrorCode.RecordNotUnique)) {
package/dist/auth.js CHANGED
@@ -6,7 +6,6 @@ import { DEFAULT_AUTH_PROVIDER } from './constants.js';
6
6
  import getDatabase from './database/index.js';
7
7
  import { useLogger } from './logger/index.js';
8
8
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
9
- import { getSchema } from './utils/get-schema.js';
10
9
  const providers = new Map();
11
10
  export function getAuthProvider(provider) {
12
11
  const logger = useLogger();
@@ -19,7 +18,7 @@ export function getAuthProvider(provider) {
19
18
  export async function registerAuthProviders() {
20
19
  const env = useEnv();
21
20
  const logger = useLogger();
22
- const options = { knex: getDatabase(), schema: await getSchema() };
21
+ const options = { knex: getDatabase() };
23
22
  const providerNames = toArray(env['AUTH_PROVIDERS']);
24
23
  // Register default provider if not disabled
25
24
  if (!env['AUTH_DISABLE_DEFAULT']) {
@@ -3,13 +3,10 @@ import getDatabase, { hasDatabaseConnection, isInstalled, validateDatabaseConnec
3
3
  import runMigrations from '../../../database/migrations/run.js';
4
4
  import installDatabase from '../../../database/seeds/run.js';
5
5
  import { useLogger } from '../../../logger/index.js';
6
- import { AccessService } from '../../../services/access.js';
7
- import { PoliciesService } from '../../../services/policies.js';
8
- import { RolesService } from '../../../services/roles.js';
9
6
  import { SettingsService } from '../../../services/settings.js';
10
- import { UsersService } from '../../../services/users.js';
11
7
  import { getSchema } from '../../../utils/get-schema.js';
12
- import { defaultAdminPolicy, defaultAdminRole, defaultAdminUser } from '../../utils/defaults.js';
8
+ import { createAdmin } from '../../../utils/create-admin.js';
9
+ import { email } from 'zod';
13
10
  export default async function bootstrap({ skipAdminInit }) {
14
11
  const logger = useLogger();
15
12
  logger.info('Initializing bootstrap...');
@@ -23,15 +20,23 @@ export default async function bootstrap({ skipAdminInit }) {
23
20
  await runMigrations(database, 'latest');
24
21
  const schema = await getSchema();
25
22
  if (skipAdminInit == null) {
26
- await createDefaultAdmin(schema);
23
+ await createAdmin(schema);
27
24
  }
28
25
  else {
29
26
  logger.info('Skipping creation of default Admin user and role...');
30
27
  }
28
+ const settingsService = new SettingsService({ schema });
31
29
  if (env['PROJECT_NAME'] && typeof env['PROJECT_NAME'] === 'string' && env['PROJECT_NAME'].length > 0) {
32
- const settingsService = new SettingsService({ schema });
33
30
  await settingsService.upsertSingleton({ project_name: env['PROJECT_NAME'] });
34
31
  }
32
+ if (email().safeParse(env['PROJECT_OWNER']).success) {
33
+ await settingsService.setOwner({
34
+ project_owner: env['PROJECT_OWNER'],
35
+ org_name: null,
36
+ project_usage: null,
37
+ product_updates: false,
38
+ });
39
+ }
35
40
  }
36
41
  else {
37
42
  logger.info('Database already initialized, skipping install');
@@ -55,29 +60,3 @@ async function waitForDatabase(database) {
55
60
  await validateDatabaseConnection(database);
56
61
  return database;
57
62
  }
58
- async function createDefaultAdmin(schema) {
59
- const logger = useLogger();
60
- const env = useEnv();
61
- const { nanoid } = await import('nanoid');
62
- logger.info('Setting up first admin role...');
63
- const accessService = new AccessService({ schema });
64
- const policiesService = new PoliciesService({ schema });
65
- const rolesService = new RolesService({ schema });
66
- const role = await rolesService.createOne(defaultAdminRole);
67
- const policy = await policiesService.createOne(defaultAdminPolicy);
68
- await accessService.createOne({ policy, role });
69
- logger.info('Adding first admin user...');
70
- const usersService = new UsersService({ schema });
71
- let adminEmail = env['ADMIN_EMAIL'];
72
- if (!adminEmail) {
73
- logger.info('No admin email provided. Defaulting to "admin@example.com"');
74
- adminEmail = 'admin@example.com';
75
- }
76
- let adminPassword = env['ADMIN_PASSWORD'];
77
- if (!adminPassword) {
78
- adminPassword = nanoid(12);
79
- logger.info(`No admin password provided. Defaulting to "${adminPassword}"`);
80
- }
81
- const token = env['ADMIN_TOKEN'] ?? null;
82
- await usersService.createOne({ ...defaultAdminUser, email: adminEmail, password: adminPassword, token, role });
83
- }
@@ -9,7 +9,7 @@ import runSeed from '../../../database/seeds/run.js';
9
9
  import { generateHash } from '../../../utils/generate-hash.js';
10
10
  import createDBConnection from '../../utils/create-db-connection.js';
11
11
  import createEnv from '../../utils/create-env/index.js';
12
- import { defaultAdminPolicy, defaultAdminRole, defaultAdminUser } from '../../utils/defaults.js';
12
+ import { defaultAdminPolicy, defaultAdminRole, defaultAdminUser } from '../../../utils/create-admin.js';
13
13
  import { drivers, getDriverForClient } from '../../utils/drivers.js';
14
14
  import { databaseQuestions } from './questions.js';
15
15
  export default async function init() {
@@ -1,5 +1,9 @@
1
+ import type { SnapshotDiff } from '@directus/types';
2
+ export declare function filterSnapshotDiff(snapshot: SnapshotDiff, filters: string[]): SnapshotDiff;
1
3
  export declare function apply(snapshotPath: string, options?: {
2
4
  yes: boolean;
3
5
  dryRun: boolean;
4
6
  ignoreRules: string;
5
7
  }): Promise<void>;
8
+ export declare function formatPath(path: any[]): string;
9
+ export declare function formatRelatedCollection(relatedCollection: string | null): string;