@directus/api 15.0.0 → 17.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 (185) 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/permissions.js +11 -2
  31. package/dist/controllers/schema.js +3 -2
  32. package/dist/controllers/shares.js +3 -3
  33. package/dist/controllers/utils.js +13 -32
  34. package/dist/database/helpers/index.d.ts +1 -1
  35. package/dist/database/index.js +9 -2
  36. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +3 -1
  37. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -1
  38. package/dist/database/migrations/20210802A-replace-groups.js +2 -1
  39. package/dist/database/migrations/20230721A-require-shares-fields.js +2 -1
  40. package/dist/database/migrations/20231215A-add-focalpoints.d.ts +3 -0
  41. package/dist/database/migrations/20231215A-add-focalpoints.js +12 -0
  42. package/dist/database/migrations/run.js +2 -1
  43. package/dist/database/run-ast.js +5 -2
  44. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -7
  45. package/dist/database/system-data/fields/files.yaml +16 -0
  46. package/dist/emitter.js +3 -1
  47. package/dist/extensions/lib/get-extensions-path.d.ts +1 -1
  48. package/dist/extensions/lib/get-extensions-path.js +2 -1
  49. package/dist/extensions/lib/get-extensions.d.ts +1 -1
  50. package/dist/extensions/lib/get-extensions.js +32 -8
  51. package/dist/extensions/lib/get-shared-deps-mapping.js +6 -4
  52. package/dist/extensions/lib/sandbox/register/call-reference.js +4 -2
  53. package/dist/extensions/lib/sandbox/sdk/generators/log.js +2 -1
  54. package/dist/extensions/lib/sync-extensions.js +6 -4
  55. package/dist/extensions/manager.js +43 -19
  56. package/dist/flows.js +13 -7
  57. package/dist/logger.d.ts +7 -7
  58. package/dist/logger.js +116 -92
  59. package/dist/mailer.js +4 -2
  60. package/dist/middleware/cache.js +4 -2
  61. package/dist/middleware/check-ip.js +25 -6
  62. package/dist/middleware/cors.js +2 -1
  63. package/dist/middleware/error-handler.js +5 -5
  64. package/dist/middleware/rate-limiter-global.js +4 -2
  65. package/dist/middleware/rate-limiter-ip.js +2 -1
  66. package/dist/middleware/respond.js +5 -3
  67. package/dist/operations/log/index.js +2 -1
  68. package/dist/rate-limiter.d.ts +2 -1
  69. package/dist/rate-limiter.js +5 -2
  70. package/dist/redis/index.d.ts +3 -2
  71. package/dist/redis/index.js +3 -2
  72. package/dist/redis/{create-redis.js → lib/create-redis.js} +2 -2
  73. package/dist/redis/utils/redis-config-available.d.ts +4 -0
  74. package/dist/redis/utils/redis-config-available.js +8 -0
  75. package/dist/request/request-interceptor.js +7 -5
  76. package/dist/request/response-interceptor.js +2 -2
  77. package/dist/request/validate-ip.d.ts +1 -1
  78. package/dist/request/validate-ip.js +23 -7
  79. package/dist/server.js +11 -7
  80. package/dist/services/activity.js +5 -4
  81. package/dist/services/assets.d.ts +2 -0
  82. package/dist/services/assets.js +9 -6
  83. package/dist/services/authentication.js +17 -9
  84. package/dist/services/authorization.d.ts +1 -1
  85. package/dist/services/authorization.js +15 -3
  86. package/dist/services/collections.js +5 -4
  87. package/dist/services/extensions.d.ts +15 -9
  88. package/dist/services/extensions.js +74 -39
  89. package/dist/services/fields.js +9 -4
  90. package/dist/services/files.d.ts +2 -2
  91. package/dist/services/files.js +22 -14
  92. package/dist/services/graphql/index.js +46 -3
  93. package/dist/services/graphql/subscription.js +2 -2
  94. package/dist/services/graphql/types/bigint.js +16 -5
  95. package/dist/services/graphql/utils/process-error.d.ts +4 -1
  96. package/dist/services/graphql/utils/process-error.js +10 -8
  97. package/dist/services/{import-export/index.d.ts → import-export.d.ts} +1 -1
  98. package/dist/services/{import-export/index.js → import-export.js} +14 -12
  99. package/dist/services/index.d.ts +1 -1
  100. package/dist/services/index.js +1 -1
  101. package/dist/services/items.js +12 -8
  102. package/dist/services/mail/index.js +4 -2
  103. package/dist/services/notifications.js +7 -3
  104. package/dist/services/permissions.d.ts +3 -2
  105. package/dist/services/permissions.js +76 -1
  106. package/dist/services/relations.js +19 -10
  107. package/dist/services/roles.js +83 -15
  108. package/dist/services/server.js +7 -5
  109. package/dist/services/shares.js +3 -2
  110. package/dist/services/specifications.js +2 -1
  111. package/dist/services/users.js +20 -9
  112. package/dist/services/versions.js +6 -5
  113. package/dist/services/webhooks.d.ts +2 -2
  114. package/dist/services/webhooks.js +2 -2
  115. package/dist/services/websocket.d.ts +1 -1
  116. package/dist/services/websocket.js +4 -3
  117. package/dist/storage/register-drivers.js +2 -1
  118. package/dist/storage/register-locations.js +2 -1
  119. package/dist/synchronization.js +3 -1
  120. package/dist/telemetry/lib/get-report.js +1 -1
  121. package/dist/telemetry/lib/init-telemetry.js +2 -2
  122. package/dist/telemetry/lib/send-report.js +1 -1
  123. package/dist/telemetry/lib/track.js +2 -3
  124. package/dist/telemetry/utils/get-user-count.js +1 -1
  125. package/dist/types/assets.d.ts +2 -0
  126. package/dist/types/items.d.ts +4 -12
  127. package/dist/types/items.js +0 -4
  128. package/dist/utils/apply-diff.js +2 -1
  129. package/dist/utils/apply-query.js +0 -11
  130. package/dist/utils/delete-from-require-cache.js +2 -1
  131. package/dist/utils/get-accountability-for-token.js +3 -2
  132. package/dist/utils/get-auth-providers.js +2 -1
  133. package/dist/utils/get-cache-headers.js +5 -2
  134. package/dist/utils/get-config-from-env.js +2 -1
  135. package/dist/utils/get-default-value.js +4 -3
  136. package/dist/utils/get-ip-from-req.js +4 -2
  137. package/dist/utils/get-permissions.js +5 -3
  138. package/dist/utils/get-schema.js +5 -2
  139. package/dist/utils/get-snapshot-diff.js +7 -9
  140. package/dist/utils/get-snapshot.js +4 -4
  141. package/dist/utils/ip-in-networks.d.ts +6 -0
  142. package/dist/utils/ip-in-networks.js +13 -0
  143. package/dist/utils/is-url-allowed.js +2 -1
  144. package/dist/utils/job-queue.d.ts +1 -0
  145. package/dist/utils/job-queue.js +3 -0
  146. package/dist/utils/sanitize-query.js +7 -2
  147. package/dist/utils/sanitize-schema.d.ts +1 -1
  148. package/dist/utils/should-clear-cache.js +2 -1
  149. package/dist/utils/should-skip-cache.js +2 -1
  150. package/dist/utils/transformations.js +95 -12
  151. package/dist/utils/validate-env.js +4 -2
  152. package/dist/utils/validate-query.js +7 -3
  153. package/dist/utils/validate-storage.js +4 -2
  154. package/dist/webhooks.js +4 -3
  155. package/dist/websocket/controllers/base.js +12 -6
  156. package/dist/websocket/controllers/graphql.js +4 -2
  157. package/dist/websocket/controllers/hooks.js +3 -2
  158. package/dist/websocket/controllers/index.js +4 -2
  159. package/dist/websocket/controllers/rest.js +4 -2
  160. package/dist/websocket/errors.js +2 -1
  161. package/dist/websocket/handlers/heartbeat.js +4 -3
  162. package/dist/websocket/handlers/subscribe.d.ts +2 -2
  163. package/dist/websocket/handlers/subscribe.js +5 -4
  164. package/package.json +57 -57
  165. package/dist/__utils__/items-utils.d.ts +0 -2
  166. package/dist/__utils__/items-utils.js +0 -31
  167. package/dist/__utils__/mock-env.d.ts +0 -18
  168. package/dist/__utils__/mock-env.js +0 -41
  169. package/dist/__utils__/schemas.d.ts +0 -13
  170. package/dist/__utils__/schemas.js +0 -301
  171. package/dist/__utils__/snapshots.d.ts +0 -5
  172. package/dist/__utils__/snapshots.js +0 -903
  173. package/dist/env.d.ts +0 -14
  174. package/dist/env.js +0 -511
  175. package/dist/messenger.d.ts +0 -24
  176. package/dist/messenger.js +0 -64
  177. package/dist/services/import-export/import-worker.d.ts +0 -9
  178. package/dist/services/import-export/import-worker.js +0 -9
  179. package/dist/utils/to-boolean.d.ts +0 -4
  180. package/dist/utils/to-boolean.js +0 -6
  181. package/dist/worker-pool.d.ts +0 -2
  182. package/dist/worker-pool.js +0 -19
  183. /package/dist/redis/{create-redis.d.ts → lib/create-redis.d.ts} +0 -0
  184. /package/dist/redis/{use-redis.d.ts → lib/use-redis.d.ts} +0 -0
  185. /package/dist/redis/{use-redis.js → lib/use-redis.js} +0 -0
