@directus/api 17.1.0 → 18.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 (95) hide show
  1. package/dist/app.js +8 -2
  2. package/dist/auth/drivers/ldap.js +14 -16
  3. package/dist/auth/drivers/local.js +16 -10
  4. package/dist/auth/drivers/oauth2.js +16 -11
  5. package/dist/auth/drivers/openid.js +16 -11
  6. package/dist/auth/drivers/saml.js +27 -12
  7. package/dist/cli/commands/init/index.js +3 -3
  8. package/dist/cli/commands/security/key.js +2 -2
  9. package/dist/cli/utils/create-env/env-stub.liquid +19 -4
  10. package/dist/cli/utils/create-env/index.js +2 -2
  11. package/dist/constants.d.ts +2 -1
  12. package/dist/constants.js +11 -4
  13. package/dist/controllers/auth.js +54 -19
  14. package/dist/controllers/extensions.js +102 -5
  15. package/dist/controllers/items.js +3 -2
  16. package/dist/controllers/permissions.js +1 -1
  17. package/dist/controllers/shares.js +19 -4
  18. package/dist/database/migrations/20220429A-add-flows.js +3 -3
  19. package/dist/database/migrations/20230526A-migrate-translation-strings.js +2 -2
  20. package/dist/database/migrations/20240204A-marketplace.d.ts +3 -0
  21. package/dist/database/migrations/20240204A-marketplace.js +68 -0
  22. package/dist/database/migrations/run.js +3 -2
  23. package/dist/extensions/lib/get-extensions-settings.d.ts +6 -2
  24. package/dist/extensions/lib/get-extensions-settings.js +70 -22
  25. package/dist/extensions/lib/get-extensions.d.ts +5 -1
  26. package/dist/extensions/lib/get-extensions.js +7 -31
  27. package/dist/extensions/lib/installation/index.d.ts +2 -0
  28. package/dist/extensions/lib/installation/index.js +9 -0
  29. package/dist/extensions/lib/installation/manager.d.ts +5 -0
  30. package/dist/extensions/lib/installation/manager.js +90 -0
  31. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +1 -1
  32. package/dist/extensions/lib/sync-extensions.js +11 -10
  33. package/dist/extensions/manager.d.ts +27 -25
  34. package/dist/extensions/manager.js +214 -183
  35. package/dist/middleware/authenticate.d.ts +1 -0
  36. package/dist/middleware/error-handler.js +22 -18
  37. package/dist/middleware/extract-token.d.ts +6 -5
  38. package/dist/middleware/extract-token.js +27 -11
  39. package/dist/middleware/merge-content-versions.d.ts +2 -0
  40. package/dist/middleware/merge-content-versions.js +26 -0
  41. package/dist/middleware/respond.js +0 -12
  42. package/dist/middleware/validate-batch.d.ts +1 -0
  43. package/dist/request/agent-with-ip-validation.d.ts +1 -1
  44. package/dist/request/agent-with-ip-validation.js +5 -1
  45. package/dist/services/activity.js +3 -3
  46. package/dist/services/assets.js +2 -3
  47. package/dist/services/authentication.d.ts +7 -2
  48. package/dist/services/authentication.js +21 -13
  49. package/dist/services/extensions.d.ts +4 -8
  50. package/dist/services/extensions.js +110 -93
  51. package/dist/services/fields.js +28 -22
  52. package/dist/services/graphql/index.js +98 -42
  53. package/dist/services/index.d.ts +1 -1
  54. package/dist/services/index.js +1 -1
  55. package/dist/services/mail/index.d.ts +1 -1
  56. package/dist/services/mail/index.js +4 -2
  57. package/dist/services/payload.js +2 -2
  58. package/dist/services/{permissions.d.ts → permissions/index.d.ts} +3 -4
  59. package/dist/services/{permissions.js → permissions/index.js} +6 -23
  60. package/dist/services/permissions/lib/with-app-minimal-permissions.d.ts +2 -0
  61. package/dist/services/permissions/lib/with-app-minimal-permissions.js +13 -0
  62. package/dist/services/relations.d.ts +2 -3
  63. package/dist/services/relations.js +2 -2
  64. package/dist/services/roles.js +1 -1
  65. package/dist/services/server.js +3 -0
  66. package/dist/services/shares.d.ts +3 -1
  67. package/dist/services/shares.js +9 -5
  68. package/dist/storage/index.js +5 -4
  69. package/dist/types/auth.d.ts +6 -4
  70. package/dist/types/graphql.d.ts +1 -0
  71. package/dist/utils/apply-query.js +3 -3
  72. package/dist/utils/filter-items.d.ts +2 -2
  73. package/dist/utils/filter-items.js +1 -3
  74. package/dist/utils/get-cache-headers.d.ts +1 -0
  75. package/dist/utils/get-cache-key.d.ts +1 -0
  76. package/dist/utils/get-graphql-query-and-variables.d.ts +1 -0
  77. package/dist/utils/get-ip-from-req.d.ts +1 -0
  78. package/dist/utils/get-milliseconds.d.ts +1 -1
  79. package/dist/utils/get-milliseconds.js +4 -1
  80. package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
  81. package/dist/utils/is-login-redirect-allowed.js +34 -0
  82. package/dist/utils/is-url-allowed.d.ts +1 -1
  83. package/dist/utils/is-url-allowed.js +5 -5
  84. package/dist/utils/is-valid-uuid.d.ts +3 -0
  85. package/dist/utils/is-valid-uuid.js +21 -0
  86. package/dist/utils/jwt.d.ts +1 -1
  87. package/dist/utils/jwt.js +3 -3
  88. package/dist/utils/merge-version-data.d.ts +3 -0
  89. package/dist/utils/merge-version-data.js +134 -0
  90. package/dist/utils/sanitize-query.js +2 -0
  91. package/dist/utils/should-skip-cache.d.ts +1 -0
  92. package/dist/utils/validate-keys.js +2 -2
  93. package/dist/utils/validate-query.js +1 -0
  94. package/dist/websocket/controllers/base.js +2 -2
  95. package/package.json +44 -45
