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