@@ -1,8 +1,8 @@
1
- import type { PermissionsAction, Query } from '@directus/types';
1
+ import type { ItemPermissions, PermissionsAction, Query } from '@directus/types';
2
2
  import type Keyv from 'keyv';
3
+ import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
3
4
  import type { QueryOptions } from './items.js';
4
5
  import { ItemsService } from './items.js';
5
- import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
6
6
  export declare class PermissionsService extends ItemsService {
7
7
  systemCache: Keyv<any>;
8
8
  constructor(options: AbstractServiceOptions);
@@ -15,4 +15,5 @@ export declare class PermissionsService extends ItemsService {
15
15
  updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
16
16
  upsertMany(payloads: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
17
17
  deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]>;
18
+ getItemPermissions(collection: string, primaryKey?: string): Promise<ItemPermissions>;
18
19
  }
@@ -1,7 +1,9 @@
1
+ import { ForbiddenError } from '@directus/errors';
1
2
  import { clearSystemCache, getCache } from '../cache.js';
2
3
  import { appAccessMinimalPermissions } from '../database/system-data/app-access-permissions/index.js';
3
- import { ItemsService } from './items.js';
4
4
  import { filterItems } from '../utils/filter-items.js';
5
+ import { AuthorizationService } from './authorization.js';
6
+ import { ItemsService } from './items.js';
5
7
  export class PermissionsService extends ItemsService {
6
8
  systemCache;
7
9
  constructor(options) {
@@ -95,4 +97,77 @@ export class PermissionsService extends ItemsService {
95
97
  }
96
98
  return res;
97
99
  }
100
+ async getItemPermissions(collection, primaryKey) {
101
+ if (!this.accountability?.user)
102
+ throw new ForbiddenError();
103
+ if (this.accountability?.admin) {
104
+ return {
105
+ update: { access: true },
106
+ delete: { access: true },
107
+ share: { access: true },
108
+ };
109
+ }
110
+ const itemPermissions = {
111
+ update: { access: false },
112
+ delete: { access: false },
113
+ share: { access: false },
114
+ };
115
+ let updateAction = 'update';
116
+ const schema = this.schema.collections[collection];
117
+ if (schema?.singleton) {
118
+ const itemsService = new ItemsService(collection, {
119
+ knex: this.knex,
120
+ schema: this.schema,
121
+ });
122
+ const query = {
123
+ fields: [schema.primary],
124
+ limit: 1,
125
+ };
126
+ try {
127
+ const result = await itemsService.readByQuery(query);
128
+ if (!result[0])
129
+ updateAction = 'create';
130
+ }
131
+ catch {
132
+ updateAction = 'create';
133
+ }
134
+ }
135
+ const authorizationService = new AuthorizationService({
136
+ knex: this.knex,
137
+ accountability: this.accountability,
138
+ schema: this.schema,
139
+ });
140
+ await Promise.all(Object.keys(itemPermissions).map((key) => {
141
+ const action = key;
142
+ const checkAction = action === 'update' ? updateAction : action;
143
+ return authorizationService
144
+ .checkAccess(checkAction, collection, primaryKey)
145
+ .then(() => (itemPermissions[action].access = true))
146
+ .catch(() => { });
147
+ }));
148
+ if (schema?.singleton && itemPermissions.update.access) {
149
+ const query = {
150
+ filter: {
151
+ _and: [
152
+ ...(this.accountability?.role ? [{ role: { _eq: this.accountability.role } }] : []),
153
+ { collection: { _eq: collection } },
154
+ { action: { _eq: updateAction } },
155
+ ],
156
+ },
157
+ fields: ['presets', 'fields'],
158
+ };
159
+ try {
160
+ const result = await this.readByQuery(query);
161
+ const permission = result[0];
162
+ if (permission) {
163
+ itemPermissions.update.presets = permission['presets'];
164
+ itemPermissions.update.fields = permission['fields'];
165
+ }
166
+ }
167
+ catch {
168
+ // No permission
169
+ }
170
+ }
171
+ return itemPermissions;
172
+ }
98
173
  }
@@ -1,3 +1,4 @@
1
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
1
2
  import { createInspector } from '@directus/schema';
2
3
  import { toArray } from '@directus/utils';
3
4
  import { clearSystemCache, getCache } from '../cache.js';
@@ -5,7 +6,6 @@ import { getHelpers } from '../database/helpers/index.js';
5
6
  import getDatabase, { getSchemaInspector } from '../database/index.js';
6
7
  import { systemRelationRows } from '../database/system-data/relations/index.js';
7
8
  import emitter from '../emitter.js';
8
- import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
9
9
  import { getDefaultIndexName } from '../utils/get-default-index-name.js';
10
10
  import { getSchema } from '../utils/get-schema.js';
11
11
  import { ItemsService } from './items.js';
@@ -115,16 +115,18 @@ export class RelationsService {
115
115
  if (!relation.field) {
116
116
  throw new InvalidPayloadError({ reason: '"field" is required' });
117
117
  }
118
- if (relation.collection in this.schema.collections === false) {
118
+ const collectionSchema = this.schema.collections[relation.collection];
119
+ if (!collectionSchema) {
119
120
  throw new InvalidPayloadError({ reason: `Collection "${relation.collection}" doesn't exist` });
120
121
  }
121
- if (relation.field in this.schema.collections[relation.collection].fields === false) {
122
+ const fieldSchema = collectionSchema.fields[relation.field];
123
+ if (!fieldSchema) {
122
124
  throw new InvalidPayloadError({
123
125
  reason: `Field "${relation.field}" doesn't exist in collection "${relation.collection}"`,
124
126
  });
125
127
  }
126
128
  // A primary key should not be a foreign key
127
- if (this.schema.collections[relation.collection].primary === relation.field) {
129
+ if (collectionSchema.primary === relation.field) {
128
130
  throw new InvalidPayloadError({
129
131
  reason: `Field "${relation.field}" in collection "${relation.collection}" is a primary key`,
130
132
  });
@@ -151,7 +153,7 @@ export class RelationsService {
151
153
  await this.knex.transaction(async (trx) => {
152
154
  if (relation.related_collection) {
153
155
  await trx.schema.alterTable(relation.collection, async (table) => {
154
- this.alterType(table, relation);
156
+ this.alterType(table, relation, fieldSchema.nullable);
155
157
  const constraintName = getDefaultIndexName('foreign', relation.collection, relation.field);
156
158
  const builder = table
157
159
  .foreign(relation.field, constraintName)
@@ -198,10 +200,12 @@ export class RelationsService {
198
200
  if (this.accountability && this.accountability.admin !== true) {
199
201
  throw new ForbiddenError();
200
202
  }
201
- if (collection in this.schema.collections === false) {
203
+ const collectionSchema = this.schema.collections[collection];
204
+ if (!collectionSchema) {
202
205
  throw new InvalidPayloadError({ reason: `Collection "${collection}" doesn't exist` });
203
206
  }
204
- if (field in this.schema.collections[collection].fields === false) {
207
+ const fieldSchema = collectionSchema.fields[field];
208
+ if (!fieldSchema) {
205
209
  throw new InvalidPayloadError({ reason: `Field "${field}" doesn't exist in collection "${collection}"` });
206
210
  }
207
211
  const existingRelation = this.schema.relations.find((existingRelation) => existingRelation.collection === collection && existingRelation.field === field);
@@ -225,7 +229,7 @@ export class RelationsService {
225
229
  constraintName = this.helpers.schema.constraintName(constraintName);
226
230
  existingRelation.schema.constraint_name = constraintName;
227
231
  }
228
- this.alterType(table, relation);
232
+ this.alterType(table, relation, fieldSchema.nullable);
229
233
  const builder = table
230
234
  .foreign(field, constraintName || undefined)
231
235
  .references(`${existingRelation.related_collection}.${this.schema.collections[existingRelation.related_collection].primary}`);
@@ -447,11 +451,16 @@ export class RelationsService {
447
451
  *
448
452
  * @TODO This is a bit of a hack, and might be better of abstracted elsewhere
449
453
  */
450
- alterType(table, relation) {
454
+ alterType(table, relation, nullable) {
451
455
  const m2oFieldDBType = this.schema.collections[relation.collection].fields[relation.field].dbType;
452
456
  const relatedFieldDBType = this.schema.collections[relation.related_collection].fields[this.schema.collections[relation.related_collection].primary].dbType;
453
457
  if (m2oFieldDBType !== relatedFieldDBType && m2oFieldDBType === 'int' && relatedFieldDBType === 'int unsigned') {
454
- table.specificType(relation.field, 'int unsigned').alter();
458
+ const alterField = table.specificType(relation.field, 'int unsigned');
459
+ // Maintains the non-nullable state
460
+ if (!nullable) {
461
+ alterField.notNullable();
462
+ }
463
+ alterField.alter();
455
464
  }
456
465
  }
457
466
  }
@@ -15,7 +15,7 @@ export class RolesService extends ItemsService {
15
15
  .whereNotIn('id', excludeKeys)
16
16
  .andWhere({ admin_access: true })
17
17
  .first();
18
- const otherAdminRolesCount = +(otherAdminRoles?.count || 0);
18
+ const otherAdminRolesCount = Number(otherAdminRoles?.count ?? 0);
19
19
  if (otherAdminRolesCount === 0) {
20
20
  throw new UnprocessableContentError({ reason: `You can't delete the last admin role` });
21
21
  }
@@ -24,30 +24,98 @@ export class RolesService extends ItemsService {
24
24
  const role = await this.knex.select('admin_access').from('directus_roles').where('id', '=', key).first();
25
25
  if (!role)
26
26
  throw new ForbiddenError();
27
- // The users that will now be in this new non-admin role
28
- let userKeys = [];
27
+ const usersBefore = (await this.knex.select('id').from('directus_users').where('role', '=', key)).map((user) => user.id);
28
+ const usersAdded = [];
29
+ const usersUpdated = [];
30
+ const usersCreated = [];
31
+ const usersRemoved = [];
29
32
  if (Array.isArray(users)) {
30
- userKeys = users.map((user) => (typeof user === 'string' ? user : user['id'])).filter((id) => id);
33
+ const usersKept = [];
34
+ for (const user of users) {
35
+ if (typeof user === 'string') {
36
+ if (usersBefore.includes(user)) {
37
+ usersKept.push(user);
38
+ }
39
+ else {
40
+ usersAdded.push({ id: user });
41
+ }
42
+ }
43
+ else if (user.id) {
44
+ if (usersBefore.includes(user.id)) {
45
+ usersKept.push(user.id);
46
+ usersUpdated.push(user);
47
+ }
48
+ else {
49
+ usersAdded.push(user);
50
+ }
51
+ }
52
+ else {
53
+ usersCreated.push(user);
54
+ }
55
+ }
56
+ usersRemoved.push(...usersBefore.filter((user) => !usersKept.includes(user)));
31
57
  }
32
58
  else {
33
- userKeys = users.update.map((user) => user['id']).filter((id) => id);
59
+ for (const user of users.update) {
60
+ if (usersBefore.includes(user['id'])) {
61
+ usersUpdated.push(user);
62
+ }
63
+ else {
64
+ usersAdded.push(user);
65
+ }
66
+ }
67
+ usersCreated.push(...users.create);
68
+ usersRemoved.push(...users.delete);
69
+ }
70
+ if (role.admin_access === false || role.admin_access === 0) {
71
+ // Admin users might have moved in from other role, thus becoming non-admin
72
+ if (usersAdded.length > 0) {
73
+ const otherAdminUsers = await this.knex
74
+ .count('*', { as: 'count' })
75
+ .from('directus_users')
76
+ .leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
77
+ .whereNotIn('directus_users.id', usersAdded)
78
+ .andWhere({ 'directus_roles.admin_access': true, status: 'active' })
79
+ .first();
80
+ const otherAdminUsersCount = Number(otherAdminUsers?.count ?? 0);
81
+ if (otherAdminUsersCount === 0) {
82
+ throw new UnprocessableContentError({ reason: `You can't remove the last admin user from the admin role` });
83
+ }
84
+ }
85
+ return;
34
86
  }
35
- const usersThatWereInRoleBefore = (await this.knex.select('id').from('directus_users').where('role', '=', key)).map((user) => user.id);
36
- const usersThatAreRemoved = usersThatWereInRoleBefore.filter((id) => Array.isArray(users) ? userKeys.includes(id) === false : users.delete.includes(id) === true);
37
- const usersThatAreAdded = Array.isArray(users) ? users : users.create;
38
- // If the role the users are moved to is an admin-role, and there's at least 1 (new) admin
39
- // user, we don't have to check for other admin
40
- // users
41
- if ((role.admin_access === true || role.admin_access === 1) && usersThatAreAdded.length > 0)
87
+ // Only added or created new users
88
+ if (usersUpdated.length === 0 && usersRemoved.length === 0)
89
+ return;
90
+ // Active admin user(s) about to be created
91
+ if (usersCreated.some((user) => !('status' in user) || user.status === 'active'))
42
92
  return;
93
+ const usersDeactivated = [...usersAdded, ...usersUpdated]
94
+ .filter((user) => 'status' in user && user.status !== 'active')
95
+ .map((user) => user.id);
96
+ const usersAddedNonDeactivated = usersAdded
97
+ .filter((user) => !usersDeactivated.includes(user.id))
98
+ .map((user) => user.id);
99
+ // Active user(s) about to become admin
100
+ if (usersAddedNonDeactivated.length > 0) {
101
+ const userCount = await this.knex
102
+ .count('*', { as: 'count' })
103
+ .from('directus_users')
104
+ .whereIn('id', usersAddedNonDeactivated)
105
+ .andWhere({ status: 'active' })
106
+ .first();
107
+ if (Number(userCount?.count ?? 0) > 0) {
108
+ return;
109
+ }
110
+ }
43
111
  const otherAdminUsers = await this.knex
44
112
  .count('*', { as: 'count' })
45
113
  .from('directus_users')
46
- .whereNotIn('directus_users.id', [...userKeys, ...usersThatAreRemoved])
47
- .andWhere({ 'directus_roles.admin_access': true })
48
114
  .leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
115
+ .whereNotIn('directus_users.id', [...usersDeactivated, ...usersRemoved])
116
+ .andWhere({ 'directus_roles.admin_access': true, status: 'active' })
49
117
  .first();
50
- const otherAdminUsersCount = +(otherAdminUsers?.count || 0);
118
+ const otherAdminUsersCount = Number(otherAdminUsers?.count ?? 0);
51
119
  if (otherAdminUsersCount === 0) {
52
120
  throw new UnprocessableContentError({ reason: `You can't remove the last admin user from the admin role` });
53
121
  }
@@ -1,19 +1,20 @@
1
- import { toArray } from '@directus/utils';
1
+ import { useEnv } from '@directus/env';
2
+ import { toArray, toBoolean } from '@directus/utils';
2
3
  import { version } from 'directus/version';
3
4
  import { merge } from 'lodash-es';
4
5
  import { Readable } from 'node:stream';
5
6
  import { performance } from 'perf_hooks';
6
7
  import { getCache } from '../cache.js';
7
8
  import getDatabase, { hasDatabaseConnection } from '../database/index.js';
8
- import env from '../env.js';
9
- import logger from '../logger.js';
9
+ import { useLogger } from '../logger.js';
10
10
  import getMailer from '../mailer.js';
11
11
  import { rateLimiterGlobal } from '../middleware/rate-limiter-global.js';
12
12
  import { rateLimiter } from '../middleware/rate-limiter-ip.js';
13
13
  import { SERVER_ONLINE } from '../server.js';
14
14
  import { getStorage } from '../storage/index.js';
15
- import { toBoolean } from '../utils/to-boolean.js';
16
15
  import { SettingsService } from './settings.js';
16
+ const env = useEnv();
17
+ const logger = useLogger();
17
18
  export class ServerService {
18
19
  knex;
19
20
  accountability;
@@ -40,7 +41,8 @@ export class ServerService {
40
41
  'theme_dark_overrides',
41
42
  'default_language',
42
43
  'public_foreground',
43
- 'public_background',
44
+ 'public_background.id',
45
+ 'public_background.type',
44
46
  'public_favicon',
45
47
  'public_note',
46
48
  'custom_css',
@@ -1,7 +1,7 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
1
3
  import argon2 from 'argon2';
2
4
  import jwt from 'jsonwebtoken';
3
- import env from '../env.js';
4
- import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
5
5
  import { getMilliseconds } from '../utils/get-milliseconds.js';
6
6
  import { md } from '../utils/md.js';
7
7
  import { Url } from '../utils/url.js';
@@ -10,6 +10,7 @@ import { AuthorizationService } from './authorization.js';
10
10
  import { ItemsService } from './items.js';
11
11
  import { MailService } from './mail/index.js';
12
12
  import { UsersService } from './users.js';
13
+ const env = useEnv();
13
14
  export class SharesService extends ItemsService {
14
15
  authorizationService;
15
16
  constructor(options) {
@@ -1,13 +1,14 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import formatTitle from '@directus/format-title';
2
3
  import { spec } from '@directus/specs';
3
4
  import { version } from 'directus/version';
4
5
  import { cloneDeep, mergeWith } from 'lodash-es';
5
6
  import { OAS_REQUIRED_SCHEMAS } from '../constants.js';
6
7
  import getDatabase from '../database/index.js';
7
- import env from '../env.js';
8
8
  import { getRelationType } from '../utils/get-relation-type.js';
9
9
  import { reduceSchema } from '../utils/reduce-schema.js';
10
10
  import { GraphQLService } from './graphql/index.js';
11
+ const env = useEnv();
11
12
  export class SpecificationService {
12
13
  accountability;
13
14
  knex;
@@ -1,3 +1,5 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ForbiddenError, InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '@directus/errors';
1
3
  import { getSimpleHash, toArray } from '@directus/utils';
2
4
  import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
3
5
  import Joi from 'joi';
@@ -5,9 +7,6 @@ import jwt from 'jsonwebtoken';
5
7
  import { cloneDeep, isEmpty } from 'lodash-es';
6
8
  import { performance } from 'perf_hooks';
7
9
  import getDatabase from '../database/index.js';
8
- import env from '../env.js';
9
- import { ForbiddenError } from '@directus/errors';
10
- import { InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '@directus/errors';
11
10
  import isUrlAllowed from '../utils/is-url-allowed.js';
12
11
  import { verifyJWT } from '../utils/jwt.js';
13
12
  import { stall } from '../utils/stall.js';
@@ -15,6 +14,7 @@ import { Url } from '../utils/url.js';
15
14
  import { ItemsService } from './items.js';
16
15
  import { MailService } from './mail/index.js';
17
16
  import { SettingsService } from './settings.js';
17
+ const env = useEnv();
18
18
  export class UsersService extends ItemsService {
19
19
  constructor(options) {
20
20
  super('directus_users', options);
@@ -213,9 +213,20 @@ export class UsersService extends ItemsService {
213
213
  async updateMany(keys, data, opts) {
214
214
  try {
215
215
  if (data['role']) {
216
- // data['role'] will be an object with id with GraphQL mutations
217
- const roleId = data['role']?.id ?? data['role'];
218
- const newRole = await this.knex.select('admin_access').from('directus_roles').where('id', roleId).first();
216
+ /*
217
+ * data['role'] has the following cases:
218
+ * - a string with existing role id
219
+ * - an object with existing role id for GraphQL mutations
220
+ * - an object with data for new role
221
+ */
222
+ const role = data['role']?.id ?? data['role'];
223
+ let newRole;
224
+ if (typeof role === 'string') {
225
+ newRole = await this.knex.select('admin_access').from('directus_roles').where('id', role).first();
226
+ }
227
+ else {
228
+ newRole = role;
229
+ }
219
230
  if (!newRole?.admin_access) {
220
231
  await this.checkRemainingAdminExistence(keys);
221
232
  }
@@ -325,13 +336,13 @@ export class UsersService extends ItemsService {
325
336
  if (isEmpty(user) || user.status === 'invited') {
326
337
  const subjectLine = subject ?? "You've been invited";
327
338
  await mailService.send({
328
- to: user.email,
339
+ to: user?.email ?? email,
329
340
  subject: subjectLine,
330
341
  template: {
331
342
  name: 'user-invitation',
332
343
  data: {
333
- url: this.inviteUrl(email, url),
334
- email: user.email,
344
+ url: this.inviteUrl(user?.email ?? email, url),
345
+ email: user?.email ?? email,
335
346
  },
336
347
  },
337
348
  });
@@ -172,26 +172,27 @@ export class VersionsService extends ItemsService {
172
172
  knex: this.knex,
173
173
  schema: this.schema,
174
174
  });
175
+ const { item, collection } = version;
175
176
  const activity = await activityService.createOne({
176
177
  action: Action.VERSION_SAVE,
177
178
  user: this.accountability?.user ?? null,
178
- collection: version['collection'],
179
+ collection,
179
180
  ip: this.accountability?.ip ?? null,
180
181
  user_agent: this.accountability?.userAgent ?? null,
181
182
  origin: this.accountability?.origin ?? null,
182
- item: version['item'],
183
+ item,
183
184
  });
184
185
  const revisionDelta = await payloadService.prepareDelta(data);
185
186
  await revisionsService.createOne({
186
187
  activity,
187
188
  version: key,
188
- collection: version['collection'],
189
- item: version['item'],
189
+ collection,
190
+ item,
190
191
  data: revisionDelta,
191
192
  delta: revisionDelta,
192
193
  });
193
194
  const { cache } = getCache();
194
- if (shouldClearCache(cache, undefined, version['collection'])) {
195
+ if (shouldClearCache(cache, undefined, collection)) {
195
196
  cache.clear();
196
197
  }
197
198
  return data;
@@ -1,8 +1,8 @@
1
- import type { Messenger } from '../messenger.js';
1
+ import type { Bus } from '@directus/memory';
2
2
  import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey, Webhook } from '../types/index.js';
3
3
  import { ItemsService } from './items.js';
4
4
  export declare class WebhooksService extends ItemsService<Webhook> {
5
- messenger: Messenger;
5
+ messenger: Bus;
6
6
  constructor(options: AbstractServiceOptions);
7
7
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
8
8
  createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
@@ -1,10 +1,10 @@
1
- import { getMessenger } from '../messenger.js';
1
+ import { useBus } from '../bus/index.js';
2
2
  import { ItemsService } from './items.js';
3
3
  export class WebhooksService extends ItemsService {
4
4
  messenger;
5
5
  constructor(options) {
6
6
  super('directus_webhooks', options);
7
- this.messenger = getMessenger();
7
+ this.messenger = useBus();
8
8
  }
9
9
  async createOne(data, opts) {
10
10
  const result = await super.createOne(data, opts);
@@ -1,6 +1,6 @@
1
1
  import type { ActionHandler } from '@directus/types';
2
- import type { WebSocketClient } from '../websocket/types.js';
3
2
  import type { WebSocketMessage } from '../websocket/messages.js';
3
+ import type { WebSocketClient } from '../websocket/types.js';
4
4
  export declare class WebSocketService {
5
5
  private controller;
6
6
  constructor();
@@ -1,8 +1,9 @@
1
- import { getWebSocketController } from '../websocket/controllers/index.js';
1
+ import { useEnv } from '@directus/env';
2
2
  import { ServiceUnavailableError } from '@directus/errors';
3
- import { toBoolean } from '../utils/to-boolean.js';
3
+ import { toBoolean } from '@directus/utils';
4
4
  import emitter from '../emitter.js';
5
- import env from '../env.js';
5
+ import { getWebSocketController } from '../websocket/controllers/index.js';
6
+ const env = useEnv();
6
7
  export class WebSocketService {
7
8
  controller;
8
9
  constructor() {
@@ -1,6 +1,7 @@
1
- import env from '../env.js';
1
+ import { useEnv } from '@directus/env';
2
2
  import { getStorageDriver } from './get-storage-driver.js';
3
3
  export const registerDrivers = async (storage) => {
4
+ const env = useEnv();
4
5
  const usedDrivers = [];
5
6
  for (const [key, value] of Object.entries(env)) {
6
7
  if ((key.startsWith('STORAGE_') && key.endsWith('_DRIVER')) === false)
@@ -1,7 +1,8 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { toArray } from '@directus/utils';
2
- import env from '../env.js';
3
3
  import { getConfigFromEnv } from '../utils/get-config-from-env.js';
4
4
  export const registerLocations = async (storage) => {
5
+ const env = useEnv();
5
6
  const locations = toArray(env['STORAGE_LOCATIONS']);
6
7
  locations.forEach((location) => {
7
8
  location = location.trim();
@@ -1,10 +1,11 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { Redis } from 'ioredis';
2
- import env from './env.js';
3
3
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
4
4
  let synchronizationManager;
5
5
  function getSynchronizationManager() {
6
6
  if (synchronizationManager)
7
7
  return synchronizationManager;
8
+ const env = useEnv();
8
9
  if (env['SYNCHRONIZATION_STORE'] === 'redis') {
9
10
  synchronizationManager = new SynchronizationManagerRedis();
10
11
  }
@@ -73,6 +74,7 @@ class SynchronizationManagerRedis {
73
74
  namespace;
74
75
  client;
75
76
  constructor() {
77
+ const env = useEnv();
76
78
  const config = getConfigFromEnv('REDIS');
77
79
  this.client = new Redis(env['REDIS'] ?? config);
78
80
  this.namespace = env['SYNCHRONIZATION_NAMESPACE'] ?? 'directus-sync';
@@ -1,6 +1,6 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { version } from 'directus/version';
2
3
  import { getDatabase, getDatabaseClient } from '../../database/index.js';
3
- import { useEnv } from '../../env.js';
4
4
  import { getItemCount } from '../utils/get-item-count.js';
5
5
  import { getUserCount } from '../utils/get-user-count.js';
6
6
  import { getUserItemCount } from '../utils/get-user-item-count.js';
@@ -1,7 +1,7 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { toBoolean } from '@directus/utils';
1
3
  import { getCache } from '../../cache.js';
2
- import { useEnv } from '../../env.js';
3
4
  import { scheduleSynchronizedJob } from '../../utils/schedule.js';
4
- import { toBoolean } from '../../utils/to-boolean.js';
5
5
  import { track } from './track.js';
6
6
  /**
7
7
  * Exported to be able to test the anonymous callback function
@@ -1,5 +1,5 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { URL } from 'node:url';
2
- import { useEnv } from '../../env.js';
3
3
  /**
4
4
  * Post an anonymous usage report to the centralized intake server
5
5
  */
@@ -1,5 +1,5 @@
1
+ import { getNodeEnv } from '@directus/utils/node';
1
2
  import { setTimeout } from 'timers/promises';
2
- import { useEnv } from '../../env.js';
3
3
  import { useLogger } from '../../logger.js';
4
4
  import { getRandomWaitTime } from '../utils/get-random-wait-time.js';
5
5
  import { getReport } from './get-report.js';
@@ -12,7 +12,6 @@ import { sendReport } from './send-report.js';
12
12
  * @returns whether or not the tracking was successful
13
13
  */
14
14
  export const track = async (opts = { wait: true }) => {
15
- const env = useEnv();
16
15
  const logger = useLogger();
17
16
  if (opts.wait) {
18
17
  await setTimeout(getRandomWaitTime());
@@ -23,7 +22,7 @@ export const track = async (opts = { wait: true }) => {
23
22
  return true;
24
23
  }
25
24
  catch (err) {
26
- if (env['NODE_ENV'] === 'development') {
25
+ if (getNodeEnv() === 'development') {
27
26
  logger.error(err);
28
27
  }
29
28
  return false;
@@ -1,5 +1,5 @@
1
+ import { toBoolean } from '@directus/utils';
1
2
  import {} from 'knex';
2
- import { toBoolean } from '../../utils/to-boolean.js';
3
3
  export const getUserCount = async (db) => {
4
4
  const counts = {
5
5
  admin: 0,
@@ -12,6 +12,8 @@ export type TransformationParams = {
12
12
  transforms?: Transformation[];
13
13
  format?: TransformationFormat | 'auto';
14
14
  quality?: number;
15
+ focal_point_x?: number;
16
+ focal_point_y?: number;
15
17
  } & TransformationResize;
16
18
  export type TransformationSet = {
17
19
  transformationParams: TransformationParams;