@directus/api 22.2.0 → 23.1.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 +2 -0
- package/dist/auth/drivers/ldap.js +14 -3
- package/dist/auth/drivers/oauth2.js +13 -2
- package/dist/auth/drivers/openid.js +13 -2
- package/dist/cache.js +4 -4
- package/dist/cli/commands/init/questions.d.ts +5 -5
- package/dist/cli/commands/schema/apply.d.ts +1 -0
- package/dist/cli/commands/schema/apply.js +20 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/utils/create-env/env-stub.liquid +1 -4
- package/dist/controllers/activity.js +30 -27
- package/dist/controllers/assets.js +1 -1
- package/dist/controllers/comments.d.ts +2 -0
- package/dist/controllers/comments.js +153 -0
- package/dist/controllers/versions.js +10 -5
- package/dist/database/index.js +3 -0
- package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
- package/dist/database/migrations/20240806A-permissions-policies.js +1 -1
- package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
- package/dist/database/migrations/20240909A-separate-comments.js +65 -0
- package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
- package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
- package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
- package/dist/database/run-ast/lib/get-db-query.js +2 -2
- package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
- package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
- package/dist/database/run-ast/run-ast.js +8 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +0 -3
- package/dist/extensions/lib/sandbox/register/route.d.ts +1 -2
- package/dist/logger/index.d.ts +2 -3
- package/dist/logger/logs-stream.d.ts +0 -1
- package/dist/mailer.js +0 -6
- package/dist/middleware/authenticate.d.ts +1 -3
- package/dist/middleware/error-handler.d.ts +0 -1
- package/dist/middleware/validate-batch.d.ts +1 -4
- package/dist/permissions/lib/fetch-permissions.d.ts +11 -1
- package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +2 -2
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
- package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
- package/dist/permissions/modules/validate-access/validate-access.js +14 -1
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +1 -2
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +14 -6
- package/dist/permissions/utils/process-permissions.d.ts +11 -1
- package/dist/permissions/utils/process-permissions.js +6 -4
- package/dist/request/agent-with-ip-validation.d.ts +0 -1
- package/dist/server.d.ts +0 -3
- package/dist/services/activity.d.ts +1 -7
- package/dist/services/activity.js +0 -103
- package/dist/services/assets.d.ts +0 -1
- package/dist/services/assets.js +5 -4
- package/dist/services/collections.js +6 -4
- package/dist/services/comments.d.ts +31 -0
- package/dist/services/comments.js +374 -0
- package/dist/services/fields.js +0 -6
- package/dist/services/files/utils/get-metadata.d.ts +0 -1
- package/dist/services/files/utils/parse-image-metadata.d.ts +0 -1
- package/dist/services/files.d.ts +0 -1
- package/dist/services/graphql/index.js +17 -16
- package/dist/services/import-export.d.ts +0 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.js +3 -1
- package/dist/services/mail/index.d.ts +2 -1
- package/dist/services/mail/index.js +4 -1
- package/dist/services/payload.js +15 -14
- package/dist/services/tus/data-store.d.ts +0 -1
- package/dist/services/users.js +3 -2
- package/dist/services/versions.js +59 -44
- package/dist/types/graphql.d.ts +0 -1
- package/dist/utils/apply-diff.js +5 -6
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/compress.d.ts +0 -1
- package/dist/utils/delete-from-require-cache.js +1 -1
- package/dist/utils/fetch-user-count/fetch-user-count.d.ts +1 -2
- package/dist/utils/generate-hash.js +2 -2
- package/dist/utils/get-address.d.ts +0 -3
- package/dist/utils/get-cache-headers.d.ts +0 -1
- package/dist/utils/get-cache-key.d.ts +0 -1
- package/dist/utils/get-column.d.ts +1 -1
- package/dist/utils/get-graphql-query-and-variables.d.ts +0 -1
- package/dist/utils/get-ip-from-req.d.ts +0 -1
- package/dist/utils/get-service.js +3 -1
- package/dist/utils/get-snapshot.js +1 -1
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/utils/sanitize-schema.js +2 -0
- package/dist/utils/should-skip-cache.d.ts +0 -1
- package/dist/websocket/authenticate.js +1 -1
- package/dist/websocket/controllers/base.d.ts +1 -10
- package/dist/websocket/controllers/base.js +15 -21
- package/dist/websocket/controllers/graphql.d.ts +0 -3
- package/dist/websocket/controllers/index.d.ts +0 -3
- package/dist/websocket/controllers/logs.d.ts +4 -5
- package/dist/websocket/controllers/logs.js +7 -3
- package/dist/websocket/controllers/rest.d.ts +0 -3
- package/dist/websocket/controllers/rest.js +1 -1
- package/dist/websocket/types.d.ts +0 -6
- package/package.json +70 -71
package/dist/services/payload.js
CHANGED
|
@@ -3,7 +3,7 @@ import { parseJSON, toArray } from '@directus/utils';
|
|
|
3
3
|
import { format, isValid, parseISO } from 'date-fns';
|
|
4
4
|
import { unflatten } from 'flat';
|
|
5
5
|
import Joi from 'joi';
|
|
6
|
-
import { clone, cloneDeep, isNil, isObject, isPlainObject,
|
|
6
|
+
import { clone, cloneDeep, isNil, isObject, isPlainObject, pick } from 'lodash-es';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { parse as wktToGeoJSON } from 'wellknown';
|
|
9
9
|
import { getHelpers } from '../database/helpers/index.js';
|
|
@@ -347,22 +347,22 @@ export class PayloadService {
|
|
|
347
347
|
knex: this.knex,
|
|
348
348
|
schema: this.schema,
|
|
349
349
|
});
|
|
350
|
-
const
|
|
350
|
+
const relatedPrimaryKeyField = this.schema.collections[relatedCollection].primary;
|
|
351
351
|
const relatedRecord = payload[relation.field];
|
|
352
352
|
if (['string', 'number'].includes(typeof relatedRecord))
|
|
353
353
|
continue;
|
|
354
|
-
const hasPrimaryKey =
|
|
355
|
-
let relatedPrimaryKey = relatedRecord[
|
|
354
|
+
const hasPrimaryKey = relatedPrimaryKeyField in relatedRecord;
|
|
355
|
+
let relatedPrimaryKey = relatedRecord[relatedPrimaryKeyField];
|
|
356
356
|
const exists = hasPrimaryKey &&
|
|
357
357
|
!!(await this.knex
|
|
358
|
-
.select(
|
|
358
|
+
.select(relatedPrimaryKeyField)
|
|
359
359
|
.from(relatedCollection)
|
|
360
|
-
.where({ [
|
|
360
|
+
.where({ [relatedPrimaryKeyField]: relatedPrimaryKey })
|
|
361
361
|
.first());
|
|
362
362
|
if (exists) {
|
|
363
|
-
const
|
|
364
|
-
if (Object.keys(
|
|
365
|
-
await service.updateOne(relatedPrimaryKey,
|
|
363
|
+
const { [relatedPrimaryKeyField]: _, ...record } = relatedRecord;
|
|
364
|
+
if (Object.keys(record).length > 0) {
|
|
365
|
+
await service.updateOne(relatedPrimaryKey, record, {
|
|
366
366
|
onRevisionCreate: (pk) => revisions.push(pk),
|
|
367
367
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
368
368
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
@@ -425,9 +425,9 @@ export class PayloadService {
|
|
|
425
425
|
.where({ [relatedPrimaryKeyField]: relatedPrimaryKey })
|
|
426
426
|
.first());
|
|
427
427
|
if (exists) {
|
|
428
|
-
const
|
|
429
|
-
if (Object.keys(
|
|
430
|
-
await service.updateOne(relatedPrimaryKey,
|
|
428
|
+
const { [relatedPrimaryKeyField]: _, ...record } = relatedRecord;
|
|
429
|
+
if (Object.keys(record).length > 0) {
|
|
430
|
+
await service.updateOne(relatedPrimaryKey, record, {
|
|
431
431
|
onRevisionCreate: (pk) => revisions.push(pk),
|
|
432
432
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
433
433
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
@@ -610,8 +610,9 @@ export class PayloadService {
|
|
|
610
610
|
if (alterations.update) {
|
|
611
611
|
const primaryKeyField = this.schema.collections[relation.collection].primary;
|
|
612
612
|
for (const item of alterations.update) {
|
|
613
|
-
|
|
614
|
-
|
|
613
|
+
const { [primaryKeyField]: key, ...record } = item;
|
|
614
|
+
await service.updateOne(key, {
|
|
615
|
+
...record,
|
|
615
616
|
[relation.field]: parent || payload[currentPrimaryKeyField],
|
|
616
617
|
}, {
|
|
617
618
|
onRevisionCreate: (pk) => revisions.push(pk),
|
package/dist/services/users.js
CHANGED
|
@@ -260,6 +260,7 @@ export class UsersService extends ItemsService {
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
// Manual constraint, see https://github.com/directus/directus/pull/19912
|
|
263
|
+
await this.knex('directus_comments').update({ user_updated: null }).whereIn('user_updated', keys);
|
|
263
264
|
await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
|
|
264
265
|
await this.knex('directus_versions').update({ user_updated: null }).whereIn('user_updated', keys);
|
|
265
266
|
await super.deleteMany(keys, opts);
|
|
@@ -373,7 +374,7 @@ export class UsersService extends ItemsService {
|
|
|
373
374
|
await this.createOne(partialUser);
|
|
374
375
|
}
|
|
375
376
|
// We want to be able to re-send the verification email
|
|
376
|
-
else if (user.status !==
|
|
377
|
+
else if (user.status !== 'unverified') {
|
|
377
378
|
// To avoid giving attackers infos about registered emails we dont fail for violated unique constraints
|
|
378
379
|
await stall(STALL_TIME, timeStart);
|
|
379
380
|
return;
|
|
@@ -415,7 +416,7 @@ export class UsersService extends ItemsService {
|
|
|
415
416
|
if (scope !== 'pending-registration')
|
|
416
417
|
throw new ForbiddenError();
|
|
417
418
|
const user = await this.getUserByEmail(email);
|
|
418
|
-
if (user?.status !==
|
|
419
|
+
if (user?.status !== 'unverified') {
|
|
419
420
|
throw new InvalidPayloadError({ reason: 'Invalid verification code' });
|
|
420
421
|
}
|
|
421
422
|
await this.updateOne(user.id, { status: 'active' });
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Action } from '@directus/constants';
|
|
2
|
-
import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
|
|
2
|
+
import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
|
|
3
3
|
import Joi from 'joi';
|
|
4
4
|
import { assign, pick } from 'lodash-es';
|
|
5
5
|
import objectHash from 'object-hash';
|
|
6
6
|
import { getCache } from '../cache.js';
|
|
7
|
-
import getDatabase from '../database/index.js';
|
|
8
7
|
import emitter from '../emitter.js';
|
|
9
8
|
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
10
9
|
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
@@ -17,19 +16,36 @@ export class VersionsService extends ItemsService {
|
|
|
17
16
|
super('directus_versions', options);
|
|
18
17
|
}
|
|
19
18
|
async validateCreateData(data) {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const versionCreateSchema = Joi.object({
|
|
20
|
+
key: Joi.string().required(),
|
|
21
|
+
name: Joi.string().allow(null),
|
|
22
|
+
collection: Joi.string().required(),
|
|
23
|
+
item: Joi.string().required(),
|
|
24
|
+
});
|
|
25
|
+
const { error } = versionCreateSchema.validate(data);
|
|
26
|
+
if (error)
|
|
27
|
+
throw new InvalidPayloadError({ reason: error.message });
|
|
22
28
|
// Reserves the "main" version key for the version query parameter
|
|
23
29
|
if (data['key'] === 'main')
|
|
24
30
|
throw new InvalidPayloadError({ reason: `"main" is a reserved version key` });
|
|
25
|
-
if (
|
|
26
|
-
|
|
31
|
+
if (this.accountability) {
|
|
32
|
+
try {
|
|
33
|
+
await validateAccess({
|
|
34
|
+
accountability: this.accountability,
|
|
35
|
+
action: 'read',
|
|
36
|
+
collection: data['collection'],
|
|
37
|
+
primaryKeys: [data['item']],
|
|
38
|
+
}, {
|
|
39
|
+
schema: this.schema,
|
|
40
|
+
knex: this.knex,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new ForbiddenError();
|
|
45
|
+
}
|
|
27
46
|
}
|
|
28
|
-
if (!data['item'])
|
|
29
|
-
throw new InvalidPayloadError({ reason: `"item" is required` });
|
|
30
47
|
const { CollectionsService } = await import('./collections.js');
|
|
31
48
|
const collectionsService = new CollectionsService({
|
|
32
|
-
accountability: null,
|
|
33
49
|
knex: this.knex,
|
|
34
50
|
schema: this.schema,
|
|
35
51
|
});
|
|
@@ -39,7 +55,11 @@ export class VersionsService extends ItemsService {
|
|
|
39
55
|
reason: `Content Versioning is not enabled for collection "${data['collection']}"`,
|
|
40
56
|
});
|
|
41
57
|
}
|
|
42
|
-
const
|
|
58
|
+
const sudoService = new VersionsService({
|
|
59
|
+
knex: this.knex,
|
|
60
|
+
schema: this.schema,
|
|
61
|
+
});
|
|
62
|
+
const existingVersions = await sudoService.readByQuery({
|
|
43
63
|
aggregate: { count: ['*'] },
|
|
44
64
|
filter: { key: { _eq: data['key'] }, collection: { _eq: data['collection'] }, item: { _eq: data['item'] } },
|
|
45
65
|
});
|
|
@@ -48,32 +68,8 @@ export class VersionsService extends ItemsService {
|
|
|
48
68
|
reason: `Version "${data['key']}" already exists for item "${data['item']}" in collection "${data['collection']}"`,
|
|
49
69
|
});
|
|
50
70
|
}
|
|
51
|
-
// will throw an error if the accountability does not have permission to read the item
|
|
52
|
-
if (this.accountability) {
|
|
53
|
-
await validateAccess({
|
|
54
|
-
accountability: this.accountability,
|
|
55
|
-
action: 'read',
|
|
56
|
-
collection: data['collection'],
|
|
57
|
-
primaryKeys: [data['item']],
|
|
58
|
-
}, {
|
|
59
|
-
schema: this.schema,
|
|
60
|
-
knex: this.knex,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
71
|
}
|
|
64
72
|
async getMainItem(collection, item, query) {
|
|
65
|
-
// will throw an error if the accountability does not have permission to read the item
|
|
66
|
-
if (this.accountability) {
|
|
67
|
-
await validateAccess({
|
|
68
|
-
accountability: this.accountability,
|
|
69
|
-
action: 'read',
|
|
70
|
-
collection,
|
|
71
|
-
primaryKeys: [item],
|
|
72
|
-
}, {
|
|
73
|
-
schema: this.schema,
|
|
74
|
-
knex: this.knex,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
73
|
const itemsService = new ItemsService(collection, {
|
|
78
74
|
knex: this.knex,
|
|
79
75
|
accountability: this.accountability,
|
|
@@ -96,6 +92,7 @@ export class VersionsService extends ItemsService {
|
|
|
96
92
|
});
|
|
97
93
|
return result.map((revision) => revision['delta']);
|
|
98
94
|
}
|
|
95
|
+
// TODO: Remove legacy need to return a version array in subsequent release
|
|
99
96
|
async getVersionSaves(key, collection, item) {
|
|
100
97
|
const filter = {
|
|
101
98
|
key: { _eq: key },
|
|
@@ -107,6 +104,9 @@ export class VersionsService extends ItemsService {
|
|
|
107
104
|
const versions = await this.readByQuery({ filter });
|
|
108
105
|
if (!versions?.[0])
|
|
109
106
|
return null;
|
|
107
|
+
if (versions[0]['delta']) {
|
|
108
|
+
return [versions[0]['delta']];
|
|
109
|
+
}
|
|
110
110
|
const saves = await this.getVersionSavesById(versions[0]['id']);
|
|
111
111
|
return saves;
|
|
112
112
|
}
|
|
@@ -122,7 +122,6 @@ export class VersionsService extends ItemsService {
|
|
|
122
122
|
}
|
|
123
123
|
const keyCombos = new Set();
|
|
124
124
|
for (const item of data) {
|
|
125
|
-
await this.validateCreateData(item);
|
|
126
125
|
const keyCombo = `${item['key']}-${item['collection']}-${item['item']}`;
|
|
127
126
|
if (keyCombos.has(keyCombo)) {
|
|
128
127
|
throw new UnprocessableContentError({
|
|
@@ -130,8 +129,6 @@ export class VersionsService extends ItemsService {
|
|
|
130
129
|
});
|
|
131
130
|
}
|
|
132
131
|
keyCombos.add(keyCombo);
|
|
133
|
-
const mainItem = await this.getMainItem(item['collection'], item['item']);
|
|
134
|
-
item['hash'] = objectHash(mainItem);
|
|
135
132
|
}
|
|
136
133
|
return super.createMany(data, opts);
|
|
137
134
|
}
|
|
@@ -139,7 +136,7 @@ export class VersionsService extends ItemsService {
|
|
|
139
136
|
// Only allow updates on "key" and "name" fields
|
|
140
137
|
const versionUpdateSchema = Joi.object({
|
|
141
138
|
key: Joi.string(),
|
|
142
|
-
name: Joi.string().allow(null)
|
|
139
|
+
name: Joi.string().allow(null),
|
|
143
140
|
});
|
|
144
141
|
const { error } = versionUpdateSchema.validate(data);
|
|
145
142
|
if (error)
|
|
@@ -205,14 +202,25 @@ export class VersionsService extends ItemsService {
|
|
|
205
202
|
data: revisionDelta,
|
|
206
203
|
delta: revisionDelta,
|
|
207
204
|
});
|
|
205
|
+
let existingDelta = version['delta'];
|
|
206
|
+
if (!existingDelta) {
|
|
207
|
+
const saves = await this.getVersionSavesById(key);
|
|
208
|
+
existingDelta = assign({}, ...saves);
|
|
209
|
+
}
|
|
210
|
+
const finalVersionDelta = assign({}, existingDelta, revisionDelta ? JSON.parse(revisionDelta) : null);
|
|
211
|
+
const sudoService = new ItemsService(this.collection, {
|
|
212
|
+
knex: this.knex,
|
|
213
|
+
schema: this.schema,
|
|
214
|
+
});
|
|
215
|
+
await sudoService.updateOne(key, { delta: finalVersionDelta });
|
|
208
216
|
const { cache } = getCache();
|
|
209
217
|
if (shouldClearCache(cache, undefined, collection)) {
|
|
210
218
|
cache.clear();
|
|
211
219
|
}
|
|
212
|
-
return
|
|
220
|
+
return finalVersionDelta;
|
|
213
221
|
}
|
|
214
222
|
async promote(version, mainHash, fields) {
|
|
215
|
-
const { id, collection, item } = (await this.readOne(version));
|
|
223
|
+
const { id, collection, item, delta } = (await this.readOne(version));
|
|
216
224
|
// will throw an error if the accountability does not have permission to update the item
|
|
217
225
|
if (this.accountability) {
|
|
218
226
|
await validateAccess({
|
|
@@ -231,11 +239,18 @@ export class VersionsService extends ItemsService {
|
|
|
231
239
|
reason: `Main item has changed since this version was last updated`,
|
|
232
240
|
});
|
|
233
241
|
}
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
let versionResult;
|
|
243
|
+
if (delta) {
|
|
244
|
+
versionResult = delta;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const saves = await this.getVersionSavesById(id);
|
|
248
|
+
versionResult = assign({}, ...saves);
|
|
249
|
+
}
|
|
236
250
|
const payloadToUpdate = fields ? pick(versionResult, fields) : versionResult;
|
|
237
251
|
const itemsService = new ItemsService(collection, {
|
|
238
252
|
accountability: this.accountability,
|
|
253
|
+
knex: this.knex,
|
|
239
254
|
schema: this.schema,
|
|
240
255
|
});
|
|
241
256
|
const payloadAfterHooks = await emitter.emitFilter(['items.promote', `${collection}.items.promote`], payloadToUpdate, {
|
|
@@ -243,7 +258,7 @@ export class VersionsService extends ItemsService {
|
|
|
243
258
|
item,
|
|
244
259
|
version,
|
|
245
260
|
}, {
|
|
246
|
-
database:
|
|
261
|
+
database: this.knex,
|
|
247
262
|
schema: this.schema,
|
|
248
263
|
accountability: this.accountability,
|
|
249
264
|
});
|
|
@@ -254,7 +269,7 @@ export class VersionsService extends ItemsService {
|
|
|
254
269
|
item: updatedItemKey,
|
|
255
270
|
version,
|
|
256
271
|
}, {
|
|
257
|
-
database:
|
|
272
|
+
database: this.knex,
|
|
258
273
|
schema: this.schema,
|
|
259
274
|
accountability: this.accountability,
|
|
260
275
|
});
|
package/dist/types/graphql.d.ts
CHANGED
package/dist/utils/apply-diff.js
CHANGED
|
@@ -26,7 +26,6 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
|
|
|
26
26
|
await transaction(database, async (trx) => {
|
|
27
27
|
const collectionsService = new CollectionsService({ knex: trx, schema });
|
|
28
28
|
const getNestedCollectionsToCreate = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => diff[0].rhs?.meta?.group === currentLevelCollection);
|
|
29
|
-
const getNestedCollectionsToDelete = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => diff[0].lhs?.meta?.group === currentLevelCollection);
|
|
30
29
|
const createCollections = async (collections) => {
|
|
31
30
|
for (const { collection, diff } of collections) {
|
|
32
31
|
if (diff?.[0]?.kind === DiffKind.NEW && diff[0].rhs) {
|
|
@@ -82,7 +81,6 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
|
|
|
82
81
|
// clean up deleted relations from existing schema
|
|
83
82
|
schema.relations = schema.relations.filter((r) => r.related_collection !== collection && r.collection !== collection);
|
|
84
83
|
}
|
|
85
|
-
await deleteCollections(getNestedCollectionsToDelete(collection));
|
|
86
84
|
try {
|
|
87
85
|
await collectionsService.deleteOne(collection, mutationOptions);
|
|
88
86
|
}
|
|
@@ -122,13 +120,14 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
|
|
|
122
120
|
// Create top level collections (no group, or highest level in existing group) first,
|
|
123
121
|
// then continue with nested collections recursively
|
|
124
122
|
await createCollections(snapshotDiff.collections.filter(filterCollectionsForCreation));
|
|
125
|
-
|
|
126
|
-
await deleteCollections(snapshotDiff.collections.filter(({ diff }) => {
|
|
123
|
+
const collectionsToDelete = snapshotDiff.collections.filter(({ diff }) => {
|
|
127
124
|
if (diff.length === 0 || diff[0] === undefined)
|
|
128
125
|
return false;
|
|
129
126
|
const collectionDiff = diff[0];
|
|
130
|
-
return collectionDiff.kind === DiffKind.DELETE
|
|
131
|
-
})
|
|
127
|
+
return collectionDiff.kind === DiffKind.DELETE;
|
|
128
|
+
});
|
|
129
|
+
if (collectionsToDelete.length > 0)
|
|
130
|
+
await deleteCollections(collectionsToDelete);
|
|
132
131
|
for (const { collection, diff } of snapshotDiff.collections) {
|
|
133
132
|
if (diff?.[0]?.kind === DiffKind.EDIT || diff?.[0]?.kind === DiffKind.ARRAY) {
|
|
134
133
|
const currentCollection = currentSnapshot.collections.find((field) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Aggregate, Filter, Permission, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { AliasMap } from './get-column-path.js';
|
|
4
|
-
export declare const generateAlias: (size?: number
|
|
4
|
+
export declare const generateAlias: (size?: number) => string;
|
|
5
5
|
type ApplyQueryOptions = {
|
|
6
6
|
aliasMap?: AliasMap;
|
|
7
7
|
isInnerQuery?: boolean;
|
package/dist/utils/compress.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export function deleteFromRequireCache(modulePath) {
|
|
|
7
7
|
const moduleCachePath = require.resolve(modulePath);
|
|
8
8
|
delete require.cache[moduleCachePath];
|
|
9
9
|
}
|
|
10
|
-
catch
|
|
10
|
+
catch {
|
|
11
11
|
logger.trace(`Module cache not found for ${modulePath}, skipped cache delete.`);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type FetchAccessLookupOptions } from './fetch-access-lookup.js';
|
|
2
|
-
export
|
|
3
|
-
}
|
|
2
|
+
export type FetchUserCountOptions = FetchAccessLookupOptions;
|
|
4
3
|
export interface UserCount {
|
|
5
4
|
admin: number;
|
|
6
5
|
app: number;
|
|
@@ -3,7 +3,7 @@ import { getConfigFromEnv } from './get-config-from-env.js';
|
|
|
3
3
|
export function generateHash(stringToHash) {
|
|
4
4
|
const argon2HashConfigOptions = getConfigFromEnv('HASH_', 'HASH_RAW'); // Disallow the HASH_RAW option, see https://github.com/directus/directus/discussions/7670#discussioncomment-1255805
|
|
5
5
|
// associatedData, if specified, must be passed as a Buffer to argon2.hash, see https://github.com/ranisalt/node-argon2/wiki/Options#associateddata
|
|
6
|
-
'associatedData' in argon2HashConfigOptions
|
|
7
|
-
|
|
6
|
+
if ('associatedData' in argon2HashConfigOptions)
|
|
7
|
+
argon2HashConfigOptions['associatedData'] = Buffer.from(argon2HashConfigOptions['associatedData']);
|
|
8
8
|
return argon2.hash(stringToHash, argon2HashConfigOptions);
|
|
9
9
|
}
|
|
@@ -21,5 +21,5 @@ type GetColumnOptions = OriginalCollectionName | (FunctionColumnOptions & Origin
|
|
|
21
21
|
* @param options Optional parameters
|
|
22
22
|
* @returns Knex raw instance
|
|
23
23
|
*/
|
|
24
|
-
export declare function getColumn(knex: Knex, table: string, column: string, alias: string | false | undefined, schema: SchemaOverview, options?: GetColumnOptions): Knex.Raw;
|
|
24
|
+
export declare function getColumn(knex: Knex, table: string, column: string, alias: (string | false) | undefined, schema: SchemaOverview, options?: GetColumnOptions): Knex.Raw;
|
|
25
25
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ForbiddenError } from '@directus/errors';
|
|
2
|
-
import { AccessService, ActivityService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PoliciesService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, TranslationsService, UsersService, VersionsService, WebhooksService, } from '../services/index.js';
|
|
2
|
+
import { AccessService, ActivityService, CommentsService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PoliciesService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, TranslationsService, UsersService, VersionsService, WebhooksService, } from '../services/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Select the correct service for the given collection. This allows the individual services to run
|
|
5
5
|
* their custom checks (f.e. it allows `UsersService` to prevent updating TFA secret from outside).
|
|
@@ -10,6 +10,8 @@ export function getService(collection, opts) {
|
|
|
10
10
|
return new AccessService(opts);
|
|
11
11
|
case 'directus_activity':
|
|
12
12
|
return new ActivityService(opts);
|
|
13
|
+
case 'directus_comments':
|
|
14
|
+
return new CommentsService({ ...opts, serviceOrigin: 'comments' });
|
|
13
15
|
case 'directus_dashboards':
|
|
14
16
|
return new DashboardsService(opts);
|
|
15
17
|
case 'directus_files':
|
|
@@ -16,7 +16,7 @@ export declare function sanitizeCollection(collection: Collection | undefined):
|
|
|
16
16
|
* @returns sanitized field
|
|
17
17
|
*/
|
|
18
18
|
export declare function sanitizeField(field: Field | undefined, sanitizeAllSchema?: boolean): Partial<Field> | undefined;
|
|
19
|
-
export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "name" | "numeric_precision" | "data_type" | "default_value" | "max_length" | "numeric_scale" | "is_nullable" | "is_unique" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment" | "foreign_key_table" | "foreign_key_column">;
|
|
19
|
+
export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "name" | "numeric_precision" | "data_type" | "default_value" | "max_length" | "numeric_scale" | "is_nullable" | "is_unique" | "is_indexed" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment" | "foreign_key_table" | "foreign_key_column">;
|
|
20
20
|
/**
|
|
21
21
|
* Pick certain database vendor specific relation properties that should be compared when performing diff
|
|
22
22
|
*
|
|
@@ -34,6 +34,7 @@ export function sanitizeField(field, sanitizeAllSchema = false) {
|
|
|
34
34
|
'schema.numeric_scale',
|
|
35
35
|
'schema.is_nullable',
|
|
36
36
|
'schema.is_unique',
|
|
37
|
+
'schema.is_indexed',
|
|
37
38
|
'schema.is_primary_key',
|
|
38
39
|
'schema.is_generated',
|
|
39
40
|
'schema.generation_expression',
|
|
@@ -54,6 +55,7 @@ export function sanitizeColumn(column) {
|
|
|
54
55
|
'numeric_scale',
|
|
55
56
|
'is_nullable',
|
|
56
57
|
'is_unique',
|
|
58
|
+
'is_indexed',
|
|
57
59
|
'is_primary_key',
|
|
58
60
|
'is_generated',
|
|
59
61
|
'generation_expression',
|
|
@@ -28,7 +28,7 @@ export async function authenticateConnection(message) {
|
|
|
28
28
|
const expires_at = getExpiresAtForToken(access_token);
|
|
29
29
|
return { accountability, expires_at, refresh_token };
|
|
30
30
|
}
|
|
31
|
-
catch
|
|
31
|
+
catch {
|
|
32
32
|
throw new WebSocketError('auth', 'AUTH_FAILED', 'Authentication failed.', message['uid']);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
4
|
-
/// <reference types="node/http.js" />
|
|
5
|
-
/// <reference types="pino-http" />
|
|
6
1
|
import type { Accountability } from '@directus/types';
|
|
7
2
|
import type { IncomingMessage, Server as httpServer } from 'http';
|
|
8
3
|
import type { RateLimiterAbstract } from 'rate-limiter-flexible';
|
|
@@ -32,12 +27,8 @@ export default abstract class SocketController {
|
|
|
32
27
|
createClient(ws: WebSocket, { accountability, expires_at }: AuthenticationState): WebSocketClient;
|
|
33
28
|
protected parseMessage(data: string): WebSocketMessage;
|
|
34
29
|
protected handleAuthRequest(client: WebSocketClient, message: WebSocketAuthMessage): Promise<void>;
|
|
30
|
+
protected checkUserRequirements(_accountability: Accountability | null): void;
|
|
35
31
|
setTokenExpireTimer(client: WebSocketClient): void;
|
|
36
32
|
checkClientTokens(): void;
|
|
37
|
-
meetsAdminRequirement({ socket, client, accountability, }: {
|
|
38
|
-
socket?: UpgradeContext['socket'];
|
|
39
|
-
client?: WebSocketClient | WebSocket;
|
|
40
|
-
accountability: Accountability | null;
|
|
41
|
-
}): boolean;
|
|
42
33
|
terminate(): void;
|
|
43
34
|
}
|
|
@@ -60,7 +60,6 @@ export default class SocketController {
|
|
|
60
60
|
authentication: {
|
|
61
61
|
mode: authMode.data,
|
|
62
62
|
timeout: authTimeout,
|
|
63
|
-
requireAdmin: false,
|
|
64
63
|
},
|
|
65
64
|
};
|
|
66
65
|
}
|
|
@@ -140,8 +139,15 @@ export default class SocketController {
|
|
|
140
139
|
socket.destroy();
|
|
141
140
|
return;
|
|
142
141
|
}
|
|
143
|
-
|
|
142
|
+
try {
|
|
143
|
+
this.checkUserRequirements(accountability);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
logger.debug('WebSocket upgrade denied - ' + JSON.stringify(accountability || 'invalid'));
|
|
147
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
148
|
+
socket.destroy();
|
|
144
149
|
return;
|
|
150
|
+
}
|
|
145
151
|
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
146
152
|
this.catchInvalidMessages(ws);
|
|
147
153
|
const state = { accountability, expires_at };
|
|
@@ -156,8 +162,7 @@ export default class SocketController {
|
|
|
156
162
|
if (getMessageType(payload) !== 'auth')
|
|
157
163
|
throw new Error();
|
|
158
164
|
const state = await authenticateConnection(WebSocketAuthMessage.parse(payload));
|
|
159
|
-
|
|
160
|
-
return;
|
|
165
|
+
this.checkUserRequirements(state.accountability);
|
|
161
166
|
ws.send(authenticationSuccess(payload['uid'], state.refresh_token));
|
|
162
167
|
this.server.emit('connection', ws, state);
|
|
163
168
|
}
|
|
@@ -238,7 +243,7 @@ export default class SocketController {
|
|
|
238
243
|
try {
|
|
239
244
|
message = WebSocketMessage.parse(parseJSON(data));
|
|
240
245
|
}
|
|
241
|
-
catch
|
|
246
|
+
catch {
|
|
242
247
|
throw new WebSocketError('server', 'INVALID_PAYLOAD', 'Unable to parse the incoming message.');
|
|
243
248
|
}
|
|
244
249
|
return message;
|
|
@@ -246,8 +251,7 @@ export default class SocketController {
|
|
|
246
251
|
async handleAuthRequest(client, message) {
|
|
247
252
|
try {
|
|
248
253
|
const { accountability, expires_at, refresh_token } = await authenticateConnection(message);
|
|
249
|
-
|
|
250
|
-
return;
|
|
254
|
+
this.checkUserRequirements(accountability);
|
|
251
255
|
client.accountability = accountability;
|
|
252
256
|
client.expires_at = expires_at;
|
|
253
257
|
this.setTokenExpireTimer(client);
|
|
@@ -269,6 +273,10 @@ export default class SocketController {
|
|
|
269
273
|
}
|
|
270
274
|
}
|
|
271
275
|
}
|
|
276
|
+
checkUserRequirements(_accountability) {
|
|
277
|
+
// there are no requirements in the abstract class
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
272
280
|
setTokenExpireTimer(client) {
|
|
273
281
|
if (client.auth_timer !== null) {
|
|
274
282
|
// clear up old timeouts if needed
|
|
@@ -305,20 +313,6 @@ export default class SocketController {
|
|
|
305
313
|
}
|
|
306
314
|
}, TOKEN_CHECK_INTERVAL);
|
|
307
315
|
}
|
|
308
|
-
meetsAdminRequirement({ socket, client, accountability, }) {
|
|
309
|
-
if (!this.authentication.requireAdmin || accountability?.admin)
|
|
310
|
-
return true;
|
|
311
|
-
logger.debug('WebSocket connection denied - ' + JSON.stringify(accountability || 'invalid'));
|
|
312
|
-
if (socket) {
|
|
313
|
-
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
314
|
-
socket.destroy();
|
|
315
|
-
}
|
|
316
|
-
else if (client) {
|
|
317
|
-
handleWebSocketError(client, new WebSocketError('auth', 'UNAUTHORIZED', 'Unauthorized.'), 'auth');
|
|
318
|
-
client.close();
|
|
319
|
-
}
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
316
|
terminate() {
|
|
323
317
|
if (this.authInterval)
|
|
324
318
|
clearInterval(this.authInterval);
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node/http.js" />
|
|
3
|
-
/// <reference types="pino-http" />
|
|
4
1
|
import type { Server } from 'graphql-ws';
|
|
5
2
|
import type { Server as httpServer } from 'http';
|
|
6
3
|
import type { GraphQLSocket, UpgradeContext, WebSocketClient } from '../types.js';
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node/http.js" />
|
|
3
|
-
/// <reference types="pino-http" />
|
|
4
1
|
import type { Server as httpServer } from 'http';
|
|
5
2
|
import { GraphQLSubscriptionController } from './graphql.js';
|
|
6
3
|
import { LogsController } from './logs.js';
|