@@ -224,13 +224,15 @@ export class FieldsService {
224
224
  accountability: this.accountability,
225
225
  schema: this.schema,
226
226
  });
227
- const hookAdjustedField = await emitter.emitFilter(`fields.create`, field, {
228
- collection: collection,
229
- }, {
230
- database: trx,
231
- schema: this.schema,
232
- accountability: this.accountability,
233
- });
227
+ const hookAdjustedField = opts?.emitEvents !== false
228
+ ? await emitter.emitFilter(`fields.create`, field, {
229
+ collection: collection,
230
+ }, {
231
+ database: trx,
232
+ schema: this.schema,
233
+ accountability: this.accountability,
234
+ })
235
+ : field;
234
236
  if (hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) {
235
237
  if (table) {
236
238
  this.addColumnToTable(table, hookAdjustedField);
@@ -307,14 +309,16 @@ export class FieldsService {
307
309
  field.type = existingType;
308
310
  }
309
311
  try {
310
- const hookAdjustedField = await emitter.emitFilter(`fields.update`, field, {
311
- keys: [field.field],
312
- collection: collection,
313
- }, {
314
- database: this.knex,
315
- schema: this.schema,
316
- accountability: this.accountability,
317
- });
312
+ const hookAdjustedField = opts?.emitEvents !== false
313
+ ? await emitter.emitFilter(`fields.update`, field, {
314
+ keys: [field.field],
315
+ collection: collection,
316
+ }, {
317
+ database: this.knex,
318
+ schema: this.schema,
319
+ accountability: this.accountability,
320
+ })
321
+ : field;
318
322
  const record = field.meta
319
323
  ? await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()
320
324
  : null;
@@ -409,13 +413,15 @@ export class FieldsService {
409
413
  const runPostColumnChange = await this.helpers.schema.preColumnChange();
410
414
  const nestedActionEvents = [];
411
415
  try {
412
- await emitter.emitFilter('fields.delete', [field], {
413
- collection: collection,
414
- }, {
415
- database: this.knex,
416
- schema: this.schema,
417
- accountability: this.accountability,
418
- });
416
+ if (opts?.emitEvents !== false) {
417
+ await emitter.emitFilter('fields.delete', [field], {
418
+ collection: collection,
419
+ }, {
420
+ database: this.knex,
421
+ schema: this.schema,
422
+ accountability: this.accountability,
423
+ });
424
+ }
419
425
  await this.knex.transaction(async (trx) => {
420
426
  const relations = this.schema.relations.filter((relation) => {
421
427
  return ((relation.collection === collection && relation.field === field) ||
@@ -5,14 +5,14 @@ import { parseFilterFunctionPath, toBoolean } from '@directus/utils';
5
5
  import argon2 from 'argon2';
6
6
  import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, NoSchemaIntrospectionCustomRule, execute, specifiedRules, validate, } from 'graphql';
7
7
  import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
8
- import { assign, flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
8
+ import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
9
9
  import { clearSystemCache, getCache } from '../../cache.js';
10
- import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL } from '../../constants.js';
10
+ import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS, } from '../../constants.js';
11
11
  import getDatabase from '../../database/index.js';
12
12
  import { generateHash } from '../../utils/generate-hash.js';
13
13
  import { getGraphQLType } from '../../utils/get-graphql-type.js';
14
- import { getMilliseconds } from '../../utils/get-milliseconds.js';
15
14
  import { getService } from '../../utils/get-service.js';
15
+ import { mergeVersionsRaw, mergeVersionsRecursive } from '../../utils/merge-version-data.js';
16
16
  import { reduceSchema } from '../../utils/reduce-schema.js';
17
17
  import { sanitizeQuery } from '../../utils/sanitize-query.js';
18
18
  import { validateQuery } from '../../utils/validate-query.js';
@@ -41,6 +41,8 @@ import { GraphQLVoid } from './types/void.js';
41
41
  import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
42
42
  import processError from './utils/process-error.js';
43
43
  import { isSystemCollection } from '@directus/system-data';
44
+ import isDirectusJWT from '../../utils/is-directus-jwt.js';
45
+ import { verifyAccessJWT } from '../../utils/jwt.js';
44
46
  const env = useEnv();
45
47
  const validationRules = Array.from(specifiedRules);
46
48
  if (env['GRAPHQL_INTROSPECTION'] === false) {
@@ -372,6 +374,14 @@ export class GraphQLService {
372
374
  },
373
375
  },
374
376
  });
377
+ VersionTypes[relation.collection]?.addFields({
378
+ [relation.field]: {
379
+ type: GraphQLJSON,
380
+ resolve: (obj, _, __, info) => {
381
+ return obj[info?.path?.key ?? relation.field];
382
+ },
383
+ },
384
+ });
375
385
  if (relation.meta?.one_field) {
376
386
  CollectionTypes[relation.related_collection]?.addFields({
377
387
  [relation.meta.one_field]: {
@@ -945,6 +955,7 @@ export class GraphQLService {
945
955
  type: ReadCollectionTypes[collection.collection],
946
956
  args: {
947
957
  id: new GraphQLNonNull(GraphQLID),
958
+ version: GraphQLString,
948
959
  },
949
960
  resolve: async ({ info, context }) => {
950
961
  const result = await self.resolveQuery(info);
@@ -1201,6 +1212,7 @@ export class GraphQLService {
1201
1212
  return null;
1202
1213
  const args = this.parseArgs(info.fieldNodes[0].arguments || [], info.variableValues);
1203
1214
  let query;
1215
+ let versionRaw = false;
1204
1216
  const isAggregate = collection.endsWith('_aggregated') && collection in this.schema.collections === false;
1205
1217
  if (isAggregate) {
1206
1218
  query = this.getAggregateQuery(args, selections);
@@ -1213,6 +1225,7 @@ export class GraphQLService {
1213
1225
  }
1214
1226
  if (collection.endsWith('_by_version') && collection in this.schema.collections === false) {
1215
1227
  collection = collection.slice(0, -11);
1228
+ versionRaw = true;
1216
1229
  }
1217
1230
  }
1218
1231
  if (args['id']) {
@@ -1240,12 +1253,16 @@ export class GraphQLService {
1240
1253
  const saves = await versionsService.getVersionSaves(args['version'], collection, args['id']);
1241
1254
  if (saves) {
1242
1255
  if (this.schema.collections[collection].singleton) {
1243
- return assign(result, ...saves);
1256
+ return versionRaw
1257
+ ? mergeVersionsRaw(result, saves)
1258
+ : mergeVersionsRecursive(result, saves, collection, this.schema);
1244
1259
  }
1245
1260
  else {
1246
1261
  if (result?.[0] === undefined)
1247
1262
  return null;
1248
- return assign(result[0], ...saves);
1263
+ return versionRaw
1264
+ ? mergeVersionsRaw(result[0], saves)
1265
+ : mergeVersionsRecursive(result[0], saves, collection, this.schema);
1249
1266
  }
1250
1267
  }
1251
1268
  }
@@ -1587,6 +1604,7 @@ export class GraphQLService {
1587
1604
  values: {
1588
1605
  json: { value: 'json' },
1589
1606
  cookie: { value: 'cookie' },
1607
+ session: { value: 'session' },
1590
1608
  },
1591
1609
  });
1592
1610
  const ServerInfo = schemaComposer.createObjectTC({
@@ -1789,21 +1807,24 @@ export class GraphQLService {
1789
1807
  accountability: accountability,
1790
1808
  schema: this.schema,
1791
1809
  });
1792
- const result = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, args?.otp);
1793
- if (args['mode'] === 'cookie') {
1794
- res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], {
1795
- httpOnly: true,
1796
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
1797
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
1798
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
1799
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
1800
- });
1810
+ const mode = args['mode'] ?? 'json';
1811
+ const { accessToken, refreshToken, expires } = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, {
1812
+ session: mode === 'session',
1813
+ otp: args?.otp,
1814
+ });
1815
+ const payload = { expires };
1816
+ if (mode === 'json') {
1817
+ payload.refresh_token = refreshToken;
1818
+ payload.access_token = accessToken;
1801
1819
  }
1802
- return {
1803
- access_token: result['accessToken'],
1804
- expires: result['expires'],
1805
- refresh_token: result['refreshToken'],
1806
- };
1820
+ if (mode === 'cookie') {
1821
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
1822
+ payload.access_token = accessToken;
1823
+ }
1824
+ if (mode === 'session') {
1825
+ res?.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
1826
+ }
1827
+ return payload;
1807
1828
  },
1808
1829
  },
1809
1830
  auth_refresh: {
@@ -1826,35 +1847,51 @@ export class GraphQLService {
1826
1847
  accountability: accountability,
1827
1848
  schema: this.schema,
1828
1849
  });
1829
- const currentRefreshToken = args['refresh_token'] || req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1850
+ const mode = args['mode'] ?? 'json';
1851
+ let currentRefreshToken;
1852
+ if (mode === 'json') {
1853
+ currentRefreshToken = args['refresh_token'];
1854
+ }
1855
+ else if (mode === 'cookie') {
1856
+ currentRefreshToken = req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1857
+ }
1858
+ else if (mode === 'session') {
1859
+ const token = req?.cookies[env['SESSION_COOKIE_NAME']];
1860
+ if (isDirectusJWT(token)) {
1861
+ const payload = verifyAccessJWT(token, env['SECRET']);
1862
+ currentRefreshToken = payload.session;
1863
+ }
1864
+ }
1830
1865
  if (!currentRefreshToken) {
1831
1866
  throw new InvalidPayloadError({
1832
- reason: `"refresh_token" is required in either the JSON payload or Cookie`,
1867
+ reason: `The refresh token is required in either the payload or cookie`,
1833
1868
  });
1834
1869
  }
1835
- const result = await authenticationService.refresh(currentRefreshToken);
1836
- if (args['mode'] === 'cookie') {
1837
- res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], {
1838
- httpOnly: true,
1839
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
1840
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
1841
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
1842
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
1843
- });
1870
+ const { accessToken, refreshToken, expires } = await authenticationService.refresh(currentRefreshToken, {
1871
+ session: mode === 'session',
1872
+ });
1873
+ const payload = { expires };
1874
+ if (mode === 'json') {
1875
+ payload.refresh_token = refreshToken;
1876
+ payload.access_token = accessToken;
1844
1877
  }
1845
- return {
1846
- access_token: result['accessToken'],
1847
- expires: result['expires'],
1848
- refresh_token: result['refreshToken'],
1849
- };
1878
+ if (mode === 'cookie') {
1879
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
1880
+ payload.access_token = accessToken;
1881
+ }
1882
+ if (mode === 'session') {
1883
+ res?.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
1884
+ }
1885
+ return payload;
1850
1886
  },
1851
1887
  },
1852
1888
  auth_logout: {
1853
1889
  type: GraphQLBoolean,
1854
1890
  args: {
1855
1891
  refresh_token: GraphQLString,
1892
+ mode: AuthMode,
1856
1893
  },
1857
- resolve: async (_, args, { req }) => {
1894
+ resolve: async (_, args, { req, res }) => {
1858
1895
  const accountability = { role: null };
1859
1896
  if (req?.ip)
1860
1897
  accountability.ip = req.ip;
@@ -1868,13 +1905,33 @@ export class GraphQLService {
1868
1905
  accountability: accountability,
1869
1906
  schema: this.schema,
1870
1907
  });
1871
- const currentRefreshToken = args['refresh_token'] || req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1908
+ const mode = args['mode'] ?? 'json';
1909
+ let currentRefreshToken;
1910
+ if (mode === 'json') {
1911
+ currentRefreshToken = args['refresh_token'];
1912
+ }
1913
+ else if (mode === 'cookie') {
1914
+ currentRefreshToken = req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1915
+ }
1916
+ else if (mode === 'session') {
1917
+ const token = req?.cookies[env['SESSION_COOKIE_NAME']];
1918
+ if (isDirectusJWT(token)) {
1919
+ const payload = verifyAccessJWT(token, env['SECRET']);
1920
+ currentRefreshToken = payload.session;
1921
+ }
1922
+ }
1872
1923
  if (!currentRefreshToken) {
1873
1924
  throw new InvalidPayloadError({
1874
- reason: `"refresh_token" is required in either the JSON payload or Cookie`,
1925
+ reason: `The refresh token is required in either the payload or cookie`,
1875
1926
  });
1876
1927
  }
1877
1928
  await authenticationService.logout(currentRefreshToken);
1929
+ if (req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
1930
+ res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_OPTIONS);
1931
+ }
1932
+ if (req?.cookies[env['SESSION_COOKIE_NAME']]) {
1933
+ res?.clearCookie(env['SESSION_COOKIE_NAME'], SESSION_COOKIE_OPTIONS);
1934
+ }
1878
1935
  return true;
1879
1936
  },
1880
1937
  },
@@ -2479,8 +2536,7 @@ export class GraphQLService {
2479
2536
  update_extensions_item: {
2480
2537
  type: Extension,
2481
2538
  args: {
2482
- bundle: GraphQLString,
2483
- name: new GraphQLNonNull(GraphQLString),
2539
+ id: GraphQLID,
2484
2540
  data: toInputObjectType(schemaComposer.createObjectTC({
2485
2541
  name: 'update_directus_extensions_input',
2486
2542
  fields: {
@@ -2498,8 +2554,8 @@ export class GraphQLService {
2498
2554
  accountability: this.accountability,
2499
2555
  schema: this.schema,
2500
2556
  });
2501
- await extensionsService.updateOne(args['bundle'], args['name'], args['data']);
2502
- return await extensionsService.readOne(args['bundle'], args['name']);
2557
+ await extensionsService.updateOne(args['id'], args['data']);
2558
+ return await extensionsService.readOne(args['id']);
2503
2559
  },
2504
2560
  },
2505
2561
  });
@@ -18,7 +18,7 @@ export * from './notifications.js';
18
18
  export * from './operations.js';
19
19
  export * from './panels.js';
20
20
  export * from './payload.js';
21
- export * from './permissions.js';
21
+ export * from './permissions/index.js';
22
22
  export * from './presets.js';
23
23
  export * from './relations.js';
24
24
  export * from './revisions.js';
@@ -18,7 +18,7 @@ export * from './notifications.js';
18
18
  export * from './operations.js';
19
19
  export * from './panels.js';
20
20
  export * from './payload.js';
21
- export * from './permissions.js';
21
+ export * from './permissions/index.js';
22
22
  export * from './presets.js';
23
23
  export * from './relations.js';
24
24
  export * from './revisions.js';
@@ -14,7 +14,7 @@ export declare class MailService {
14
14
  knex: Knex;
15
15
  mailer: Transporter;
16
16
  constructor(opts: AbstractServiceOptions);
17
- send<T>(options: EmailOptions): Promise<T>;
17
+ send(options: EmailOptions): Promise<void>;
18
18
  private renderTemplate;
19
19
  private getDefaultTemplateData;
20
20
  }
@@ -55,8 +55,10 @@ export class MailService {
55
55
  .map((line) => line.trim())
56
56
  .join('\n');
57
57
  }
58
- const info = await this.mailer.sendMail({ ...emailOptions, from, html });
59
- return info;
58
+ this.mailer.sendMail({ ...emailOptions, from, html }).catch((error) => {
59
+ logger.warn(`Email send failed:`);
60
+ logger.warn(error);
61
+ });
60
62
  }
61
63
  async renderTemplate(template, variables) {
62
64
  const customTemplatePath = path.resolve(getExtensionsPath(), 'templates', template + '.liquid');
@@ -4,7 +4,7 @@ import { format, isValid, parseISO } from 'date-fns';
4
4
  import { unflatten } from 'flat';
5
5
  import Joi from 'joi';
6
6
  import { clone, cloneDeep, isNil, isObject, isPlainObject, omit, pick } from 'lodash-es';
7
- import { v4 as uuid } from 'uuid';
7
+ import { randomUUID } from 'node:crypto';
8
8
  import { parse as wktToGeoJSON } from 'wellknown';
9
9
  import { getHelpers } from '../database/helpers/index.js';
10
10
  import getDatabase from '../database/index.js';
@@ -39,7 +39,7 @@ export class PayloadService {
39
39
  },
40
40
  async uuid({ action, value }) {
41
41
  if (action === 'create' && !value) {
42
- return uuid();
42
+ return randomUUID();
43
43
  }
44
44
  return value;
45
45
  },
@@ -1,14 +1,13 @@
1
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';
4
- import type { QueryOptions } from './items.js';
5
- import { ItemsService } from './items.js';
3
+ import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../../types/index.js';
4
+ import type { QueryOptions } from '../items.js';
5
+ import { ItemsService } from '../items.js';
6
6
  export declare class PermissionsService extends ItemsService {
7
7
  systemCache: Keyv<any>;
8
8
  constructor(options: AbstractServiceOptions);
9
9
  getAllowedFields(action: PermissionsAction, collection?: string): Record<string, string[]>;
10
10
  readByQuery(query: Query, opts?: QueryOptions): Promise<Partial<Item>[]>;
11
- readMany(keys: PrimaryKey[], query?: Query, opts?: QueryOptions): Promise<Partial<Item>[]>;
12
11
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
13
12
  createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
14
13
  updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
@@ -1,9 +1,8 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
- import { clearSystemCache, getCache } from '../cache.js';
3
- import { appAccessMinimalPermissions } from '@directus/system-data';
4
- import { filterItems } from '../utils/filter-items.js';
5
- import { AuthorizationService } from './authorization.js';
6
- import { ItemsService } from './items.js';
2
+ import { clearSystemCache, getCache } from '../../cache.js';
3
+ import { AuthorizationService } from '../authorization.js';
4
+ import { ItemsService } from '../items.js';
5
+ import { withAppMinimalPermissions } from './lib/with-app-minimal-permissions.js';
7
6
  export class PermissionsService extends ItemsService {
8
7
  systemCache;
9
8
  constructor(options) {
@@ -30,24 +29,8 @@ export class PermissionsService extends ItemsService {
30
29
  return fieldsPerCollection;
31
30
  }
32
31
  async readByQuery(query, opts) {
33
- const result = await super.readByQuery(query, opts);
34
- if (Array.isArray(result) && this.accountability && this.accountability.app === true) {
35
- result.push(...filterItems(appAccessMinimalPermissions.map((permission) => ({
36
- ...permission,
37
- role: this.accountability.role,
38
- })), query.filter));
39
- }
40
- return result;
41
- }
42
- async readMany(keys, query = {}, opts) {
43
- const result = await super.readMany(keys, query, opts);
44
- if (this.accountability && this.accountability.app === true) {
45
- result.push(...filterItems(appAccessMinimalPermissions.map((permission) => ({
46
- ...permission,
47
- role: this.accountability.role,
48
- })), query.filter));
49
- }
50
- return result;
32
+ const result = (await super.readByQuery(query, opts));
33
+ return withAppMinimalPermissions(this.accountability, result, query.filter);
51
34
  }
52
35
  async createOne(data, opts) {
53
36
  const res = await super.createOne(data, opts);
@@ -0,0 +1,2 @@
1
+ import type { Accountability, Permission, Query } from '@directus/types';
2
+ export declare function withAppMinimalPermissions(accountability: Accountability | null, permissions: Permission[], filter: Query['filter']): Permission[];
@@ -0,0 +1,13 @@
1
+ import { appAccessMinimalPermissions } from '@directus/system-data';
2
+ import { filterItems } from '../../../utils/filter-items.js';
3
+ import { mergePermissions } from '../../../utils/merge-permissions.js';
4
+ export function withAppMinimalPermissions(accountability, permissions, filter) {
5
+ if (accountability?.app === true) {
6
+ const filteredAppMinimalPermissions = filterItems(appAccessMinimalPermissions.map((permission) => ({
7
+ ...permission,
8
+ role: accountability.role,
9
+ })), filter);
10
+ return mergePermissions('or', permissions, filteredAppMinimalPermissions);
11
+ }
12
+ return permissions;
13
+ }
@@ -4,9 +4,8 @@ import type Keyv from 'keyv';
4
4
  import type { Knex } from 'knex';
5
5
  import type { Helpers } from '../database/helpers/index.js';
6
6
  import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
7
- import type { QueryOptions } from './items.js';
8
- import { ItemsService } from './items.js';
9
- import { PermissionsService } from './permissions.js';
7
+ import { ItemsService, type QueryOptions } from './items.js';
8
+ import { PermissionsService } from './permissions/index.js';
10
9
  export declare class RelationsService {
11
10
  knex: Knex;
12
11
  permissionsService: PermissionsService;
@@ -1,5 +1,6 @@
1
1
  import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
2
  import { createInspector } from '@directus/schema';
3
+ import { systemRelationRows } from '@directus/system-data';
3
4
  import { toArray } from '@directus/utils';
4
5
  import { clearSystemCache, getCache } from '../cache.js';
5
6
  import { getHelpers } from '../database/helpers/index.js';
@@ -8,8 +9,7 @@ import emitter from '../emitter.js';
8
9
  import { getDefaultIndexName } from '../utils/get-default-index-name.js';
9
10
  import { getSchema } from '../utils/get-schema.js';
10
11
  import { ItemsService } from './items.js';
11
- import { PermissionsService } from './permissions.js';
12
- import { systemRelationRows } from '@directus/system-data';
12
+ import { PermissionsService } from './permissions/index.js';
13
13
  export class RelationsService {
14
14
  knex;
15
15
  permissionsService;
@@ -1,7 +1,7 @@
1
1
  import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
2
2
  import { getMatch } from 'ip-matching';
3
3
  import { ItemsService } from './items.js';
4
- import { PermissionsService } from './permissions.js';
4
+ import { PermissionsService } from './permissions/index.js';
5
5
  import { PresetsService } from './presets.js';
6
6
  import { UsersService } from './users.js';
7
7
  export class RolesService extends ItemsService {
@@ -68,6 +68,9 @@ export class ServerService {
68
68
  else {
69
69
  info['rateLimitGlobal'] = false;
70
70
  }
71
+ info['extensions'] = {
72
+ limit: env['EXTENSIONS_LIMIT'] ?? null,
73
+ };
71
74
  info['queryLimit'] = {
72
75
  default: env['QUERY_LIMIT_DEFAULT'],
73
76
  max: Number.isFinite(env['QUERY_LIMIT_MAX']) ? env['QUERY_LIMIT_MAX'] : -1,
@@ -5,7 +5,9 @@ export declare class SharesService extends ItemsService {
5
5
  authorizationService: AuthorizationService;
6
6
  constructor(options: AbstractServiceOptions);
7
7
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
8
- login(payload: Record<string, any>): Promise<LoginResult>;
8
+ login(payload: Record<string, any>, options?: Partial<{
9
+ session: boolean;
10
+ }>): Promise<Omit<LoginResult, 'id'>>;
9
11
  /**
10
12
  * Send a link to the given share ID to the given email(s). Note: you can only send a link to a share
11
13
  * if you have read access to that particular share
@@ -25,7 +25,7 @@ export class SharesService extends ItemsService {
25
25
  await this.authorizationService.checkAccess('share', data['collection'], data['item']);
26
26
  return super.createOne(data, opts);
27
27
  }
28
- async login(payload) {
28
+ async login(payload, options) {
29
29
  const { nanoid } = await import('nanoid');
30
30
  const record = await this.knex
31
31
  .select({
@@ -70,12 +70,16 @@ export class SharesService extends ItemsService {
70
70
  collection: record.share_collection,
71
71
  },
72
72
  };
73
+ const refreshToken = nanoid(64);
74
+ const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
75
+ if (options?.session) {
76
+ tokenPayload.session = refreshToken;
77
+ }
78
+ const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'];
73
79
  const accessToken = jwt.sign(tokenPayload, env['SECRET'], {
74
- expiresIn: env['ACCESS_TOKEN_TTL'],
80
+ expiresIn: TTL,
75
81
  issuer: 'directus',
76
82
  });
77
- const refreshToken = nanoid(64);
78
- const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
79
83
  await this.knex('directus_sessions').insert({
80
84
  token: refreshToken,
81
85
  expires: refreshTokenExpiration,
@@ -88,7 +92,7 @@ export class SharesService extends ItemsService {
88
92
  return {
89
93
  accessToken,
90
94
  refreshToken,
91
- expires: getMilliseconds(env['ACCESS_TOKEN_TTL']),
95
+ expires: getMilliseconds(TTL),
92
96
  };
93
97
  }
94
98
  /**
@@ -9,8 +9,9 @@ export const getStorage = async () => {
9
9
  return _cache.storage;
10
10
  const { StorageManager } = await import('@directus/storage');
11
11
  validateEnv(['STORAGE_LOCATIONS']);
12
- _cache.storage = new StorageManager();
13
- await registerDrivers(_cache.storage);
14
- await registerLocations(_cache.storage);
15
- return _cache.storage;
12
+ const storage = new StorageManager();
13
+ await registerDrivers(storage);
14
+ await registerLocations(storage);
15
+ _cache.storage = storage;
16
+ return storage;
16
17
  };
@@ -27,6 +27,7 @@ export interface Session {
27
27
  export type DirectusTokenPayload = {
28
28
  id?: string;
29
29
  role: string | null;
30
+ session?: string;
30
31
  app_access: boolean | number;
31
32
  admin_access: boolean | number;
32
33
  share?: string;
@@ -47,8 +48,9 @@ export type ShareData = {
47
48
  share_password?: string;
48
49
  };
49
50
  export type LoginResult = {
50
- accessToken: any;
51
- refreshToken: any;
52
- expires: any;
53
- id?: any;
51
+ accessToken: string;
52
+ refreshToken: string;
53
+ expires: number;
54
+ id?: string;
54
55
  };
56
+ export type AuthenticationMode = 'json' | 'cookie' | 'session';
@@ -1,3 +1,4 @@
1
+ /// <reference types="cookie-parser" />
1
2
  import type { Request, Response } from 'express';
2
3
  import type { DocumentNode } from 'graphql';
3
4
  export interface GraphQLParams {
@@ -1,12 +1,12 @@
1
+ import { InvalidQueryError } from '@directus/errors';
1
2
  import { getFilterOperatorsForType, getOutputTypeForFunction } from '@directus/utils';
2
3
  import { clone, isPlainObject } from 'lodash-es';
3
4
  import { customAlphabet } from 'nanoid/non-secure';
4
- import validate from 'uuid-validate';
5
5
  import { getHelpers } from '../database/helpers/index.js';
6
- import { InvalidQueryError } from '@directus/errors';
7
6
  import { getColumnPath } from './get-column-path.js';
8
7
  import { getColumn } from './get-column.js';
9
8
  import { getRelationInfo } from './get-relation-info.js';
9
+ import { isValidUuid } from './is-valid-uuid.js';
10
10
  import { stripFunction } from './strip-function.js';
11
11
  export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
12
12
  /**
@@ -548,7 +548,7 @@ export async function applySearch(schema, dbQuery, searchQuery, collection) {
548
548
  this.orWhere({ [`${collection}.${name}`]: number });
549
549
  }
550
550
  }
551
- else if (field.type === 'uuid' && validate(searchQuery)) {
551
+ else if (field.type === 'uuid' && isValidUuid(searchQuery)) {
552
552
  this.orWhere({ [`${collection}.${name}`]: searchQuery });
553
553
  }
554
554
  });