directus 9.16.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.
- package/dist/app.js +7 -7
- package/dist/auth/drivers/ldap.js +1 -0
- package/dist/auth/drivers/local.js +6 -0
- package/dist/auth/drivers/oauth2.js +1 -0
- package/dist/auth/drivers/openid.js +1 -0
- package/dist/cli/utils/create-env/env-stub.liquid +4 -0
- package/dist/controllers/activity.js +1 -0
- package/dist/controllers/auth.js +6 -9
- package/dist/database/migrations/20220826A-add-origin-to-accountability.d.ts +3 -0
- package/dist/database/migrations/20220826A-add-origin-to-accountability.js +21 -0
- package/dist/database/system-data/fields/activity.yaml +6 -0
- package/dist/database/system-data/fields/sessions.yaml +2 -0
- package/dist/env.js +7 -0
- package/dist/extensions.d.ts +1 -0
- package/dist/extensions.js +16 -3
- package/dist/flows.js +27 -17
- package/dist/mailer.js +1 -0
- package/dist/middleware/authenticate.d.ts +1 -1
- package/dist/middleware/authenticate.js +1 -0
- package/dist/middleware/authenticate.test.js +44 -4
- package/dist/middleware/validate-batch.d.ts +1 -2
- package/dist/middleware/validate-batch.js +10 -13
- package/dist/middleware/validate-batch.test.d.ts +1 -0
- package/dist/middleware/validate-batch.test.js +82 -0
- package/dist/operations/request/index.js +2 -2
- package/dist/operations/transform/index.d.ts +1 -1
- package/dist/operations/transform/index.js +1 -1
- package/dist/services/authentication.js +13 -3
- package/dist/services/files.js +2 -2
- package/dist/services/graphql/index.js +45 -37
- package/dist/services/items.js +15 -3
- package/dist/services/payload.js +28 -4
- package/dist/services/server.js +5 -4
- package/dist/services/shares.js +2 -1
- package/dist/utils/async-handler.d.ts +2 -6
- package/dist/utils/async-handler.js +1 -13
- package/dist/utils/async-handler.test.d.ts +1 -0
- package/dist/utils/async-handler.test.js +18 -0
- package/package.json +12 -10
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);
|
|
@@ -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';
|
|
@@ -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
|
|
|
@@ -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);
|
package/dist/controllers/auth.js
CHANGED
|
@@ -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,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;
|
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
|
|
@@ -151,6 +154,7 @@ const allowedEnvironmentVars = [
|
|
|
151
154
|
'EMAIL_VERIFY_SETUP',
|
|
152
155
|
'EMAIL_SENDMAIL_NEW_LINE',
|
|
153
156
|
'EMAIL_SENDMAIL_PATH',
|
|
157
|
+
'EMAIL_SMTP_NAME',
|
|
154
158
|
'EMAIL_SMTP_HOST',
|
|
155
159
|
'EMAIL_SMTP_PORT',
|
|
156
160
|
'EMAIL_SMTP_USER',
|
|
@@ -172,6 +176,8 @@ const allowedEnvironmentVars = [
|
|
|
172
176
|
// limits & optimization
|
|
173
177
|
'RELATIONAL_BATCH_SIZE',
|
|
174
178
|
'EXPORT_BATCH_SIZE',
|
|
179
|
+
// flows
|
|
180
|
+
'FLOWS_EXEC_ALLOWED_MODULES',
|
|
175
181
|
].map((name) => new RegExp(`^${name}$`));
|
|
176
182
|
const acceptedEnvTypes = ['string', 'number', 'regex', 'array', 'json'];
|
|
177
183
|
const defaults = {
|
|
@@ -194,6 +200,7 @@ const defaults = {
|
|
|
194
200
|
REFRESH_TOKEN_COOKIE_SECURE: false,
|
|
195
201
|
REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
|
|
196
202
|
REFRESH_TOKEN_COOKIE_NAME: 'directus_refresh_token',
|
|
203
|
+
LOGIN_STALL_TIME: 500,
|
|
197
204
|
ROOT_REDIRECT: './admin',
|
|
198
205
|
CORS_ENABLED: false,
|
|
199
206
|
CORS_ORIGIN: false,
|
package/dist/extensions.d.ts
CHANGED
package/dist/extensions.js
CHANGED
|
@@ -78,19 +78,27 @@ class ExtensionManager {
|
|
|
78
78
|
this.reloadQueue = new job_queue_1.JobQueue();
|
|
79
79
|
}
|
|
80
80
|
async initialize(options = {}) {
|
|
81
|
+
const prevOptions = this.options;
|
|
81
82
|
this.options = {
|
|
82
83
|
...defaultOptions,
|
|
83
84
|
...options,
|
|
84
85
|
};
|
|
85
|
-
this.
|
|
86
|
+
if (!prevOptions.watch && this.options.watch) {
|
|
87
|
+
this.initializeWatcher();
|
|
88
|
+
}
|
|
89
|
+
else if (prevOptions.watch && !this.options.watch) {
|
|
90
|
+
await this.closeWatcher();
|
|
91
|
+
}
|
|
86
92
|
if (!this.isLoaded) {
|
|
87
93
|
await this.load();
|
|
88
|
-
this.updateWatchedExtensions(this.extensions);
|
|
89
94
|
const loadedExtensions = this.getExtensionsList();
|
|
90
95
|
if (loadedExtensions.length > 0) {
|
|
91
96
|
logger_1.default.info(`Loaded extensions: ${loadedExtensions.join(', ')}`);
|
|
92
97
|
}
|
|
93
98
|
}
|
|
99
|
+
if (!prevOptions.watch && this.options.watch) {
|
|
100
|
+
this.updateWatchedExtensions(this.extensions);
|
|
101
|
+
}
|
|
94
102
|
}
|
|
95
103
|
reload() {
|
|
96
104
|
this.reloadQueue.enqueue(async () => {
|
|
@@ -158,7 +166,7 @@ class ExtensionManager {
|
|
|
158
166
|
this.isLoaded = false;
|
|
159
167
|
}
|
|
160
168
|
initializeWatcher() {
|
|
161
|
-
if (
|
|
169
|
+
if (!this.watcher) {
|
|
162
170
|
logger_1.default.info('Watching extensions for changes...');
|
|
163
171
|
const localExtensionPaths = (env_1.default.SERVE_APP ? constants_1.EXTENSION_TYPES : constants_1.API_OR_HYBRID_EXTENSION_TYPES).flatMap((type) => {
|
|
164
172
|
const typeDir = path_1.default.posix.join(path_1.default.relative('.', env_1.default.EXTENSIONS_PATH).split(path_1.default.sep).join(path_1.default.posix.sep), (0, utils_1.pluralize)(type));
|
|
@@ -175,6 +183,11 @@ class ExtensionManager {
|
|
|
175
183
|
.on('unlink', () => this.reload());
|
|
176
184
|
}
|
|
177
185
|
}
|
|
186
|
+
async closeWatcher() {
|
|
187
|
+
if (this.watcher) {
|
|
188
|
+
await this.watcher.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
178
191
|
updateWatchedExtensions(added, removed = []) {
|
|
179
192
|
if (this.watcher) {
|
|
180
193
|
const toPackageExtensionPaths = (extensions) => extensions
|
package/dist/flows.js
CHANGED
|
@@ -119,7 +119,7 @@ class FlowManager {
|
|
|
119
119
|
return handler(data, context);
|
|
120
120
|
}
|
|
121
121
|
async load() {
|
|
122
|
-
var _a, _b, _c, _d
|
|
122
|
+
var _a, _b, _c, _d;
|
|
123
123
|
const flowsService = new services_1.FlowsService({ knex: (0, database_1.default)(), schema: await (0, get_schema_1.getSchema)() });
|
|
124
124
|
const flows = await flowsService.readByQuery({
|
|
125
125
|
filter: { status: { _eq: 'active' } },
|
|
@@ -129,19 +129,23 @@ class FlowManager {
|
|
|
129
129
|
const flowTrees = flows.map((flow) => (0, construct_flow_tree_1.constructFlowTree)(flow));
|
|
130
130
|
for (const flow of flowTrees) {
|
|
131
131
|
if (flow.trigger === 'event') {
|
|
132
|
-
const events = (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
132
|
+
const events = ((_a = flow.options) === null || _a === void 0 ? void 0 : _a.scope)
|
|
133
|
+
? (0, utils_1.toArray)(flow.options.scope)
|
|
134
|
+
.map((scope) => {
|
|
135
|
+
var _a, _b, _c;
|
|
136
|
+
if (['items.create', 'items.update', 'items.delete'].includes(scope)) {
|
|
137
|
+
return ((_c = (_b = (_a = flow.options) === null || _a === void 0 ? void 0 : _a.collections) === null || _b === void 0 ? void 0 : _b.map((collection) => {
|
|
138
|
+
if (collection.startsWith('directus_')) {
|
|
139
|
+
const action = scope.split('.')[1];
|
|
140
|
+
return collection.substring(9) + '.' + action;
|
|
141
|
+
}
|
|
142
|
+
return `${collection}.${scope}`;
|
|
143
|
+
})) !== null && _c !== void 0 ? _c : []);
|
|
144
|
+
}
|
|
145
|
+
return scope;
|
|
146
|
+
})
|
|
147
|
+
.flat()
|
|
148
|
+
: [];
|
|
145
149
|
if (flow.options.type === 'filter') {
|
|
146
150
|
const handler = (payload, meta, context) => this.executeFlow(flow, { payload, ...meta }, {
|
|
147
151
|
accountability: context.accountability,
|
|
@@ -196,9 +200,9 @@ class FlowManager {
|
|
|
196
200
|
return this.executeFlow(flow, data, context);
|
|
197
201
|
}
|
|
198
202
|
};
|
|
199
|
-
const method = (
|
|
203
|
+
const method = (_c = (_b = flow.options) === null || _b === void 0 ? void 0 : _b.method) !== null && _c !== void 0 ? _c : 'GET';
|
|
200
204
|
// Default return to $last for webhooks
|
|
201
|
-
flow.options.return = (
|
|
205
|
+
flow.options.return = (_d = flow.options.return) !== null && _d !== void 0 ? _d : '$last';
|
|
202
206
|
this.webhookFlowHandlers[`${method}-${flow.id}`] = handler;
|
|
203
207
|
}
|
|
204
208
|
else if (flow.trigger === 'manual') {
|
|
@@ -254,7 +258,7 @@ class FlowManager {
|
|
|
254
258
|
this.isLoaded = false;
|
|
255
259
|
}
|
|
256
260
|
async executeFlow(flow, data = null, context = {}) {
|
|
257
|
-
var _a, _b, _c, _d, _e, _f;
|
|
261
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
258
262
|
const database = (_a = context.database) !== null && _a !== void 0 ? _a : (0, database_1.default)();
|
|
259
263
|
const schema = (_b = context.schema) !== null && _b !== void 0 ? _b : (await (0, get_schema_1.getSchema)({ database }));
|
|
260
264
|
const keyedData = {
|
|
@@ -263,11 +267,13 @@ class FlowManager {
|
|
|
263
267
|
[ACCOUNTABILITY_KEY]: (_c = context === null || context === void 0 ? void 0 : context.accountability) !== null && _c !== void 0 ? _c : null,
|
|
264
268
|
};
|
|
265
269
|
let nextOperation = flow.operation;
|
|
270
|
+
let lastOperationStatus = 'unknown';
|
|
266
271
|
const steps = [];
|
|
267
272
|
while (nextOperation !== null) {
|
|
268
273
|
const { successor, data, status, options } = await this.executeOperation(nextOperation, keyedData, context);
|
|
269
274
|
keyedData[nextOperation.key] = data;
|
|
270
275
|
keyedData[LAST_KEY] = data;
|
|
276
|
+
lastOperationStatus = status;
|
|
271
277
|
steps.push({ operation: nextOperation.id, key: nextOperation.key, status, options });
|
|
272
278
|
nextOperation = successor;
|
|
273
279
|
}
|
|
@@ -283,6 +289,7 @@ class FlowManager {
|
|
|
283
289
|
collection: 'directus_flows',
|
|
284
290
|
ip: (_e = accountability === null || accountability === void 0 ? void 0 : accountability.ip) !== null && _e !== void 0 ? _e : null,
|
|
285
291
|
user_agent: (_f = accountability === null || accountability === void 0 ? void 0 : accountability.userAgent) !== null && _f !== void 0 ? _f : null,
|
|
292
|
+
origin: (_g = accountability === null || accountability === void 0 ? void 0 : accountability.origin) !== null && _g !== void 0 ? _g : null,
|
|
286
293
|
item: flow.id,
|
|
287
294
|
});
|
|
288
295
|
if (flow.accountability === 'all') {
|
|
@@ -301,6 +308,9 @@ class FlowManager {
|
|
|
301
308
|
});
|
|
302
309
|
}
|
|
303
310
|
}
|
|
311
|
+
if (flow.trigger === 'event' && flow.options.type === 'filter' && lastOperationStatus === 'reject') {
|
|
312
|
+
throw keyedData[LAST_KEY];
|
|
313
|
+
}
|
|
304
314
|
if (flow.options.return === '$all') {
|
|
305
315
|
return keyedData;
|
|
306
316
|
}
|
package/dist/mailer.js
CHANGED
|
@@ -37,6 +37,7 @@ function getMailer() {
|
|
|
37
37
|
}
|
|
38
38
|
const tls = (0, get_config_from_env_1.getConfigFromEnv)('EMAIL_SMTP_TLS_');
|
|
39
39
|
transporter = nodemailer_1.default.createTransport({
|
|
40
|
+
name: env_1.default.EMAIL_SMTP_NAME,
|
|
40
41
|
pool: env_1.default.EMAIL_SMTP_POOL,
|
|
41
42
|
host: env_1.default.EMAIL_SMTP_HOST,
|
|
42
43
|
port: env_1.default.EMAIL_SMTP_PORT,
|
|
@@ -3,5 +3,5 @@ import { NextFunction, Request, Response } from 'express';
|
|
|
3
3
|
* Verify the passed JWT and assign the user ID and role to `req`
|
|
4
4
|
*/
|
|
5
5
|
export declare const handler: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
6
|
-
declare const _default:
|
|
6
|
+
declare const _default: (req: Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: NextFunction) => Promise<void>;
|
|
7
7
|
export default _default;
|
|
@@ -24,6 +24,7 @@ const handler = async (req, res, next) => {
|
|
|
24
24
|
app: false,
|
|
25
25
|
ip: (0, get_ip_from_req_1.getIPFromReq)(req),
|
|
26
26
|
userAgent: req.get('user-agent'),
|
|
27
|
+
origin: req.get('origin'),
|
|
27
28
|
};
|
|
28
29
|
const database = (0, database_1.default)();
|
|
29
30
|
const customAccountability = await emitter_1.default.emitFilter('authenticate', defaultAccountability, {
|
|
@@ -34,7 +34,16 @@ test('Short-circuits when authenticate filter is used', async () => {
|
|
|
34
34
|
test('Uses default public accountability when no token is given', async () => {
|
|
35
35
|
const req = {
|
|
36
36
|
ip: '127.0.0.1',
|
|
37
|
-
get: jest.fn((string) =>
|
|
37
|
+
get: jest.fn((string) => {
|
|
38
|
+
switch (string) {
|
|
39
|
+
case 'user-agent':
|
|
40
|
+
return 'fake-user-agent';
|
|
41
|
+
case 'origin':
|
|
42
|
+
return 'fake-origin';
|
|
43
|
+
default:
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
38
47
|
};
|
|
39
48
|
const res = {};
|
|
40
49
|
const next = jest.fn();
|
|
@@ -47,6 +56,7 @@ test('Uses default public accountability when no token is given', async () => {
|
|
|
47
56
|
app: false,
|
|
48
57
|
ip: '127.0.0.1',
|
|
49
58
|
userAgent: 'fake-user-agent',
|
|
59
|
+
origin: 'fake-origin',
|
|
50
60
|
});
|
|
51
61
|
expect(next).toHaveBeenCalledTimes(1);
|
|
52
62
|
});
|
|
@@ -70,7 +80,16 @@ test('Sets accountability to payload contents if valid token is passed', async (
|
|
|
70
80
|
}, env_1.default.SECRET, { issuer: 'directus' });
|
|
71
81
|
const req = {
|
|
72
82
|
ip: '127.0.0.1',
|
|
73
|
-
get: jest.fn((string) =>
|
|
83
|
+
get: jest.fn((string) => {
|
|
84
|
+
switch (string) {
|
|
85
|
+
case 'user-agent':
|
|
86
|
+
return 'fake-user-agent';
|
|
87
|
+
case 'origin':
|
|
88
|
+
return 'fake-origin';
|
|
89
|
+
default:
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
74
93
|
token,
|
|
75
94
|
};
|
|
76
95
|
const res = {};
|
|
@@ -85,6 +104,7 @@ test('Sets accountability to payload contents if valid token is passed', async (
|
|
|
85
104
|
share_scope: shareScope,
|
|
86
105
|
ip: '127.0.0.1',
|
|
87
106
|
userAgent: 'fake-user-agent',
|
|
107
|
+
origin: 'fake-origin',
|
|
88
108
|
});
|
|
89
109
|
expect(next).toHaveBeenCalledTimes(1);
|
|
90
110
|
// Test with 1/0 instead or true/false
|
|
@@ -107,6 +127,7 @@ test('Sets accountability to payload contents if valid token is passed', async (
|
|
|
107
127
|
share_scope: shareScope,
|
|
108
128
|
ip: '127.0.0.1',
|
|
109
129
|
userAgent: 'fake-user-agent',
|
|
130
|
+
origin: 'fake-origin',
|
|
110
131
|
});
|
|
111
132
|
expect(next).toHaveBeenCalledTimes(1);
|
|
112
133
|
});
|
|
@@ -120,7 +141,16 @@ test('Throws InvalidCredentialsException when static token is used, but user doe
|
|
|
120
141
|
});
|
|
121
142
|
const req = {
|
|
122
143
|
ip: '127.0.0.1',
|
|
123
|
-
get: jest.fn((string) =>
|
|
144
|
+
get: jest.fn((string) => {
|
|
145
|
+
switch (string) {
|
|
146
|
+
case 'user-agent':
|
|
147
|
+
return 'fake-user-agent';
|
|
148
|
+
case 'origin':
|
|
149
|
+
return 'fake-origin';
|
|
150
|
+
default:
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}),
|
|
124
154
|
token: 'static-token',
|
|
125
155
|
};
|
|
126
156
|
const res = {};
|
|
@@ -131,7 +161,16 @@ test('Throws InvalidCredentialsException when static token is used, but user doe
|
|
|
131
161
|
test('Sets accountability to user information when static token is used', async () => {
|
|
132
162
|
const req = {
|
|
133
163
|
ip: '127.0.0.1',
|
|
134
|
-
get: jest.fn((string) =>
|
|
164
|
+
get: jest.fn((string) => {
|
|
165
|
+
switch (string) {
|
|
166
|
+
case 'user-agent':
|
|
167
|
+
return 'fake-user-agent';
|
|
168
|
+
case 'origin':
|
|
169
|
+
return 'fake-origin';
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
135
174
|
token: 'static-token',
|
|
136
175
|
};
|
|
137
176
|
const res = {};
|
|
@@ -144,6 +183,7 @@ test('Sets accountability to user information when static token is used', async
|
|
|
144
183
|
admin: testUser.admin_access,
|
|
145
184
|
ip: '127.0.0.1',
|
|
146
185
|
userAgent: 'fake-user-agent',
|
|
186
|
+
origin: 'fake-origin',
|
|
147
187
|
};
|
|
148
188
|
jest.mocked(database_1.default).mockReturnValue({
|
|
149
189
|
select: jest.fn().mockReturnThis(),
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => RequestHandler;
|
|
1
|
+
export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => (req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express").Response<any, Record<string, any>>, next: import("express").NextFunction) => Promise<void>;
|
|
@@ -14,39 +14,36 @@ const validateBatch = (scope) => (0, async_handler_1.default)(async (req, res, n
|
|
|
14
14
|
req.body = {};
|
|
15
15
|
return next();
|
|
16
16
|
}
|
|
17
|
+
if (req.method.toLowerCase() !== 'search' && scope !== 'read' && req.singleton) {
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
17
20
|
if (!req.body)
|
|
18
21
|
throw new exceptions_1.InvalidPayloadException('Payload in body is required');
|
|
19
|
-
if (req.
|
|
22
|
+
if (['update', 'delete'].includes(scope) && Array.isArray(req.body)) {
|
|
20
23
|
return next();
|
|
24
|
+
}
|
|
25
|
+
// In reads, the query in the body should override the query params for searching
|
|
26
|
+
if (scope === 'read' && req.body.query) {
|
|
27
|
+
req.sanitizedQuery = (0, sanitize_query_1.sanitizeQuery)(req.body.query, req.accountability);
|
|
28
|
+
}
|
|
21
29
|
// Every cRUD action has either keys or query
|
|
22
30
|
let batchSchema = joi_1.default.object().keys({
|
|
23
31
|
keys: joi_1.default.array().items(joi_1.default.alternatives(joi_1.default.string(), joi_1.default.number())),
|
|
24
32
|
query: joi_1.default.object().unknown(),
|
|
25
33
|
});
|
|
26
|
-
|
|
27
|
-
if (scope !== 'read') {
|
|
34
|
+
if (['update', 'delete'].includes(scope)) {
|
|
28
35
|
batchSchema = batchSchema.xor('query', 'keys');
|
|
29
36
|
}
|
|
30
37
|
// In updates, we add a required `data` that holds the update payload if an array isn't used
|
|
31
38
|
if (scope === 'update') {
|
|
32
|
-
if (Array.isArray(req.body))
|
|
33
|
-
return next();
|
|
34
39
|
batchSchema = batchSchema.keys({
|
|
35
40
|
data: joi_1.default.object().unknown().required(),
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
|
-
// In deletes, we want to keep supporting an array of just primary keys
|
|
39
|
-
if (scope === 'delete' && Array.isArray(req.body)) {
|
|
40
|
-
return next();
|
|
41
|
-
}
|
|
42
43
|
const { error } = batchSchema.validate(req.body);
|
|
43
44
|
if (error) {
|
|
44
45
|
throw new exceptions_2.FailedValidationException(error.details[0]);
|
|
45
46
|
}
|
|
46
|
-
// In reads, the query in the body should override the query params for searching
|
|
47
|
-
if (scope === 'read' && req.body.query) {
|
|
48
|
-
req.sanitizedQuery = (0, sanitize_query_1.sanitizeQuery)(req.body.query, req.accountability);
|
|
49
|
-
}
|
|
50
47
|
return next();
|
|
51
48
|
});
|
|
52
49
|
exports.validateBatch = validateBatch;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../../src/types/express.d.ts';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const validate_batch_1 = require("./validate-batch");
|
|
4
|
+
require("../../src/types/express.d.ts");
|
|
5
|
+
const exceptions_1 = require("../exceptions");
|
|
6
|
+
const exceptions_2 = require("@directus/shared/exceptions");
|
|
7
|
+
let mockRequest;
|
|
8
|
+
let mockResponse;
|
|
9
|
+
const nextFunction = jest.fn();
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockRequest = {};
|
|
12
|
+
mockResponse = {};
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
test('Sets body to empty, calls next on GET requests', async () => {
|
|
16
|
+
mockRequest.method = 'GET';
|
|
17
|
+
await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
|
|
18
|
+
expect(mockRequest.body).toEqual({});
|
|
19
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
20
|
+
});
|
|
21
|
+
test(`Short circuits on singletons that aren't queried through SEARCH`, async () => {
|
|
22
|
+
mockRequest.method = 'PATCH';
|
|
23
|
+
mockRequest.singleton = true;
|
|
24
|
+
mockRequest.body = { title: 'test' };
|
|
25
|
+
await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
|
|
26
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
test('Throws InvalidPayloadException on missing body', async () => {
|
|
29
|
+
mockRequest.method = 'SEARCH';
|
|
30
|
+
await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
|
|
31
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_1.InvalidPayloadException);
|
|
33
|
+
});
|
|
34
|
+
test(`Short circuits on Array body in update/delete use`, async () => {
|
|
35
|
+
mockRequest.method = 'PATCH';
|
|
36
|
+
mockRequest.body = [1, 2, 3];
|
|
37
|
+
await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
|
|
38
|
+
expect(mockRequest.sanitizedQuery).toBe(undefined);
|
|
39
|
+
expect(nextFunction).toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
test(`Sets sanitizedQuery based on body.query in read operations`, async () => {
|
|
42
|
+
mockRequest.method = 'SEARCH';
|
|
43
|
+
mockRequest.body = {
|
|
44
|
+
query: {
|
|
45
|
+
sort: 'id',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
|
|
49
|
+
expect(mockRequest.sanitizedQuery).toEqual({
|
|
50
|
+
sort: ['id'],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
test(`Doesn't allow both query and keys in a batch delete`, async () => {
|
|
54
|
+
mockRequest.method = 'DELETE';
|
|
55
|
+
mockRequest.body = {
|
|
56
|
+
keys: [1, 2, 3],
|
|
57
|
+
query: { filter: {} },
|
|
58
|
+
};
|
|
59
|
+
await (0, validate_batch_1.validateBatch)('delete')(mockRequest, mockResponse, nextFunction);
|
|
60
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_2.FailedValidationException);
|
|
62
|
+
});
|
|
63
|
+
test(`Requires 'data' on batch update`, async () => {
|
|
64
|
+
mockRequest.method = 'PATCH';
|
|
65
|
+
mockRequest.body = {
|
|
66
|
+
keys: [1, 2, 3],
|
|
67
|
+
query: { filter: {} },
|
|
68
|
+
};
|
|
69
|
+
await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
|
|
70
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_2.FailedValidationException);
|
|
72
|
+
});
|
|
73
|
+
test(`Calls next when all is well`, async () => {
|
|
74
|
+
mockRequest.method = 'PATCH';
|
|
75
|
+
mockRequest.body = {
|
|
76
|
+
query: { filter: {} },
|
|
77
|
+
data: {},
|
|
78
|
+
};
|
|
79
|
+
await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
|
|
80
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeUndefined();
|
|
82
|
+
});
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const utils_1 = require("@directus/shared/utils");
|
|
7
7
|
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const encodeurl_1 = __importDefault(require("encodeurl"));
|
|
8
9
|
exports.default = (0, utils_1.defineOperationApi)({
|
|
9
10
|
id: 'request',
|
|
10
11
|
handler: async ({ url, method, body, headers }) => {
|
|
@@ -16,9 +17,8 @@ exports.default = (0, utils_1.defineOperationApi)({
|
|
|
16
17
|
if (!customHeaders['Content-Type'] && isValidJSON(body)) {
|
|
17
18
|
customHeaders['Content-Type'] = 'application/json';
|
|
18
19
|
}
|
|
19
|
-
const shouldEncode = decodeURI(url) === url;
|
|
20
20
|
const result = await (0, axios_1.default)({
|
|
21
|
-
url:
|
|
21
|
+
url: (0, encodeurl_1.default)(url),
|
|
22
22
|
method,
|
|
23
23
|
data: body,
|
|
24
24
|
headers: customHeaders,
|
|
@@ -36,15 +36,23 @@ class AuthenticationService {
|
|
|
36
36
|
* to handle password existence checks elsewhere
|
|
37
37
|
*/
|
|
38
38
|
async login(providerName = constants_1.DEFAULT_AUTH_PROVIDER, payload, otp) {
|
|
39
|
-
var _a, _b;
|
|
40
|
-
const STALL_TIME =
|
|
39
|
+
var _a, _b, _c;
|
|
40
|
+
const STALL_TIME = env_1.default.LOGIN_STALL_TIME;
|
|
41
41
|
const timeStart = perf_hooks_1.performance.now();
|
|
42
42
|
const provider = (0, auth_1.getAuthProvider)(providerName);
|
|
43
|
+
let userId;
|
|
44
|
+
try {
|
|
45
|
+
userId = await provider.getUserID((0, lodash_1.cloneDeep)(payload));
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
await (0, stall_1.stall)(STALL_TIME, timeStart);
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
43
51
|
const user = await this.knex
|
|
44
52
|
.select('u.id', 'u.first_name', 'u.last_name', 'u.email', 'u.password', 'u.status', 'u.role', 'r.admin_access', 'r.app_access', 'u.tfa_secret', 'u.provider', 'u.external_identifier', 'u.auth_data')
|
|
45
53
|
.from('directus_users as u')
|
|
46
54
|
.leftJoin('directus_roles as r', 'u.role', 'r.id')
|
|
47
|
-
.where('u.id',
|
|
55
|
+
.where('u.id', userId)
|
|
48
56
|
.first();
|
|
49
57
|
const updatedPayload = await emitter_1.default.emitFilter('auth.login', payload, {
|
|
50
58
|
status: 'pending',
|
|
@@ -151,6 +159,7 @@ class AuthenticationService {
|
|
|
151
159
|
expires: refreshTokenExpiration,
|
|
152
160
|
ip: (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.ip,
|
|
153
161
|
user_agent: (_b = this.accountability) === null || _b === void 0 ? void 0 : _b.userAgent,
|
|
162
|
+
origin: (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.origin,
|
|
154
163
|
});
|
|
155
164
|
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
|
|
156
165
|
if (this.accountability) {
|
|
@@ -159,6 +168,7 @@ class AuthenticationService {
|
|
|
159
168
|
user: user.id,
|
|
160
169
|
ip: this.accountability.ip,
|
|
161
170
|
user_agent: this.accountability.userAgent,
|
|
171
|
+
origin: this.accountability.origin,
|
|
162
172
|
collection: 'directus_users',
|
|
163
173
|
item: user.id,
|
|
164
174
|
});
|
package/dist/services/files.js
CHANGED
|
@@ -46,6 +46,7 @@ const utils_1 = require("@directus/shared/utils");
|
|
|
46
46
|
const items_1 = require("./items");
|
|
47
47
|
const net_1 = __importDefault(require("net"));
|
|
48
48
|
const os_1 = __importDefault(require("os"));
|
|
49
|
+
const encodeurl_1 = __importDefault(require("encodeurl"));
|
|
49
50
|
const lookupDNS = (0, util_1.promisify)(dns_1.lookup);
|
|
50
51
|
class FilesService extends items_1.ItemsService {
|
|
51
52
|
constructor(options) {
|
|
@@ -229,8 +230,7 @@ class FilesService extends items_1.ItemsService {
|
|
|
229
230
|
}
|
|
230
231
|
let fileResponse;
|
|
231
232
|
try {
|
|
232
|
-
|
|
233
|
-
fileResponse = await axios_1.default.get(shouldEncode ? encodeURI(importURL) : importURL, {
|
|
233
|
+
fileResponse = await axios_1.default.get((0, encodeurl_1.default)(importURL), {
|
|
234
234
|
responseType: 'stream',
|
|
235
235
|
});
|
|
236
236
|
}
|
|
@@ -293,41 +293,43 @@ class GraphQLService {
|
|
|
293
293
|
return obj[field.field];
|
|
294
294
|
},
|
|
295
295
|
};
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
296
|
+
if (action === 'read') {
|
|
297
|
+
if (field.type === 'date') {
|
|
298
|
+
acc[`${field.field}_func`] = {
|
|
299
|
+
type: DateFunctions,
|
|
300
|
+
resolve: (obj) => {
|
|
301
|
+
const funcFields = Object.keys(DateFunctions.getFields()).map((key) => `${field.field}_${key}`);
|
|
302
|
+
return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (field.type === 'time') {
|
|
307
|
+
acc[`${field.field}_func`] = {
|
|
308
|
+
type: TimeFunctions,
|
|
309
|
+
resolve: (obj) => {
|
|
310
|
+
const funcFields = Object.keys(TimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
|
|
311
|
+
return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (field.type === 'dateTime' || field.type === 'timestamp') {
|
|
316
|
+
acc[`${field.field}_func`] = {
|
|
317
|
+
type: DateTimeFunctions,
|
|
318
|
+
resolve: (obj) => {
|
|
319
|
+
const funcFields = Object.keys(DateTimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
|
|
320
|
+
return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (field.type === 'json' || field.type === 'alias') {
|
|
325
|
+
acc[`${field.field}_func`] = {
|
|
326
|
+
type: CountFunctions,
|
|
327
|
+
resolve: (obj) => {
|
|
328
|
+
const funcFields = Object.keys(CountFunctions.getFields()).map((key) => `${field.field}_${key}`);
|
|
329
|
+
return (0, lodash_1.mapKeys)((0, lodash_1.pick)(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
331
333
|
}
|
|
332
334
|
return acc;
|
|
333
335
|
}, {}),
|
|
@@ -1615,6 +1617,7 @@ class GraphQLService {
|
|
|
1615
1617
|
const accountability = {
|
|
1616
1618
|
ip: req === null || req === void 0 ? void 0 : req.ip,
|
|
1617
1619
|
userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
|
|
1620
|
+
origin: req === null || req === void 0 ? void 0 : req.get('origin'),
|
|
1618
1621
|
role: null,
|
|
1619
1622
|
};
|
|
1620
1623
|
const authenticationService = new authentication_1.AuthenticationService({
|
|
@@ -1649,6 +1652,7 @@ class GraphQLService {
|
|
|
1649
1652
|
const accountability = {
|
|
1650
1653
|
ip: req === null || req === void 0 ? void 0 : req.ip,
|
|
1651
1654
|
userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
|
|
1655
|
+
origin: req === null || req === void 0 ? void 0 : req.get('origin'),
|
|
1652
1656
|
role: null,
|
|
1653
1657
|
};
|
|
1654
1658
|
const authenticationService = new authentication_1.AuthenticationService({
|
|
@@ -1685,6 +1689,7 @@ class GraphQLService {
|
|
|
1685
1689
|
const accountability = {
|
|
1686
1690
|
ip: req === null || req === void 0 ? void 0 : req.ip,
|
|
1687
1691
|
userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
|
|
1692
|
+
origin: req === null || req === void 0 ? void 0 : req.get('origin'),
|
|
1688
1693
|
role: null,
|
|
1689
1694
|
};
|
|
1690
1695
|
const authenticationService = new authentication_1.AuthenticationService({
|
|
@@ -1709,6 +1714,7 @@ class GraphQLService {
|
|
|
1709
1714
|
const accountability = {
|
|
1710
1715
|
ip: req === null || req === void 0 ? void 0 : req.ip,
|
|
1711
1716
|
userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
|
|
1717
|
+
origin: req === null || req === void 0 ? void 0 : req.get('origin'),
|
|
1712
1718
|
role: null,
|
|
1713
1719
|
};
|
|
1714
1720
|
const service = new users_1.UsersService({ accountability, schema: this.schema });
|
|
@@ -1733,6 +1739,7 @@ class GraphQLService {
|
|
|
1733
1739
|
const accountability = {
|
|
1734
1740
|
ip: req === null || req === void 0 ? void 0 : req.ip,
|
|
1735
1741
|
userAgent: req === null || req === void 0 ? void 0 : req.get('user-agent'),
|
|
1742
|
+
origin: req === null || req === void 0 ? void 0 : req.get('origin'),
|
|
1736
1743
|
role: null,
|
|
1737
1744
|
};
|
|
1738
1745
|
const service = new users_1.UsersService({ accountability, schema: this.schema });
|
|
@@ -2303,7 +2310,7 @@ class GraphQLService {
|
|
|
2303
2310
|
comment: (0, graphql_1.GraphQLNonNull)(graphql_1.GraphQLString),
|
|
2304
2311
|
},
|
|
2305
2312
|
resolve: async (_, args, __, info) => {
|
|
2306
|
-
var _a, _b, _c, _d, _e;
|
|
2313
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2307
2314
|
const service = new activity_1.ActivityService({
|
|
2308
2315
|
accountability: this.accountability,
|
|
2309
2316
|
schema: this.schema,
|
|
@@ -2314,9 +2321,10 @@ class GraphQLService {
|
|
|
2314
2321
|
user: (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.user,
|
|
2315
2322
|
ip: (_b = this.accountability) === null || _b === void 0 ? void 0 : _b.ip,
|
|
2316
2323
|
user_agent: (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.userAgent,
|
|
2324
|
+
origin: (_d = this.accountability) === null || _d === void 0 ? void 0 : _d.origin,
|
|
2317
2325
|
});
|
|
2318
2326
|
if ('directus_activity' in ReadCollectionTypes) {
|
|
2319
|
-
const selections = this.replaceFragmentsInSelections((
|
|
2327
|
+
const selections = this.replaceFragmentsInSelections((_f = (_e = info.fieldNodes[0]) === null || _e === void 0 ? void 0 : _e.selectionSet) === null || _f === void 0 ? void 0 : _f.selections, info.fragments);
|
|
2320
2328
|
const query = this.getQuery(args, selections || [], info.variableValues);
|
|
2321
2329
|
return await service.readOne(primaryKey, query);
|
|
2322
2330
|
}
|
package/dist/services/items.js
CHANGED
|
@@ -130,6 +130,7 @@ class ItemsService {
|
|
|
130
130
|
collection: this.collection,
|
|
131
131
|
ip: this.accountability.ip,
|
|
132
132
|
user_agent: this.accountability.userAgent,
|
|
133
|
+
origin: this.accountability.origin,
|
|
133
134
|
item: primaryKey,
|
|
134
135
|
});
|
|
135
136
|
// If revisions are tracked, create revisions record
|
|
@@ -214,7 +215,16 @@ class ItemsService {
|
|
|
214
215
|
* Get items by query
|
|
215
216
|
*/
|
|
216
217
|
async readByQuery(query, opts) {
|
|
217
|
-
|
|
218
|
+
const updatedQuery = (opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false
|
|
219
|
+
? await emitter_1.default.emitFilter(`${this.eventScope}.query`, query, {
|
|
220
|
+
collection: this.collection,
|
|
221
|
+
}, {
|
|
222
|
+
database: this.knex,
|
|
223
|
+
schema: this.schema,
|
|
224
|
+
accountability: this.accountability,
|
|
225
|
+
})
|
|
226
|
+
: query;
|
|
227
|
+
let ast = await (0, get_ast_from_query_1.default)(this.collection, updatedQuery, this.schema, {
|
|
218
228
|
accountability: this.accountability,
|
|
219
229
|
// By setting the permissions action, you can read items using the permissions for another
|
|
220
230
|
// operation's permissions. This is used to dynamically check if you have update/delete
|
|
@@ -240,7 +250,7 @@ class ItemsService {
|
|
|
240
250
|
}
|
|
241
251
|
const filteredRecords = (opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false
|
|
242
252
|
? await emitter_1.default.emitFilter(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, records, {
|
|
243
|
-
query,
|
|
253
|
+
query: updatedQuery,
|
|
244
254
|
collection: this.collection,
|
|
245
255
|
}, {
|
|
246
256
|
database: this.knex,
|
|
@@ -251,7 +261,7 @@ class ItemsService {
|
|
|
251
261
|
if ((opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false) {
|
|
252
262
|
emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, {
|
|
253
263
|
payload: filteredRecords,
|
|
254
|
-
query,
|
|
264
|
+
query: updatedQuery,
|
|
255
265
|
collection: this.collection,
|
|
256
266
|
}, {
|
|
257
267
|
database: this.knex || (0, database_1.default)(),
|
|
@@ -406,6 +416,7 @@ class ItemsService {
|
|
|
406
416
|
collection: this.collection,
|
|
407
417
|
ip: this.accountability.ip,
|
|
408
418
|
user_agent: this.accountability.userAgent,
|
|
419
|
+
origin: this.accountability.origin,
|
|
409
420
|
item: key,
|
|
410
421
|
})));
|
|
411
422
|
if (this.schema.collections[this.collection].accountability === 'all') {
|
|
@@ -573,6 +584,7 @@ class ItemsService {
|
|
|
573
584
|
collection: this.collection,
|
|
574
585
|
ip: this.accountability.ip,
|
|
575
586
|
user_agent: this.accountability.userAgent,
|
|
587
|
+
origin: this.accountability.origin,
|
|
576
588
|
item: key,
|
|
577
589
|
})));
|
|
578
590
|
}
|
package/dist/services/payload.js
CHANGED
|
@@ -538,10 +538,34 @@ class PayloadService {
|
|
|
538
538
|
if (error)
|
|
539
539
|
throw new exceptions_1.InvalidPayloadException(`Invalid one-to-many update structure: ${error.message}`);
|
|
540
540
|
if (alterations.create) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
541
|
+
const sortField = relation.meta.sort_field;
|
|
542
|
+
let createPayload;
|
|
543
|
+
if (sortField !== null) {
|
|
544
|
+
const highestOrderNumber = await this.knex
|
|
545
|
+
.from(relation.collection)
|
|
546
|
+
.where({ [relation.field]: parent })
|
|
547
|
+
.whereNotNull(sortField)
|
|
548
|
+
.max(sortField)
|
|
549
|
+
.first();
|
|
550
|
+
createPayload = alterations.create.map((item, index) => {
|
|
551
|
+
const record = (0, lodash_1.cloneDeep)(item);
|
|
552
|
+
// add sort field value if it is not supplied in the item
|
|
553
|
+
if (parent !== null && record[sortField] === undefined) {
|
|
554
|
+
record[sortField] = (highestOrderNumber === null || highestOrderNumber === void 0 ? void 0 : highestOrderNumber.max) ? highestOrderNumber.max + index + 1 : index + 1;
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
...record,
|
|
558
|
+
[relation.field]: parent || payload[currentPrimaryKeyField],
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
createPayload = alterations.create.map((item) => ({
|
|
564
|
+
...item,
|
|
565
|
+
[relation.field]: parent || payload[currentPrimaryKeyField],
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
await itemsService.createMany(createPayload, {
|
|
545
569
|
onRevisionCreate: (pk) => revisions.push(pk),
|
|
546
570
|
bypassEmitAction: (params) => nestedActionEvents.push(params),
|
|
547
571
|
emitEvents: opts === null || opts === void 0 ? void 0 : opts.emitEvents,
|
package/dist/services/server.js
CHANGED
|
@@ -143,7 +143,7 @@ class ServerService {
|
|
|
143
143
|
componentType: 'datastore',
|
|
144
144
|
observedUnit: 'ms',
|
|
145
145
|
observedValue: 0,
|
|
146
|
-
threshold: 150,
|
|
146
|
+
threshold: env_1.default.DB_HEALTHCHECK_THRESHOLD ? +env_1.default.DB_HEALTHCHECK_THRESHOLD : 150,
|
|
147
147
|
},
|
|
148
148
|
];
|
|
149
149
|
const startTime = perf_hooks_1.performance.now();
|
|
@@ -188,7 +188,7 @@ class ServerService {
|
|
|
188
188
|
componentType: 'cache',
|
|
189
189
|
observedValue: 0,
|
|
190
190
|
observedUnit: 'ms',
|
|
191
|
-
threshold: 150,
|
|
191
|
+
threshold: env_1.default.CACHE_HEALTHCHECK_THRESHOLD ? +env_1.default.CACHE_HEALTHCHECK_THRESHOLD : 150,
|
|
192
192
|
},
|
|
193
193
|
],
|
|
194
194
|
};
|
|
@@ -222,7 +222,7 @@ class ServerService {
|
|
|
222
222
|
componentType: 'ratelimiter',
|
|
223
223
|
observedValue: 0,
|
|
224
224
|
observedUnit: 'ms',
|
|
225
|
-
threshold: 150,
|
|
225
|
+
threshold: env_1.default.RATE_LIMITER_HEALTHCHECK_THRESHOLD ? +env_1.default.RATE_LIMITER_HEALTHCHECK_THRESHOLD : 150,
|
|
226
226
|
},
|
|
227
227
|
],
|
|
228
228
|
};
|
|
@@ -249,13 +249,14 @@ class ServerService {
|
|
|
249
249
|
const checks = {};
|
|
250
250
|
for (const location of (0, utils_1.toArray)(env_1.default.STORAGE_LOCATIONS)) {
|
|
251
251
|
const disk = storage_1.default.disk(location);
|
|
252
|
+
const envThresholdKey = `STORAGE_${location}_HEALTHCHECK_THRESHOLD`.toUpperCase();
|
|
252
253
|
checks[`storage:${location}:responseTime`] = [
|
|
253
254
|
{
|
|
254
255
|
status: 'ok',
|
|
255
256
|
componentType: 'objectstore',
|
|
256
257
|
observedValue: 0,
|
|
257
258
|
observedUnit: 'ms',
|
|
258
|
-
threshold: 750,
|
|
259
|
+
threshold: env_1.default[envThresholdKey] ? +env_1.default[envThresholdKey] : 750,
|
|
259
260
|
},
|
|
260
261
|
];
|
|
261
262
|
const startTime = perf_hooks_1.performance.now();
|
package/dist/services/shares.js
CHANGED
|
@@ -31,7 +31,7 @@ class SharesService extends items_1.ItemsService {
|
|
|
31
31
|
return super.createOne(data, opts);
|
|
32
32
|
}
|
|
33
33
|
async login(payload) {
|
|
34
|
-
var _a, _b;
|
|
34
|
+
var _a, _b, _c;
|
|
35
35
|
const record = await this.knex
|
|
36
36
|
.select({
|
|
37
37
|
share_id: 'id',
|
|
@@ -86,6 +86,7 @@ class SharesService extends items_1.ItemsService {
|
|
|
86
86
|
expires: refreshTokenExpiration,
|
|
87
87
|
ip: (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.ip,
|
|
88
88
|
user_agent: (_b = this.accountability) === null || _b === void 0 ? void 0 : _b.userAgent,
|
|
89
|
+
origin: (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.origin,
|
|
89
90
|
share: record.share_id,
|
|
90
91
|
});
|
|
91
92
|
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
* Handles promises in routes.
|
|
4
|
-
*/
|
|
5
|
-
declare function asyncHandler(handler: RequestHandler): RequestHandler;
|
|
6
|
-
declare function asyncHandler(handler: ErrorRequestHandler): ErrorRequestHandler;
|
|
1
|
+
import type { RequestHandler, Request, Response, NextFunction } from 'express';
|
|
2
|
+
declare const asyncHandler: (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
7
3
|
export default asyncHandler;
|
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
if (handler.length === 2 || handler.length === 3) {
|
|
5
|
-
const scoped = (req, res, next) => Promise.resolve(handler(req, res, next)).catch(next);
|
|
6
|
-
return scoped;
|
|
7
|
-
}
|
|
8
|
-
else if (handler.length === 4) {
|
|
9
|
-
const scoped = (err, req, res, next) => Promise.resolve(handler(err, req, res, next)).catch(next);
|
|
10
|
-
return scoped;
|
|
11
|
-
}
|
|
12
|
-
else {
|
|
13
|
-
throw new Error(`Failed to asyncHandle() function "${handler.name}"`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
3
|
+
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
|
16
4
|
exports.default = asyncHandler;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../../src/types/express.d.ts';
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
require("../../src/types/express.d.ts");
|
|
7
|
+
const async_handler_1 = __importDefault(require("./async-handler"));
|
|
8
|
+
let mockRequest;
|
|
9
|
+
let mockResponse;
|
|
10
|
+
const nextFunction = jest.fn();
|
|
11
|
+
test('Wraps async middleware in Promise resolve that will catch rejects and pass them to the nextFn', async () => {
|
|
12
|
+
const err = new Error('testing');
|
|
13
|
+
const middleware = async (_req, _res, _next) => {
|
|
14
|
+
throw err;
|
|
15
|
+
};
|
|
16
|
+
await (0, async_handler_1.default)(middleware)(mockRequest, mockResponse, nextFunction);
|
|
17
|
+
expect(nextFunction).toHaveBeenCalledWith(err);
|
|
18
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "directus",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.17.0",
|
|
4
4
|
"license": "GPL-3.0-only",
|
|
5
5
|
"homepage": "https://github.com/directus/directus#readme",
|
|
6
6
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
|
|
@@ -66,16 +66,16 @@
|
|
|
66
66
|
],
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@aws-sdk/client-ses": "^3.107.0",
|
|
69
|
-
"@directus/app": "9.
|
|
70
|
-
"@directus/drive": "9.
|
|
71
|
-
"@directus/drive-azure": "9.
|
|
72
|
-
"@directus/drive-gcs": "9.
|
|
73
|
-
"@directus/drive-s3": "9.
|
|
69
|
+
"@directus/app": "9.17.0",
|
|
70
|
+
"@directus/drive": "9.17.0",
|
|
71
|
+
"@directus/drive-azure": "9.17.0",
|
|
72
|
+
"@directus/drive-gcs": "9.17.0",
|
|
73
|
+
"@directus/drive-s3": "9.17.0",
|
|
74
74
|
"@directus/extensions-sdk": "^9.14.1",
|
|
75
75
|
"@directus/format-title": "^9.15.0",
|
|
76
|
-
"@directus/schema": "9.
|
|
77
|
-
"@directus/shared": "9.
|
|
78
|
-
"@directus/specs": "9.
|
|
76
|
+
"@directus/schema": "9.17.0",
|
|
77
|
+
"@directus/shared": "9.17.0",
|
|
78
|
+
"@directus/specs": "9.17.0",
|
|
79
79
|
"@godaddy/terminus": "^4.10.2",
|
|
80
80
|
"@rollup/plugin-alias": "^3.1.9",
|
|
81
81
|
"@rollup/plugin-virtual": "^2.1.0",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"deep-map": "^2.0.0",
|
|
98
98
|
"destroy": "^1.2.0",
|
|
99
99
|
"dotenv": "^10.0.0",
|
|
100
|
+
"encodeurl": "^1.0.2",
|
|
100
101
|
"eventemitter2": "^6.4.5",
|
|
101
102
|
"execa": "^5.1.1",
|
|
102
103
|
"exifr": "^7.1.3",
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
"mime-types": "^2.1.35",
|
|
127
128
|
"ms": "^2.1.3",
|
|
128
129
|
"nanoid": "^3.1.23",
|
|
129
|
-
"node-cron": "^3.0.
|
|
130
|
+
"node-cron": "^3.0.2",
|
|
130
131
|
"node-machine-id": "^1.1.12",
|
|
131
132
|
"nodemailer": "^6.7.5",
|
|
132
133
|
"object-hash": "^2.2.0",
|
|
@@ -175,6 +176,7 @@
|
|
|
175
176
|
"@types/cors": "2.8.12",
|
|
176
177
|
"@types/deep-diff": "1.0.1",
|
|
177
178
|
"@types/destroy": "1.0.0",
|
|
179
|
+
"@types/encodeurl": "^1.0.0",
|
|
178
180
|
"@types/express": "4.17.13",
|
|
179
181
|
"@types/express-pino-logger": "4.0.3",
|
|
180
182
|
"@types/express-serve-static-core": "^4.17.29",
|