directus 9.15.1 → 9.17.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 (107) hide show
  1. package/dist/__utils__/items-utils.d.ts +2 -0
  2. package/dist/__utils__/items-utils.js +36 -0
  3. package/dist/__utils__/schemas.d.ts +13 -0
  4. package/dist/__utils__/schemas.js +304 -0
  5. package/dist/__utils__/snapshots.d.ts +5 -0
  6. package/dist/__utils__/snapshots.js +897 -0
  7. package/dist/app.js +8 -7
  8. package/dist/auth/drivers/ldap.js +1 -0
  9. package/dist/auth/drivers/local.js +6 -0
  10. package/dist/auth/drivers/oauth2.js +1 -0
  11. package/dist/auth/drivers/openid.js +1 -0
  12. package/dist/cli/index.test.d.ts +1 -0
  13. package/dist/cli/index.test.js +58 -0
  14. package/dist/cli/utils/create-env/env-stub.liquid +6 -2
  15. package/dist/controllers/activity.js +1 -0
  16. package/dist/controllers/assets.js +20 -16
  17. package/dist/controllers/auth.js +6 -9
  18. package/dist/controllers/files.test.d.ts +1 -0
  19. package/dist/controllers/files.test.js +49 -0
  20. package/dist/controllers/server.js +0 -1
  21. package/dist/database/migrations/20220826A-add-origin-to-accountability.d.ts +3 -0
  22. package/dist/database/migrations/20220826A-add-origin-to-accountability.js +21 -0
  23. package/dist/database/migrations/run.test.d.ts +1 -0
  24. package/dist/database/migrations/run.test.js +92 -0
  25. package/dist/database/system-data/fields/activity.yaml +6 -0
  26. package/dist/database/system-data/fields/sessions.yaml +2 -0
  27. package/dist/env.js +15 -0
  28. package/dist/env.test.d.ts +8 -0
  29. package/dist/env.test.js +39 -0
  30. package/dist/extensions.d.ts +1 -0
  31. package/dist/extensions.js +16 -3
  32. package/dist/flows.js +28 -17
  33. package/dist/mailer.js +1 -0
  34. package/dist/middleware/authenticate.d.ts +1 -1
  35. package/dist/middleware/authenticate.js +1 -0
  36. package/dist/middleware/authenticate.test.d.ts +1 -0
  37. package/dist/middleware/authenticate.test.js +214 -0
  38. package/dist/middleware/extract-token.test.d.ts +1 -0
  39. package/dist/middleware/extract-token.test.js +60 -0
  40. package/dist/middleware/validate-batch.d.ts +1 -2
  41. package/dist/middleware/validate-batch.js +10 -13
  42. package/dist/middleware/validate-batch.test.d.ts +1 -0
  43. package/dist/middleware/validate-batch.test.js +82 -0
  44. package/dist/operations/exec/index.d.ts +5 -0
  45. package/dist/operations/exec/index.js +26 -0
  46. package/dist/operations/exec/index.test.d.ts +1 -0
  47. package/dist/operations/exec/index.test.js +95 -0
  48. package/dist/operations/notification/index.js +9 -6
  49. package/dist/operations/request/index.js +22 -3
  50. package/dist/operations/transform/index.d.ts +1 -1
  51. package/dist/operations/transform/index.js +1 -1
  52. package/dist/services/authentication.js +13 -3
  53. package/dist/services/files.js +3 -2
  54. package/dist/services/files.test.d.ts +1 -0
  55. package/dist/services/files.test.js +53 -0
  56. package/dist/services/flows.js +4 -0
  57. package/dist/services/graphql/index.d.ts +2 -2
  58. package/dist/services/graphql/index.js +78 -75
  59. package/dist/services/items.js +98 -42
  60. package/dist/services/items.test.d.ts +1 -0
  61. package/dist/services/items.test.js +765 -0
  62. package/dist/services/payload.d.ts +7 -4
  63. package/dist/services/payload.js +63 -12
  64. package/dist/services/payload.test.d.ts +1 -0
  65. package/dist/services/payload.test.js +94 -0
  66. package/dist/services/server.js +10 -7
  67. package/dist/services/shares.js +2 -1
  68. package/dist/services/specifications.test.d.ts +1 -0
  69. package/dist/services/specifications.test.js +96 -0
  70. package/dist/types/items.d.ts +11 -0
  71. package/dist/utils/apply-query.js +7 -3
  72. package/dist/utils/apply-snapshot.js +15 -0
  73. package/dist/utils/apply-snapshot.test.d.ts +1 -0
  74. package/dist/utils/apply-snapshot.test.js +305 -0
  75. package/dist/utils/async-handler.d.ts +2 -6
  76. package/dist/utils/async-handler.js +1 -13
  77. package/dist/utils/async-handler.test.d.ts +1 -0
  78. package/dist/utils/async-handler.test.js +18 -0
  79. package/dist/utils/calculate-field-depth.test.d.ts +1 -0
  80. package/dist/utils/calculate-field-depth.test.js +76 -0
  81. package/dist/utils/filter-items.test.d.ts +1 -0
  82. package/dist/utils/filter-items.test.js +60 -0
  83. package/dist/utils/get-cache-key.test.d.ts +1 -0
  84. package/dist/utils/get-cache-key.test.js +53 -0
  85. package/dist/utils/get-column-path.test.d.ts +1 -0
  86. package/dist/utils/get-column-path.test.js +136 -0
  87. package/dist/utils/get-config-from-env.test.d.ts +1 -0
  88. package/dist/utils/get-config-from-env.test.js +19 -0
  89. package/dist/utils/get-graphql-type.d.ts +1 -1
  90. package/dist/utils/get-graphql-type.js +4 -1
  91. package/dist/utils/get-os-info.d.ts +9 -0
  92. package/dist/utils/get-os-info.js +47 -0
  93. package/dist/utils/get-relation-info.test.d.ts +1 -0
  94. package/dist/utils/get-relation-info.test.js +88 -0
  95. package/dist/utils/get-relation-type.test.d.ts +1 -0
  96. package/dist/utils/get-relation-type.test.js +69 -0
  97. package/dist/utils/get-string-byte-size.test.d.ts +1 -0
  98. package/dist/utils/get-string-byte-size.test.js +8 -0
  99. package/dist/utils/is-directus-jwt.test.d.ts +1 -0
  100. package/dist/utils/is-directus-jwt.test.js +26 -0
  101. package/dist/utils/jwt.test.d.ts +1 -0
  102. package/dist/utils/jwt.test.js +36 -0
  103. package/dist/utils/merge-permissions.test.d.ts +1 -0
  104. package/dist/utils/merge-permissions.test.js +80 -0
  105. package/dist/utils/validate-keys.test.d.ts +1 -0
  106. package/dist/utils/validate-keys.test.js +97 -0
  107. package/package.json +14 -12
