@directus/api 16.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/controllers/permissions.js +11 -2
- package/dist/controllers/utils.js +13 -32
- package/dist/middleware/respond.js +1 -1
- package/dist/services/assets.js +1 -3
- package/dist/services/authorization.d.ts +1 -1
- package/dist/services/authorization.js +15 -3
- package/dist/services/{import-export/index.d.ts → import-export.d.ts} +1 -1
- package/dist/services/{import-export/index.js → import-export.js} +10 -10
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/permissions.d.ts +3 -2
- package/dist/services/permissions.js +76 -1
- package/dist/services/roles.js +83 -15
- package/dist/services/server.js +2 -1
- package/dist/types/items.d.ts +4 -12
- package/dist/types/items.js +0 -4
- package/package.json +18 -18
- package/dist/services/import-export/import-worker.d.ts +0 -9
- package/dist/services/import-export/import-worker.js +0 -9
- package/dist/worker-pool.d.ts +0 -2
- package/dist/worker-pool.js +0 -19
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { isDirectusError } from '@directus/errors';
|
|
1
|
+
import { ErrorCode, isDirectusError } from '@directus/errors';
|
|
2
2
|
import express from 'express';
|
|
3
|
-
import { ErrorCode } from '@directus/errors';
|
|
4
3
|
import { respond } from '../middleware/respond.js';
|
|
5
4
|
import useCollection from '../middleware/use-collection.js';
|
|
6
5
|
import { validateBatch } from '../middleware/validate-batch.js';
|
|
@@ -152,4 +151,14 @@ router.delete('/:pk', asyncHandler(async (req, _res, next) => {
|
|
|
152
151
|
await service.deleteOne(req.params['pk']);
|
|
153
152
|
return next();
|
|
154
153
|
}), respond);
|
|
154
|
+
router.get('/me/:collection/:pk?', asyncHandler(async (req, res, next) => {
|
|
155
|
+
const { collection, pk } = req.params;
|
|
156
|
+
const service = new PermissionsService({
|
|
157
|
+
accountability: req.accountability,
|
|
158
|
+
schema: req.schema,
|
|
159
|
+
});
|
|
160
|
+
const itemPermissions = await service.getItemPermissions(collection, pk);
|
|
161
|
+
res.locals['payload'] = { data: itemPermissions };
|
|
162
|
+
return next();
|
|
163
|
+
}), respond);
|
|
155
164
|
export default router;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
import { InvalidPayloadError, InvalidQueryError, UnsupportedMediaTypeError } from '@directus/errors';
|
|
1
2
|
import argon2 from 'argon2';
|
|
2
3
|
import Busboy from 'busboy';
|
|
3
4
|
import { Router } from 'express';
|
|
4
5
|
import Joi from 'joi';
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import { createRequire } from 'node:module';
|
|
7
|
-
import { InvalidPayloadError, InvalidQueryError, UnsupportedMediaTypeError } from '@directus/errors';
|
|
8
6
|
import collectionExists from '../middleware/collection-exists.js';
|
|
9
7
|
import { respond } from '../middleware/respond.js';
|
|
10
|
-
import { ExportService } from '../services/import-export
|
|
8
|
+
import { ExportService, ImportService } from '../services/import-export.js';
|
|
11
9
|
import { RevisionsService } from '../services/revisions.js';
|
|
12
10
|
import { UtilsService } from '../services/utils.js';
|
|
13
11
|
import asyncHandler from '../utils/async-handler.js';
|
|
@@ -66,6 +64,10 @@ router.post('/import/:collection', collectionExists, asyncHandler(async (req, re
|
|
|
66
64
|
if (req.is('multipart/form-data') === false) {
|
|
67
65
|
throw new UnsupportedMediaTypeError({ mediaType: req.headers['content-type'], where: 'Content-Type header' });
|
|
68
66
|
}
|
|
67
|
+
const service = new ImportService({
|
|
68
|
+
accountability: req.accountability,
|
|
69
|
+
schema: req.schema,
|
|
70
|
+
});
|
|
69
71
|
let headers;
|
|
70
72
|
if (req.headers['content-type']) {
|
|
71
73
|
headers = req.headers;
|
|
@@ -78,34 +80,13 @@ router.post('/import/:collection', collectionExists, asyncHandler(async (req, re
|
|
|
78
80
|
}
|
|
79
81
|
const busboy = Busboy({ headers });
|
|
80
82
|
busboy.on('file', async (_fieldname, fileStream, { mimeType }) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const workerPool = getWorkerPool();
|
|
89
|
-
const require = createRequire(import.meta.url);
|
|
90
|
-
const filename = require.resolve('../services/import-export/import-worker');
|
|
91
|
-
const workerData = {
|
|
92
|
-
collection: req.params['collection'],
|
|
93
|
-
mimeType,
|
|
94
|
-
filePath: tmpFile.path,
|
|
95
|
-
accountability: req.accountability,
|
|
96
|
-
schema: req.schema,
|
|
97
|
-
};
|
|
98
|
-
try {
|
|
99
|
-
await workerPool.run(workerData, { filename });
|
|
100
|
-
res.status(200).end();
|
|
101
|
-
}
|
|
102
|
-
catch (error) {
|
|
103
|
-
next(error);
|
|
104
|
-
}
|
|
105
|
-
finally {
|
|
106
|
-
await tmpFile.cleanup();
|
|
107
|
-
}
|
|
108
|
-
});
|
|
83
|
+
try {
|
|
84
|
+
await service.import(req.params['collection'], mimeType, fileStream);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return next(err);
|
|
88
|
+
}
|
|
89
|
+
return res.status(200).end();
|
|
109
90
|
});
|
|
110
91
|
busboy.on('error', (err) => next(err));
|
|
111
92
|
req.pipe(busboy);
|
|
@@ -3,7 +3,7 @@ import { parse as parseBytesConfiguration } from 'bytes';
|
|
|
3
3
|
import { assign } from 'lodash-es';
|
|
4
4
|
import { getCache, setCacheValue } from '../cache.js';
|
|
5
5
|
import { useLogger } from '../logger.js';
|
|
6
|
-
import { ExportService } from '../services/import-export
|
|
6
|
+
import { ExportService } from '../services/import-export.js';
|
|
7
7
|
import { VersionsService } from '../services/versions.js';
|
|
8
8
|
import asyncHandler from '../utils/async-handler.js';
|
|
9
9
|
import { getCacheControlHeader } from '../utils/get-cache-headers.js';
|
package/dist/services/assets.js
CHANGED
|
@@ -24,7 +24,7 @@ export class AssetsService {
|
|
|
24
24
|
constructor(options) {
|
|
25
25
|
this.knex = options.knex || getDatabase();
|
|
26
26
|
this.accountability = options.accountability || null;
|
|
27
|
-
this.filesService = new FilesService(options);
|
|
27
|
+
this.filesService = new FilesService({ ...options, accountability: null });
|
|
28
28
|
this.authorizationService = new AuthorizationService(options);
|
|
29
29
|
}
|
|
30
30
|
async getAsset(id, transformation, range) {
|
|
@@ -46,8 +46,6 @@ export class AssetsService {
|
|
|
46
46
|
await this.authorizationService.checkAccess('read', 'directus_files', id);
|
|
47
47
|
}
|
|
48
48
|
const file = (await this.filesService.readOne(id, { limit: 1 }));
|
|
49
|
-
if (!file)
|
|
50
|
-
throw new ForbiddenError();
|
|
51
49
|
const exists = await storage.location(file.storage).exists(file.filename_disk);
|
|
52
50
|
if (!exists)
|
|
53
51
|
throw new ForbiddenError();
|
|
@@ -13,5 +13,5 @@ export declare class AuthorizationService {
|
|
|
13
13
|
* Checks if the provided payload matches the configured permissions, and adds the presets to the payload.
|
|
14
14
|
*/
|
|
15
15
|
validatePayload(action: PermissionsAction, collection: string, data: Partial<Item>): Partial<Item>;
|
|
16
|
-
checkAccess(action: PermissionsAction, collection: string, pk
|
|
16
|
+
checkAccess(action: PermissionsAction, collection: string, pk?: PrimaryKey | PrimaryKey[]): Promise<void>;
|
|
17
17
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { ForbiddenError } from '@directus/errors';
|
|
1
2
|
import { validatePayload } from '@directus/utils';
|
|
2
3
|
import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
|
|
3
4
|
import { cloneDeep, flatten, isArray, isNil, merge, reduce, uniq, uniqWith } from 'lodash-es';
|
|
4
5
|
import { GENERATE_SPECIAL } from '../constants.js';
|
|
5
6
|
import getDatabase from '../database/index.js';
|
|
6
|
-
import { ForbiddenError } from '@directus/errors';
|
|
7
7
|
import { getRelationInfo } from '../utils/get-relation-info.js';
|
|
8
8
|
import { stripFunction } from '../utils/strip-function.js';
|
|
9
9
|
import { ItemsService } from './items.js';
|
|
@@ -430,15 +430,27 @@ export class AuthorizationService {
|
|
|
430
430
|
};
|
|
431
431
|
if (Array.isArray(pk)) {
|
|
432
432
|
const result = await itemsService.readMany(pk, { ...query, limit: pk.length }, { permissionsAction: action });
|
|
433
|
-
|
|
433
|
+
// for the unexpected case that the result is not an array (for example due to filter hook)
|
|
434
|
+
if (!isArray(result))
|
|
434
435
|
throw new ForbiddenError();
|
|
435
436
|
if (result.length !== pk.length)
|
|
436
437
|
throw new ForbiddenError();
|
|
437
438
|
}
|
|
438
|
-
else {
|
|
439
|
+
else if (pk) {
|
|
439
440
|
const result = await itemsService.readOne(pk, query, { permissionsAction: action });
|
|
440
441
|
if (!result)
|
|
441
442
|
throw new ForbiddenError();
|
|
442
443
|
}
|
|
444
|
+
else {
|
|
445
|
+
query.limit = 1;
|
|
446
|
+
const result = await itemsService.readByQuery(query, { permissionsAction: action });
|
|
447
|
+
// for the unexpected case that the result is not an array (for example due to filter hook)
|
|
448
|
+
if (!isArray(result))
|
|
449
|
+
throw new ForbiddenError();
|
|
450
|
+
// for create action, an empty array is expected - for other actions, the first item is expected to be available
|
|
451
|
+
const access = action === 'create' ? result.length === 0 : !!result[0];
|
|
452
|
+
if (!access)
|
|
453
|
+
throw new ForbiddenError();
|
|
454
|
+
}
|
|
443
455
|
}
|
|
444
456
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Accountability, File, Query, SchemaOverview } from '@directus/types';
|
|
3
3
|
import type { Knex } from 'knex';
|
|
4
4
|
import type { Readable } from 'node:stream';
|
|
5
|
-
import type { AbstractServiceOptions } from '
|
|
5
|
+
import type { AbstractServiceOptions } from '../types/index.js';
|
|
6
6
|
type ExportFormat = 'csv' | 'json' | 'xml' | 'yaml';
|
|
7
7
|
export declare class ImportService {
|
|
8
8
|
knex: Knex;
|
|
@@ -10,16 +10,16 @@ import { createReadStream } from 'node:fs';
|
|
|
10
10
|
import { appendFile } from 'node:fs/promises';
|
|
11
11
|
import Papa from 'papaparse';
|
|
12
12
|
import StreamArray from 'stream-json/streamers/StreamArray.js';
|
|
13
|
-
import getDatabase from '
|
|
14
|
-
import emitter from '
|
|
15
|
-
import { useLogger } from '
|
|
16
|
-
import { getDateFormatted } from '
|
|
17
|
-
import { Url } from '
|
|
18
|
-
import { userName } from '
|
|
19
|
-
import { FilesService } from '
|
|
20
|
-
import { ItemsService } from '
|
|
21
|
-
import { NotificationsService } from '
|
|
22
|
-
import { UsersService } from '
|
|
13
|
+
import getDatabase from '../database/index.js';
|
|
14
|
+
import emitter from '../emitter.js';
|
|
15
|
+
import { useLogger } from '../logger.js';
|
|
16
|
+
import { getDateFormatted } from '../utils/get-date-formatted.js';
|
|
17
|
+
import { Url } from '../utils/url.js';
|
|
18
|
+
import { userName } from '../utils/user-name.js';
|
|
19
|
+
import { FilesService } from './files.js';
|
|
20
|
+
import { ItemsService } from './items.js';
|
|
21
|
+
import { NotificationsService } from './notifications.js';
|
|
22
|
+
import { UsersService } from './users.js';
|
|
23
23
|
const env = useEnv();
|
|
24
24
|
const logger = useLogger();
|
|
25
25
|
export class ImportService {
|
package/dist/services/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export * from './files.js';
|
|
|
10
10
|
export * from './flows.js';
|
|
11
11
|
export * from './folders.js';
|
|
12
12
|
export * from './graphql/index.js';
|
|
13
|
-
export * from './import-export
|
|
13
|
+
export * from './import-export.js';
|
|
14
14
|
export * from './items.js';
|
|
15
15
|
export * from './mail/index.js';
|
|
16
16
|
export * from './meta.js';
|
package/dist/services/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export * from './files.js';
|
|
|
10
10
|
export * from './flows.js';
|
|
11
11
|
export * from './folders.js';
|
|
12
12
|
export * from './graphql/index.js';
|
|
13
|
-
export * from './import-export
|
|
13
|
+
export * from './import-export.js';
|
|
14
14
|
export * from './items.js';
|
|
15
15
|
export * from './mail/index.js';
|
|
16
16
|
export * from './meta.js';
|
|
@@ -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
|
}
|
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
package/dist/types/items.d.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* I know this looks a little silly, but it allows us to explicitly differentiate between when we're
|
|
3
|
-
* expecting an item vs any other generic object.
|
|
4
|
-
*/
|
|
5
1
|
import type { DirectusError } from '@directus/errors';
|
|
6
2
|
import type { EventContext } from '@directus/types';
|
|
7
3
|
import type { MutationTracker } from '../services/items.js';
|
|
8
4
|
export type Item = Record<string, any>;
|
|
9
5
|
export type PrimaryKey = string | number;
|
|
10
|
-
export type Alterations = {
|
|
11
|
-
create:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
update: {
|
|
15
|
-
[key: string]: any;
|
|
16
|
-
}[];
|
|
17
|
-
delete: (number | string)[];
|
|
6
|
+
export type Alterations<T extends Item = Item, K extends keyof T | undefined = undefined> = {
|
|
7
|
+
create: Partial<T>[];
|
|
8
|
+
update: (K extends keyof T ? Partial<T> & Pick<T, K> : Partial<T>)[];
|
|
9
|
+
delete: (K extends keyof T ? T[K] : PrimaryKey)[];
|
|
18
10
|
};
|
|
19
11
|
export type MutationOptions = {
|
|
20
12
|
/**
|
package/dist/types/items.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@authenio/samlify-node-xmllint": "2.0.0",
|
|
62
|
-
"@aws-sdk/client-ses": "3.
|
|
62
|
+
"@aws-sdk/client-ses": "3.511.0",
|
|
63
63
|
"@directus/format-title": "10.1.0",
|
|
64
64
|
"@godaddy/terminus": "4.12.1",
|
|
65
65
|
"@rollup/plugin-alias": "5.1.0",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"encodeurl": "1.0.2",
|
|
86
86
|
"eventemitter2": "6.4.9",
|
|
87
87
|
"execa": "8.0.1",
|
|
88
|
-
"exif-reader": "2.0.
|
|
88
|
+
"exif-reader": "2.0.1",
|
|
89
89
|
"express": "4.18.2",
|
|
90
90
|
"flat": "6.0.1",
|
|
91
91
|
"fs-extra": "11.2.0",
|
|
@@ -138,7 +138,6 @@
|
|
|
138
138
|
"sharp": "0.33.2",
|
|
139
139
|
"snappy": "7.2.2",
|
|
140
140
|
"stream-json": "1.8.0",
|
|
141
|
-
"tinypool": "0.8.2",
|
|
142
141
|
"tsx": "4.7.0",
|
|
143
142
|
"uuid": "9.0.1",
|
|
144
143
|
"uuid-validate": "0.0.3",
|
|
@@ -146,26 +145,26 @@
|
|
|
146
145
|
"ws": "8.16.0",
|
|
147
146
|
"zod": "3.22.4",
|
|
148
147
|
"zod-validation-error": "3.0.0",
|
|
148
|
+
"@directus/app": "10.15.0",
|
|
149
|
+
"@directus/env": "1.0.1",
|
|
150
|
+
"@directus/extensions": "0.3.1",
|
|
151
|
+
"@directus/errors": "0.2.2",
|
|
149
152
|
"@directus/constants": "11.0.3",
|
|
150
|
-
"@directus/
|
|
151
|
-
"@directus/extensions": "0.3.0",
|
|
152
|
-
"@directus/extensions-sdk": "10.3.1",
|
|
153
|
+
"@directus/extensions-sdk": "10.3.2",
|
|
153
154
|
"@directus/memory": "1.0.1",
|
|
154
|
-
"@directus/app": "10.14.0",
|
|
155
|
-
"@directus/errors": "0.2.2",
|
|
156
|
-
"@directus/schema": "11.0.1",
|
|
157
155
|
"@directus/pressure": "1.0.15",
|
|
156
|
+
"@directus/schema": "11.0.1",
|
|
158
157
|
"@directus/specs": "10.2.6",
|
|
159
|
-
"@directus/storage-driver-azure": "10.0.16",
|
|
160
158
|
"@directus/storage": "10.0.9",
|
|
159
|
+
"@directus/storage-driver-azure": "10.0.16",
|
|
161
160
|
"@directus/storage-driver-cloudinary": "10.0.16",
|
|
162
|
-
"@directus/storage-driver-s3": "10.0.16",
|
|
163
161
|
"@directus/storage-driver-gcs": "10.0.16",
|
|
164
|
-
"@directus/storage-driver-supabase": "1.0.8",
|
|
165
162
|
"@directus/storage-driver-local": "10.0.16",
|
|
163
|
+
"@directus/storage-driver-supabase": "1.0.8",
|
|
164
|
+
"@directus/storage-driver-s3": "10.0.16",
|
|
166
165
|
"@directus/validation": "0.0.11",
|
|
167
|
-
"directus": "
|
|
168
|
-
"
|
|
166
|
+
"@directus/utils": "11.0.4",
|
|
167
|
+
"directus": "10.9.1"
|
|
169
168
|
},
|
|
170
169
|
"devDependencies": {
|
|
171
170
|
"@ngneat/falso": "7.1.1",
|
|
@@ -179,7 +178,7 @@
|
|
|
179
178
|
"@types/destroy": "1.0.3",
|
|
180
179
|
"@types/encodeurl": "1.0.2",
|
|
181
180
|
"@types/express": "4.17.21",
|
|
182
|
-
"@types/express-serve-static-core": "4.17.
|
|
181
|
+
"@types/express-serve-static-core": "4.17.43",
|
|
183
182
|
"@types/fs-extra": "11.0.4",
|
|
184
183
|
"@types/glob-to-regexp": "0.4.4",
|
|
185
184
|
"@types/inquirer": "9.0.7",
|
|
@@ -190,7 +189,7 @@
|
|
|
190
189
|
"@types/lodash-es": "4.17.12",
|
|
191
190
|
"@types/mime-types": "2.1.4",
|
|
192
191
|
"@types/ms": "0.7.34",
|
|
193
|
-
"@types/node": "18.19.
|
|
192
|
+
"@types/node": "18.19.14",
|
|
194
193
|
"@types/node-schedule": "2.1.6",
|
|
195
194
|
"@types/nodemailer": "6.4.14",
|
|
196
195
|
"@types/object-hash": "3.0.6",
|
|
@@ -208,8 +207,9 @@
|
|
|
208
207
|
"knex-mock-client": "2.0.1",
|
|
209
208
|
"typescript": "5.3.3",
|
|
210
209
|
"vitest": "1.2.2",
|
|
210
|
+
"@directus/random": "0.2.5",
|
|
211
211
|
"@directus/tsconfig": "1.0.1",
|
|
212
|
-
"@directus/types": "11.0.
|
|
212
|
+
"@directus/types": "11.0.5"
|
|
213
213
|
},
|
|
214
214
|
"optionalDependencies": {
|
|
215
215
|
"@keyv/redis": "2.8.4",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { Accountability, SchemaOverview } from '@directus/types';
|
|
2
|
-
export type ImportWorkerData = {
|
|
3
|
-
collection: string;
|
|
4
|
-
mimeType: string;
|
|
5
|
-
filePath: string;
|
|
6
|
-
accountability: Accountability | undefined;
|
|
7
|
-
schema: SchemaOverview;
|
|
8
|
-
};
|
|
9
|
-
export default function ({ collection, mimeType, filePath, accountability, schema }: ImportWorkerData): Promise<void>;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from 'node:fs';
|
|
2
|
-
import { ImportService } from './index.js';
|
|
3
|
-
export default async function ({ collection, mimeType, filePath, accountability, schema }) {
|
|
4
|
-
const service = new ImportService({
|
|
5
|
-
accountability: accountability,
|
|
6
|
-
schema: schema,
|
|
7
|
-
});
|
|
8
|
-
await service.import(collection, mimeType, createReadStream(filePath));
|
|
9
|
-
}
|
package/dist/worker-pool.d.ts
DELETED
package/dist/worker-pool.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import Tinypool from 'tinypool';
|
|
3
|
-
let workerPool;
|
|
4
|
-
export function getWorkerPool() {
|
|
5
|
-
if (!workerPool) {
|
|
6
|
-
workerPool = new Tinypool({
|
|
7
|
-
minThreads: 0,
|
|
8
|
-
maxQueue: 'auto',
|
|
9
|
-
});
|
|
10
|
-
// TODO Workaround currently required for failing CPU count on ARM in Tinypool,
|
|
11
|
-
// remove again once fixed upstream
|
|
12
|
-
if (workerPool.options.maxThreads === 0) {
|
|
13
|
-
const availableParallelism = os.availableParallelism();
|
|
14
|
-
workerPool.options.maxThreads = availableParallelism;
|
|
15
|
-
workerPool.options.maxQueue = availableParallelism ** 2;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return workerPool;
|
|
19
|
-
}
|