package/dist/app.js CHANGED
@@ -135,6 +135,13 @@ async function createApp() {
135
135
  await emitter_1.default.emitInit('app.before', { app });
136
136
  await emitter_1.default.emitInit('middlewares.before', { app });
137
137
  app.use(logger_1.expressLogger);
138
+ app.use((_req, res, next) => {
139
+ res.setHeader('X-Powered-By', 'Directus');
140
+ next();
141
+ });
142
+ if (env_1.default.CORS_ENABLED === true) {
143
+ app.use(cors_1.default);
144
+ }
138
145
  app.use((req, res, next) => {
139
146
  express_1.default.json({
140
147
  limit: env_1.default.MAX_PAYLOAD_SIZE,
@@ -147,13 +154,6 @@ async function createApp() {
147
154
  });
148
155
  app.use((0, cookie_parser_1.default)());
149
156
  app.use(extract_token_1.default);
150
- app.use((_req, res, next) => {
151
- res.setHeader('X-Powered-By', 'Directus');
152
- next();
153
- });
154
- if (env_1.default.CORS_ENABLED === true) {
155
- app.use(cors_1.default);
156
- }
157
157
  app.get('/', (_req, res, next) => {
158
158
  if (env_1.default.ROOT_REDIRECT) {
159
159
  res.redirect(env_1.default.ROOT_REDIRECT);
@@ -190,6 +190,7 @@ async function createApp() {
190
190
  if (env_1.default.RATE_LIMITER_ENABLED === true) {
191
191
  app.use(rate_limiter_1.default);
192
192
  }
193
+ app.get('/server/ping', (req, res) => res.send('pong'));
193
194
  app.use(authenticate_1.default);
194
195
  app.use(check_ip_1.checkIP);
195
196
  app.use(sanitize_query_1.default);
@@ -302,6 +302,7 @@ function createLDAPAuthRouter(provider) {
302
302
  const accountability = {
303
303
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
304
304
  userAgent: req.get('user-agent'),
305
+ origin: req.get('origin'),
305
306
  role: null,
306
307
  };
307
308
  const authenticationService = new services_1.AuthenticationService({
@@ -15,6 +15,8 @@ const env_1 = __importDefault(require("../../env"));
15
15
  const respond_1 = require("../../middleware/respond");
16
16
  const constants_1 = require("../../constants");
17
17
  const get_ip_from_req_1 = require("../../utils/get-ip-from-req");
18
+ const perf_hooks_1 = require("perf_hooks");
19
+ const stall_1 = require("../../utils/stall");
18
20
  class LocalAuthDriver extends auth_1.AuthDriver {
19
21
  async getUserID(payload) {
20
22
  if (!payload.email) {
@@ -50,9 +52,12 @@ function createLocalAuthRouter(provider) {
50
52
  }).unknown();
51
53
  router.post('/', (0, async_handler_1.default)(async (req, res, next) => {
52
54
  var _a;
55
+ const STALL_TIME = env_1.default.LOGIN_STALL_TIME;
56
+ const timeStart = perf_hooks_1.performance.now();
53
57
  const accountability = {
54
58
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
55
59
  userAgent: req.get('user-agent'),
60
+ origin: req.get('origin'),
56
61
  role: null,
57
62
  };
58
63
  const authenticationService = new services_1.AuthenticationService({
@@ -61,6 +66,7 @@ function createLocalAuthRouter(provider) {
61
66
  });
62
67
  const { error } = userLoginSchema.validate(req.body);
63
68
  if (error) {
69
+ await (0, stall_1.stall)(STALL_TIME, timeStart);
64
70
  throw new exceptions_1.InvalidPayloadException(error.message);
65
71
  }
66
72
  const mode = req.body.mode || 'json';
@@ -222,6 +222,7 @@ function createOAuth2AuthRouter(providerName) {
222
222
  accountability: {
223
223
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
224
224
  userAgent: req.get('user-agent'),
225
+ origin: req.get('origin'),
225
226
  role: null,
226
227
  },
227
228
  schema: req.schema,
@@ -241,6 +241,7 @@ function createOpenIDAuthRouter(providerName) {
241
241
  accountability: {
242
242
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
243
243
  userAgent: req.get('user-agent'),
244
+ origin: req.get('origin'),
244
245
  role: null,
245
246
  },
246
247
  schema: req.schema,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("./index");
4
+ jest.mock('../../src/env', () => ({
5
+ ...jest.requireActual('../../src/env').default,
6
+ EXTENSIONS_PATH: '',
7
+ SERVE_APP: false,
8
+ DB_CLIENT: 'pg',
9
+ DB_HOST: 'localhost',
10
+ DB_PORT: 5432,
11
+ DB_DATABASE: 'directus',
12
+ DB_USER: 'postgres',
13
+ DB_PASSWORD: 'psql1234',
14
+ }));
15
+ jest.mock('@directus/shared/utils/node/get-extensions', () => ({
16
+ getPackageExtensions: jest.fn(() => Promise.resolve([])),
17
+ getLocalExtensions: jest.fn(() => Promise.resolve([customCliExtension])),
18
+ }));
19
+ jest.mock(`/hooks/custom-cli/index.js`, () => customCliHook, { virtual: true });
20
+ const customCliExtension = {
21
+ path: `/hooks/custom-cli`,
22
+ name: 'custom-cli',
23
+ type: 'hook',
24
+ entrypoint: 'index.js',
25
+ local: true,
26
+ };
27
+ const beforeHook = jest.fn();
28
+ const afterAction = jest.fn();
29
+ const afterHook = jest.fn(({ program }) => {
30
+ program.command('custom').action(afterAction);
31
+ });
32
+ const customCliHook = ({ init }) => {
33
+ init('cli.before', beforeHook);
34
+ init('cli.after', afterHook);
35
+ };
36
+ const writeOut = jest.fn();
37
+ const writeErr = jest.fn();
38
+ const setup = async () => {
39
+ const program = await (0, index_1.createCli)();
40
+ program.exitOverride();
41
+ program.configureOutput({ writeOut, writeErr });
42
+ return program;
43
+ };
44
+ beforeEach(jest.clearAllMocks);
45
+ describe('cli hooks', () => {
46
+ test('should call hooks before and after creating the cli', async () => {
47
+ const program = await setup();
48
+ expect(beforeHook).toHaveBeenCalledTimes(1);
49
+ expect(beforeHook).toHaveBeenCalledWith({ event: 'cli.before', program });
50
+ expect(afterHook).toHaveBeenCalledTimes(1);
51
+ expect(afterHook).toHaveBeenCalledWith({ event: 'cli.after', program });
52
+ });
53
+ test('should be able to add a custom cli command', async () => {
54
+ const program = await setup();
55
+ program.parseAsync(['custom'], { from: 'user' });
56
+ expect(afterAction).toHaveBeenCalledTimes(1);
57
+ });
58
+ });
@@ -211,6 +211,10 @@ REFRESH_TOKEN_COOKIE_NAME="directus_refresh_token"
211
211
  # Which domain to use for the refresh cookie. Useful for development mode.
212
212
  # REFRESH_TOKEN_COOKIE_DOMAIN
213
213
 
214
+ # The duration in milliseconds that a login request will be stalled for,
215
+ # and it should be greater than the time taken for a login request with an invalid password [500]
216
+ # LOGIN_STALL_TIME=500
217
+
214
218
  # Whether or not to enable the CORS headers [false]
215
219
  CORS_ENABLED=true
216
220
 
@@ -296,10 +300,10 @@ EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
296
300
  ## Email (Sendmail Transport)
297
301
 
298
302
  # What new line style to use in sendmail ["unix"]
299
- EMAIL_SENDMAIL_NEW_LINE="unix"
303
+ # EMAIL_SENDMAIL_NEW_LINE="unix"
300
304
 
301
305
  # Path to your sendmail executable ["/usr/sbin/sendmail"]
302
- EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
306
+ # EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
303
307
 
304
308
  ## Email (SMTP Transport)
305
309
  # EMAIL_SMTP_HOST="localhost"
@@ -75,6 +75,7 @@ router.post('/comment', (0, async_handler_1.default)(async (req, res, next) => {
75
75
  user: (_a = req.accountability) === null || _a === void 0 ? void 0 : _a.user,
76
76
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
77
77
  user_agent: req.get('user-agent'),
78
+ origin: req.get('origin'),
78
79
  });
79
80
  try {
80
81
  const record = await service.readOne(primaryKey, req.sanitizedQuery);
@@ -148,27 +148,31 @@ router.get('/:pk/:filename?',
148
148
  res.setHeader('Content-Length', stat.size);
149
149
  return res.end();
150
150
  }
151
- stream.on('data', (chunk) => res.write(chunk));
151
+ let isDataSent = false;
152
+ stream.on('data', (chunk) => {
153
+ isDataSent = true;
154
+ res.write(chunk);
155
+ });
152
156
  stream.on('end', () => {
153
- res.status(200).send();
157
+ res.end();
154
158
  });
155
159
  stream.on('error', (e) => {
156
160
  logger_1.default.error(e, `Couldn't stream file ${file.id} to the client`);
157
- stream.unpipe(res);
158
- res.removeHeader('Content-Disposition');
159
- res.removeHeader('Content-Type');
160
- res.removeHeader('Content-Disposition');
161
- res.removeHeader('Cache-Control');
162
- res.status(500).json({
163
- errors: [
164
- {
165
- message: 'An unexpected error occurred.',
166
- extensions: {
167
- code: 'INTERNAL_SERVER_ERROR',
161
+ if (!isDataSent) {
162
+ res.removeHeader('Content-Type');
163
+ res.removeHeader('Content-Disposition');
164
+ res.removeHeader('Cache-Control');
165
+ res.status(500).json({
166
+ errors: [
167
+ {
168
+ message: 'An unexpected error occurred.',
169
+ extensions: {
170
+ code: 'INTERNAL_SERVER_ERROR',
171
+ },
168
172
  },
169
- },
170
- ],
171
- });
173
+ ],
174
+ });
175
+ }
172
176
  });
173
177
  }));
174
178
  exports.default = router;
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = require("express");
7
- const ms_1 = __importDefault(require("ms"));
8
7
  const env_1 = __importDefault(require("../env"));
9
8
  const exceptions_1 = require("../exceptions");
10
9
  const respond_1 = require("../middleware/respond");
@@ -15,6 +14,7 @@ const logger_1 = __importDefault(require("../logger"));
15
14
  const drivers_1 = require("../auth/drivers");
16
15
  const constants_1 = require("../constants");
17
16
  const get_ip_from_req_1 = require("../utils/get-ip-from-req");
17
+ const constants_2 = require("../constants");
18
18
  const router = (0, express_1.Router)();
19
19
  const authProviders = (0, get_auth_providers_1.getAuthProviders)();
20
20
  for (const authProvider of authProviders) {
@@ -43,10 +43,10 @@ if (!env_1.default.AUTH_DISABLE_DEFAULT) {
43
43
  router.use('/login', (0, drivers_1.createLocalAuthRouter)(constants_1.DEFAULT_AUTH_PROVIDER));
44
44
  }
45
45
  router.post('/refresh', (0, async_handler_1.default)(async (req, res, next) => {
46
- var _a;
47
46
  const accountability = {
48
47
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
49
48
  userAgent: req.get('user-agent'),
49
+ origin: req.get('origin'),
50
50
  role: null,
51
51
  };
52
52
  const authenticationService = new services_1.AuthenticationService({
@@ -66,13 +66,7 @@ router.post('/refresh', (0, async_handler_1.default)(async (req, res, next) => {
66
66
  payload.data.refresh_token = refreshToken;
67
67
  }
68
68
  if (mode === 'cookie') {
69
- res.cookie(env_1.default.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
70
- httpOnly: true,
71
- domain: env_1.default.REFRESH_TOKEN_COOKIE_DOMAIN,
72
- maxAge: (0, ms_1.default)(env_1.default.REFRESH_TOKEN_TTL),
73
- secure: (_a = env_1.default.REFRESH_TOKEN_COOKIE_SECURE) !== null && _a !== void 0 ? _a : false,
74
- sameSite: env_1.default.REFRESH_TOKEN_COOKIE_SAME_SITE || 'strict',
75
- });
69
+ res.cookie(env_1.default.REFRESH_TOKEN_COOKIE_NAME, refreshToken, constants_2.COOKIE_OPTIONS);
76
70
  }
77
71
  res.locals.payload = payload;
78
72
  return next();
@@ -82,6 +76,7 @@ router.post('/logout', (0, async_handler_1.default)(async (req, res, next) => {
82
76
  const accountability = {
83
77
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
84
78
  userAgent: req.get('user-agent'),
79
+ origin: req.get('origin'),
85
80
  role: null,
86
81
  };
87
82
  const authenticationService = new services_1.AuthenticationService({
@@ -110,6 +105,7 @@ router.post('/password/request', (0, async_handler_1.default)(async (req, res, n
110
105
  const accountability = {
111
106
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
112
107
  userAgent: req.get('user-agent'),
108
+ origin: req.get('origin'),
113
109
  role: null,
114
110
  };
115
111
  const service = new services_1.UsersService({ accountability, schema: req.schema });
@@ -137,6 +133,7 @@ router.post('/password/reset', (0, async_handler_1.default)(async (req, res, nex
137
133
  const accountability = {
138
134
  ip: (0, get_ip_from_req_1.getIPFromReq)(req),
139
135
  userAgent: req.get('user-agent'),
136
+ origin: req.get('origin'),
140
137
  role: null,
141
138
  };
142
139
  const service = new services_1.UsersService({ accountability, schema: req.schema });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ // @ts-nocheck
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ jest.mock('../../src/cache');
8
+ jest.mock('../../src/database');
9
+ jest.mock('../../src/utils/validate-env');
10
+ const files_1 = require("./files");
11
+ const invalid_payload_1 = require("../exceptions/invalid-payload");
12
+ const stream_1 = require("stream");
13
+ const form_data_1 = __importDefault(require("form-data"));
14
+ describe('multipartHandler', () => {
15
+ it(`Errors out if request doesn't contain any files to upload`, () => {
16
+ const fakeForm = new form_data_1.default();
17
+ fakeForm.append('field', 'test');
18
+ const req = {
19
+ headers: fakeForm.getHeaders(),
20
+ is: jest.fn().mockReturnValue(true),
21
+ body: fakeForm.getBuffer(),
22
+ params: {},
23
+ pipe: (input) => stream.pipe(input),
24
+ };
25
+ const stream = new stream_1.PassThrough();
26
+ stream.push(fakeForm.getBuffer());
27
+ (0, files_1.multipartHandler)(req, {}, (err) => {
28
+ expect(err.message).toBe('No files where included in the body');
29
+ expect(err).toBeInstanceOf(invalid_payload_1.InvalidPayloadException);
30
+ });
31
+ });
32
+ it(`Errors out if uploaded file doesn't include the filename`, () => {
33
+ const fakeForm = new form_data_1.default();
34
+ fakeForm.append('file', Buffer.from('<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 243 266" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g id="Calligraphy"><path d="M67.097,135.868c0,3.151 0.598,14.121 -11.586,14.121c-17.076,0 -15.95,-12.947 -15.95,-12.947c0,-2.521 4.597,-5.638 4.597,-7.318c0,-0.63 0.041,-3.519 -2.27,-3.519c-5.671,0 -5.671,10.083 -5.671,10.083c0,0 0.419,15.336 19.116,15.336c20.205,0 30.04,-23.712 30.04,-30.88c0,-18.197 -51.112,-27.701 -51.112,-57.949c0,1.575 -2.205,-13.864 14.18,-13.864c28.358,0 44.426,42.536 44.426,71.524c0,28.988 -16.699,55.455 -16.699,55.455c0,0 33.4,-25.837 33.4,-76.25c0,-70.264 -46.003,-69.634 -46.003,-69.634c-4.792,0 -7.602,-0.241 -28.398,20.555c-20.797,20.797 -17.646,29.83 -17.646,29.83c0,31.93 49.576,32.35 49.576,55.457Z" style="fill-rule:nonzero;"/><path d="M241.886,174.861c-1.602,-9.142 -15.448,-9.916 -22.675,-9.682c-0.7,-0.003 -1.172,0.02 -1.327,0.03c-8.804,0.01 -19.314,4.179 -33.072,13.115c-3.554,2.308 -7.19,4.847 -10.902,7.562c-6.979,-31.39 -13.852,-63.521 -28.033,-63.521c20.415,-20.119 22.19,-16.272 22.19,-39.054c0,-11.244 14.498,-21.35 14.498,-21.35l-0.296,-2.024c-19.193,5.304 -37.307,-8.577 -42.2,-12.755c5.375,-9.663 9.584,-12.565 9.584,-12.565c1.891,-20.377 15.965,-27.31 15.965,-27.31c1.681,-4.201 6.092,-7.142 6.092,-7.142c-70.162,22.267 -54.247,189.298 -54.247,189.298c-0.475,-55.91 5.238,-92.242 11.977,-115.55c9.094,8.248 24.425,11.765 24.425,11.765c-7.396,3.55 -5.324,12.13 -5.324,19.527c0,7.397 -3.848,10.651 -3.848,10.651l-21.893,22.782c17.043,0.294 23.638,31.657 30.689,63.056c-2.548,2.042 -5.125,4.12 -7.728,6.219c-16.396,13.223 -33.351,26.897 -50.266,37.354c-19.086,11.797 -35.151,17.533 -49.116,17.533c-25.25,0 -44.118,-24.368 -44.118,-46.154c0,-9.838 3.227,-17.831 5.935,-22.805c2.935,-5.39 5.911,-8.503 5.967,-8.561c0.001,0 0.001,0 0.001,-0.001l-0.013,-0.012c1.803,-1.885 4.841,-5.181 10.423,-5.181c20.715,0 27.475,40.776 55.603,40.776c24.857,0 31.834,-20.497 37.286,-31.399c0,0 -8.94,11.12 -21.587,11.12c-27.038,0 -35.323,-40.557 -55.166,-40.557c-13.41,0 -22.743,15.506 -31.029,27.281c0,0 0.018,-0.001 0.048,-0.003c-1.02,1.415 -2.214,3.233 -3.41,5.425c-2.847,5.21 -6.239,13.587 -6.239,23.917c0,22.816 19.801,48.334 46.299,48.334c14.381,0 30.822,-5.84 50.262,-17.858c17.033,-10.529 34.04,-24.246 50.489,-37.511c2.309,-1.862 4.607,-3.715 6.891,-5.549c6.952,30.814 14.606,60.912 33.278,60.912c14.794,0 26.923,-25.445 26.923,-25.445c-7.987,7.101 -13.313,5.621 -13.313,5.621c-13.139,0.379 -19.937,-27.594 -26.48,-56.931c16.455,-12.099 31.46,-20.829 43.488,-20.829l0.072,-0.003c0.082,-0.005 5.246,-0.305 9.957,1.471c-2.95,1.636 -4.947,4.782 -4.947,8.394c0,5.299 4.296,9.594 9.594,9.594c5.298,0 9.594,-4.295 9.594,-9.594c0,-0.826 -0.104,-1.627 -0.301,-2.391Z" style="fill-rule:nonzero;"/></g></svg>'));
35
+ const req = {
36
+ headers: fakeForm.getHeaders(),
37
+ is: jest.fn().mockReturnValue(true),
38
+ body: fakeForm.getBuffer(),
39
+ params: {},
40
+ pipe: (input) => stream.pipe(input),
41
+ };
42
+ const stream = new stream_1.PassThrough();
43
+ stream.push(fakeForm.getBuffer());
44
+ (0, files_1.multipartHandler)(req, {}, (err) => {
45
+ expect(err.message).toBe('File is missing filename');
46
+ expect(err).toBeInstanceOf(invalid_payload_1.InvalidPayloadException);
47
+ });
48
+ });
49
+ });
@@ -36,7 +36,6 @@ router.get('/specs/graphql/:scope?', (0, async_handler_1.default)(async (req, re
36
36
  res.attachment(filename);
37
37
  res.send(result);
38
38
  }));
39
- router.get('/ping', (req, res) => res.send('pong'));
40
39
  router.get('/info', (0, async_handler_1.default)(async (req, res, next) => {
41
40
  const service = new services_1.ServerService({
42
41
  accountability: req.accountability,
@@ -0,0 +1,3 @@
1
+ import { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.down = exports.up = void 0;
4
+ async function up(knex) {
5
+ await knex.schema.alterTable('directus_activity', (table) => {
6
+ table.string('origin').nullable();
7
+ });
8
+ await knex.schema.alterTable('directus_sessions', (table) => {
9
+ table.string('origin').nullable();
10
+ });
11
+ }
12
+ exports.up = up;
13
+ async function down(knex) {
14
+ await knex.schema.alterTable('directus_activity', (table) => {
15
+ table.dropColumn('origin');
16
+ });
17
+ await knex.schema.alterTable('directus_sessions', (table) => {
18
+ table.dropColumn('origin');
19
+ });
20
+ }
21
+ exports.down = down;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const knex_1 = __importDefault(require("knex"));
7
+ const knex_mock_client_1 = require("knex-mock-client");
8
+ const run_1 = __importDefault(require("./run"));
9
+ describe('run', () => {
10
+ let db;
11
+ let tracker;
12
+ beforeAll(() => {
13
+ db = (0, knex_1.default)({ client: knex_mock_client_1.MockClient });
14
+ tracker = (0, knex_mock_client_1.getTracker)();
15
+ });
16
+ afterEach(() => {
17
+ tracker.reset();
18
+ });
19
+ describe('when passed the argument up', () => {
20
+ it('returns "Nothing To Upgrade" if no directus_migrations', async () => {
21
+ tracker.on.select('directus_migrations').response(['Empty']);
22
+ await (0, run_1.default)(db, 'up').catch((e) => {
23
+ expect(e).toBeInstanceOf(Error);
24
+ expect(e.message).toBe('Nothing to upgrade');
25
+ });
26
+ });
27
+ it('returns "Method implemented in the dialect driver" if no directus_migrations', async () => {
28
+ tracker.on.select('directus_migrations').response([]);
29
+ await (0, run_1.default)(db, 'up').catch((e) => {
30
+ expect(e).toBeInstanceOf(Error);
31
+ expect(e.message).toBe('Method implemented in the dialect driver');
32
+ });
33
+ });
34
+ it('returns undefined if the migration is successful', async () => {
35
+ tracker.on.select('directus_migrations').response([
36
+ {
37
+ version: '20201028A',
38
+ name: 'Remove Collection Foreign Keys',
39
+ timestamp: '2021-11-27 11:36:56.471595-05',
40
+ },
41
+ ]);
42
+ tracker.on.delete('directus_relations').response([]);
43
+ tracker.on.insert('directus_migrations').response(['Remove System Relations', '20201029A']);
44
+ expect(await (0, run_1.default)(db, 'up')).toBe(undefined);
45
+ });
46
+ });
47
+ describe('when passed the argument down', () => {
48
+ it('returns "Nothing To downgrade" if no valid directus_migrations', async () => {
49
+ tracker.on.select('directus_migrations').response(['Empty']);
50
+ await (0, run_1.default)(db, 'down').catch((e) => {
51
+ expect(e).toBeInstanceOf(Error);
52
+ expect(e.message).toBe(`Couldn't find migration`);
53
+ });
54
+ });
55
+ it('returns "Method implemented in the dialect driver" if no directus_migrations', async () => {
56
+ tracker.on.select('directus_migrations').response([]);
57
+ await (0, run_1.default)(db, 'down').catch((e) => {
58
+ expect(e).toBeInstanceOf(Error);
59
+ expect(e.message).toBe('Nothing to downgrade');
60
+ });
61
+ });
62
+ it(`returns "Couldn't find migration" if an invalid migration object is supplied`, async () => {
63
+ tracker.on.select('directus_migrations').response([
64
+ {
65
+ version: '202018129A',
66
+ name: 'Fake Migration',
67
+ timestamp: '2020-00-32 11:36:56.471595-05',
68
+ },
69
+ ]);
70
+ await (0, run_1.default)(db, 'down').catch((e) => {
71
+ expect(e).toBeInstanceOf(Error);
72
+ expect(e.message).toBe(`Couldn't find migration`);
73
+ });
74
+ });
75
+ });
76
+ describe('when passed the argument latest', () => {
77
+ it('returns "Nothing To downgrade" if no valid directus_migrations', async () => {
78
+ tracker.on.select('directus_migrations').response(['Empty']);
79
+ await (0, run_1.default)(db, 'latest').catch((e) => {
80
+ expect(e).toBeInstanceOf(Error);
81
+ expect(e.message).toBe(`Method implemented in the dialect driver`);
82
+ });
83
+ });
84
+ it('returns "Method implemented in the dialect driver" if no directus_migrations', async () => {
85
+ tracker.on.select('directus_migrations').response([]);
86
+ await (0, run_1.default)(db, 'latest').catch((e) => {
87
+ expect(e).toBeInstanceOf(Error);
88
+ expect(e.message).toBe('Method implemented in the dialect driver');
89
+ });
90
+ });
91
+ });
92
+ });
@@ -60,6 +60,12 @@ fields:
60
60
  font: monospace
61
61
  width: half
62
62
 
63
+ - field: origin
64
+ display: formatted-value
65
+ display_options:
66
+ font: monospace
67
+ width: half
68
+
63
69
  - field: ip
64
70
  display: formatted-value
65
71
  display_options:
@@ -11,4 +11,6 @@ fields:
11
11
  width: half
12
12
  - field: user_agent
13
13
  width: half
14
+ - field: origin
15
+ width: half
14
16
  - field: share
package/dist/env.js CHANGED
@@ -42,6 +42,7 @@ const allowedEnvironmentVars = [
42
42
  'REFRESH_TOKEN_COOKIE_SECURE',
43
43
  'REFRESH_TOKEN_COOKIE_SAME_SITE',
44
44
  'REFRESH_TOKEN_COOKIE_NAME',
45
+ 'LOGIN_STALL_TIME',
45
46
  'PASSWORD_RESET_URL_ALLOW_LIST',
46
47
  'USER_INVITE_URL_ALLOW_LIST',
47
48
  'IP_TRUST_PROXY',
@@ -79,6 +80,7 @@ const allowedEnvironmentVars = [
79
80
  'CACHE_REDIS_PASSWORD',
80
81
  'CACHE_MEMCACHE',
81
82
  'CACHE_VALUE_MAX_SIZE',
83
+ 'CACHE_HEALTHCHECK_THRESHOLD',
82
84
  // storage
83
85
  'STORAGE_LOCATIONS',
84
86
  'STORAGE_.+_DRIVER',
@@ -95,6 +97,7 @@ const allowedEnvironmentVars = [
95
97
  'STORAGE_.+_ENDPOINT',
96
98
  'STORAGE_.+_KEY_FILENAME',
97
99
  'STORAGE_.+_BUCKET',
100
+ 'STORAGE_.+_HEALTHCHECK_THRESHOLD',
98
101
  // metadata
99
102
  'FILE_METADATA_ALLOW_LIST',
100
103
  // assets
@@ -138,12 +141,20 @@ const allowedEnvironmentVars = [
138
141
  // extensions
139
142
  'EXTENSIONS_PATH',
140
143
  'EXTENSIONS_AUTO_RELOAD',
144
+ // messenger
145
+ 'MESSENGER_STORE',
146
+ 'MESSENGER_NAMESPACE',
147
+ 'MESSENGER_REDIS',
148
+ 'MESSENGER_REDIS_HOST',
149
+ 'MESSENGER_REDIS_PORT',
150
+ 'MESSENGER_REDIS_PASSWORD',
141
151
  // emails
142
152
  'EMAIL_FROM',
143
153
  'EMAIL_TRANSPORT',
144
154
  'EMAIL_VERIFY_SETUP',
145
155
  'EMAIL_SENDMAIL_NEW_LINE',
146
156
  'EMAIL_SENDMAIL_PATH',
157
+ 'EMAIL_SMTP_NAME',
147
158
  'EMAIL_SMTP_HOST',
148
159
  'EMAIL_SMTP_PORT',
149
160
  'EMAIL_SMTP_USER',
@@ -165,6 +176,8 @@ const allowedEnvironmentVars = [
165
176
  // limits & optimization
166
177
  'RELATIONAL_BATCH_SIZE',
167
178
  'EXPORT_BATCH_SIZE',
179
+ // flows
180
+ 'FLOWS_EXEC_ALLOWED_MODULES',
168
181
  ].map((name) => new RegExp(`^${name}$`));
169
182
  const acceptedEnvTypes = ['string', 'number', 'regex', 'array', 'json'];
170
183
  const defaults = {
@@ -187,6 +200,7 @@ const defaults = {
187
200
  REFRESH_TOKEN_COOKIE_SECURE: false,
188
201
  REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
189
202
  REFRESH_TOKEN_COOKIE_NAME: 'directus_refresh_token',
203
+ LOGIN_STALL_TIME: 500,
190
204
  ROOT_REDIRECT: './admin',
191
205
  CORS_ENABLED: false,
192
206
  CORS_ORIGIN: false,
@@ -226,6 +240,7 @@ const defaults = {
226
240
  EXPORT_BATCH_SIZE: 5000,
227
241
  FILE_METADATA_ALLOW_LIST: 'ifd0.Make,ifd0.Model,exif.FNumber,exif.ExposureTime,exif.FocalLength,exif.ISO',
228
242
  GRAPHQL_INTROSPECTION: true,
243
+ FLOWS_EXEC_ALLOWED_MODULES: false,
229
244
  };
230
245
  // Allows us to force certain environment variable into a type, instead of relying
231
246
  // on the auto-parsed type in processValues. ref #3705
@@ -0,0 +1,8 @@
1
+ declare const testEnv: {
2
+ NUMBER: string;
3
+ NUMBER_CAST_AS_STRING: string;
4
+ REGEX: string;
5
+ CSV: string;
6
+ CSV_CAST_AS_STRING: string;
7
+ MULTIPLE: string;
8
+ };
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ const testEnv = {
3
+ NUMBER: '1234',
4
+ NUMBER_CAST_AS_STRING: 'string:1234',
5
+ REGEX: 'regex:\\.example\\.com$',
6
+ CSV: 'one,two,three,four',
7
+ CSV_CAST_AS_STRING: 'string:one,two,three,four',
8
+ MULTIPLE: 'array:string:https://example.com,regex:\\.example2\\.com$',
9
+ };
10
+ describe('env processed values', () => {
11
+ const originalEnv = process.env;
12
+ let env;
13
+ beforeEach(() => {
14
+ jest.resetModules();
15
+ process.env = { ...testEnv };
16
+ env = jest.requireActual('../src/env').default;
17
+ });
18
+ afterEach(() => {
19
+ process.env = originalEnv;
20
+ });
21
+ test('Number value should be a number', () => {
22
+ expect(env.NUMBER).toStrictEqual(1234);
23
+ });
24
+ test('Number value casted as string should be a string', () => {
25
+ expect(env.NUMBER_CAST_AS_STRING).toStrictEqual('1234');
26
+ });
27
+ test('Value casted as regex', () => {
28
+ expect(env.REGEX).toBeInstanceOf(RegExp);
29
+ });
30
+ test('CSV value should be an array', () => {
31
+ expect(env.CSV).toStrictEqual(['one', 'two', 'three', 'four']);
32
+ });
33
+ test('CSV value casted as string should be a string', () => {
34
+ expect(env.CSV_CAST_AS_STRING).toStrictEqual('one,two,three,four');
35
+ });
36
+ test('Multiple type cast', () => {
37
+ expect(env.MULTIPLE).toStrictEqual(['https://example.com', /\.example2\.com$/]);
38
+ });
39
+ });
@@ -24,6 +24,7 @@ declare class ExtensionManager {
24
24
  private load;
25
25
  private unload;
26
26
  private initializeWatcher;
27
+ private closeWatcher;
27
28
  private updateWatchedExtensions;
28
29
  private getExtensions;
29
30
  private generateExtensionBundles;