@directus/api 19.2.0 → 19.3.1
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/database/helpers/index.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mssql.js +9 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mysql.js +17 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/oracle.js +9 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +4 -0
- package/dist/database/helpers/schema/dialects/postgres.js +14 -0
- package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/sqlite.js +9 -0
- package/dist/database/helpers/schema/index.d.ts +3 -3
- package/dist/database/helpers/schema/index.js +3 -3
- package/dist/database/helpers/schema/types.d.ts +4 -0
- package/dist/database/helpers/schema/types.js +6 -0
- package/dist/middleware/graphql.js +5 -1
- package/dist/services/extensions.js +1 -1
- package/dist/services/roles.d.ts +1 -0
- package/dist/services/roles.js +210 -11
- package/dist/services/users.js +66 -3
- package/dist/telemetry/lib/get-report.js +22 -10
- package/dist/telemetry/types/report.d.ts +12 -0
- package/dist/telemetry/utils/check-increased-user-limits.d.ts +7 -0
- package/dist/telemetry/utils/check-increased-user-limits.js +22 -0
- package/dist/telemetry/utils/get-extension-count.d.ts +9 -0
- package/dist/telemetry/utils/get-extension-count.js +19 -0
- package/dist/telemetry/utils/get-field-count.d.ts +6 -0
- package/dist/telemetry/utils/get-field-count.js +12 -0
- package/dist/telemetry/utils/get-item-count.d.ts +10 -6
- package/dist/telemetry/utils/get-item-count.js +13 -9
- package/dist/telemetry/utils/get-role-counts-by-roles.d.ts +6 -0
- package/dist/telemetry/utils/get-role-counts-by-roles.js +27 -0
- package/dist/telemetry/utils/get-role-counts-by-users.d.ts +11 -0
- package/dist/telemetry/utils/get-role-counts-by-users.js +34 -0
- package/dist/telemetry/utils/get-user-count.d.ts +3 -2
- package/dist/telemetry/utils/get-user-count.js +7 -4
- package/dist/telemetry/utils/get-user-counts-by-roles.d.ts +7 -0
- package/dist/telemetry/utils/get-user-counts-by-roles.js +35 -0
- package/dist/telemetry/utils/get-user-item-count.js +4 -2
- package/dist/telemetry/utils/should-check-user-limits.d.ts +4 -0
- package/dist/telemetry/utils/should-check-user-limits.js +13 -0
- package/package.json +30 -30
|
@@ -9,7 +9,7 @@ import * as numberHelpers from './number/index.js';
|
|
|
9
9
|
export declare function getHelpers(database: Knex): {
|
|
10
10
|
date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
|
|
11
11
|
st: geometryHelpers.mysql | geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
|
|
12
|
-
schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle;
|
|
12
|
+
schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
|
|
13
13
|
sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
|
|
14
14
|
number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
|
|
15
15
|
};
|
|
@@ -4,4 +4,5 @@ import { SchemaHelper } from '../types.js';
|
|
|
4
4
|
export declare class SchemaHelperCockroachDb extends SchemaHelper {
|
|
5
5
|
changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
|
|
6
6
|
constraintName(existingName: string): string;
|
|
7
|
+
getDatabaseSize(): Promise<number | null>;
|
|
7
8
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SchemaHelper } from '../types.js';
|
|
2
|
+
import { useEnv } from '@directus/env';
|
|
3
|
+
const env = useEnv();
|
|
2
4
|
export class SchemaHelperCockroachDb extends SchemaHelper {
|
|
3
5
|
async changeToType(table, column, type, options = {}) {
|
|
4
6
|
await this.changeToTypeByCopy(table, column, type, options);
|
|
@@ -14,4 +16,15 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
|
|
|
14
16
|
return existingName + suffix;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
19
|
+
async getDatabaseSize() {
|
|
20
|
+
try {
|
|
21
|
+
const result = await this.knex
|
|
22
|
+
.select(this.knex.raw('round(SUM(range_size_mb) * 1024 * 1024, 0) AS size'))
|
|
23
|
+
.from(this.knex.raw('[SHOW RANGES FROM database ??]', [env['DB_DATABASE']]));
|
|
24
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
17
30
|
}
|
|
@@ -4,4 +4,5 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
|
|
|
4
4
|
applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
|
|
5
5
|
applyOffset(rootQuery: Knex.QueryBuilder, offset: number): void;
|
|
6
6
|
formatUUID(uuid: string): string;
|
|
7
|
+
getDatabaseSize(): Promise<number | null>;
|
|
7
8
|
}
|
|
@@ -17,4 +17,13 @@ export class SchemaHelperMSSQL extends SchemaHelper {
|
|
|
17
17
|
formatUUID(uuid) {
|
|
18
18
|
return uuid.toUpperCase();
|
|
19
19
|
}
|
|
20
|
+
async getDatabaseSize() {
|
|
21
|
+
try {
|
|
22
|
+
const result = await this.knex.raw('SELECT SUM(size) * 8192 AS size FROM sys.database_files;');
|
|
23
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
20
29
|
}
|
|
@@ -2,4 +2,5 @@ import type { Knex } from 'knex';
|
|
|
2
2
|
import { SchemaHelper } from '../types.js';
|
|
3
3
|
export declare class SchemaHelperMySQL extends SchemaHelper {
|
|
4
4
|
applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
|
|
5
|
+
getDatabaseSize(): Promise<number | null>;
|
|
5
6
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
1
2
|
import { getDatabaseVersion } from '../../../index.js';
|
|
2
3
|
import { SchemaHelper } from '../types.js';
|
|
4
|
+
const env = useEnv();
|
|
3
5
|
export class SchemaHelperMySQL extends SchemaHelper {
|
|
4
6
|
applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields) {
|
|
5
7
|
if (getDatabaseVersion()?.startsWith('5.7')) {
|
|
@@ -11,4 +13,19 @@ export class SchemaHelperMySQL extends SchemaHelper {
|
|
|
11
13
|
}
|
|
12
14
|
return super.applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields);
|
|
13
15
|
}
|
|
16
|
+
async getDatabaseSize() {
|
|
17
|
+
try {
|
|
18
|
+
const result = (await this.knex
|
|
19
|
+
.sum('size AS size')
|
|
20
|
+
.from(this.knex
|
|
21
|
+
.select(this.knex.raw('data_length + index_length AS size'))
|
|
22
|
+
.from('information_schema.TABLES')
|
|
23
|
+
.where('table_schema', '=', String(env['DB_DATABASE']))
|
|
24
|
+
.as('size')));
|
|
25
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
14
31
|
}
|
|
@@ -29,4 +29,13 @@ export class SchemaHelperOracle extends SchemaHelper {
|
|
|
29
29
|
}
|
|
30
30
|
return field.type;
|
|
31
31
|
}
|
|
32
|
+
async getDatabaseSize() {
|
|
33
|
+
try {
|
|
34
|
+
const result = await this.knex.raw('select SUM(bytes) from dba_segments');
|
|
35
|
+
return result[0]?.['SUM(BYTES)'] ? Number(result[0]?.['SUM(BYTES)']) : null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
32
41
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { SchemaHelper } from '../types.js';
|
|
3
|
+
const env = useEnv();
|
|
4
|
+
export class SchemaHelperPostgres extends SchemaHelper {
|
|
5
|
+
async getDatabaseSize() {
|
|
6
|
+
try {
|
|
7
|
+
const result = await this.knex.select(this.knex.raw(`pg_database_size(?) as size;`, [env['DB_DATABASE']]));
|
|
8
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -10,4 +10,13 @@ export class SchemaHelperSQLite extends SchemaHelper {
|
|
|
10
10
|
async postColumnChange() {
|
|
11
11
|
await this.knex.raw('PRAGMA foreign_keys = ON');
|
|
12
12
|
}
|
|
13
|
+
async getDatabaseSize() {
|
|
14
|
+
try {
|
|
15
|
+
const result = await this.knex.raw('SELECT page_count * page_size as "size" FROM pragma_page_count(), pragma_page_size();');
|
|
16
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
13
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { SchemaHelperDefault as postgres } from './dialects/default.js';
|
|
2
1
|
export { SchemaHelperCockroachDb as cockroachdb } from './dialects/cockroachdb.js';
|
|
3
2
|
export { SchemaHelperDefault as redshift } from './dialects/default.js';
|
|
3
|
+
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
|
|
4
|
+
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
|
|
4
5
|
export { SchemaHelperOracle as oracle } from './dialects/oracle.js';
|
|
6
|
+
export { SchemaHelperPostgres as postgres } from './dialects/postgres.js';
|
|
5
7
|
export { SchemaHelperSQLite as sqlite } from './dialects/sqlite.js';
|
|
6
|
-
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
|
|
7
|
-
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { SchemaHelperDefault as postgres } from './dialects/default.js';
|
|
2
1
|
export { SchemaHelperCockroachDb as cockroachdb } from './dialects/cockroachdb.js';
|
|
3
2
|
export { SchemaHelperDefault as redshift } from './dialects/default.js';
|
|
3
|
+
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
|
|
4
|
+
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
|
|
4
5
|
export { SchemaHelperOracle as oracle } from './dialects/oracle.js';
|
|
6
|
+
export { SchemaHelperPostgres as postgres } from './dialects/postgres.js';
|
|
5
7
|
export { SchemaHelperSQLite as sqlite } from './dialects/sqlite.js';
|
|
6
|
-
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
|
|
7
|
-
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
|
|
@@ -23,4 +23,8 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
|
|
|
23
23
|
castA2oPrimaryKey(): string;
|
|
24
24
|
applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
|
|
25
25
|
formatUUID(uuid: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* @returns Size of the database in bytes
|
|
28
|
+
*/
|
|
29
|
+
getDatabaseSize(): Promise<number | null>;
|
|
26
30
|
}
|
|
@@ -3,6 +3,7 @@ import { getOperationAST, parse, Source } from 'graphql';
|
|
|
3
3
|
import { InvalidPayloadError, InvalidQueryError, MethodNotAllowedError } from '@directus/errors';
|
|
4
4
|
import { GraphQLValidationError } from '../services/graphql/errors/validation.js';
|
|
5
5
|
import asyncHandler from '../utils/async-handler.js';
|
|
6
|
+
import { useEnv } from '@directus/env';
|
|
6
7
|
export const parseGraphQL = asyncHandler(async (req, res, next) => {
|
|
7
8
|
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
8
9
|
throw new MethodNotAllowedError({ allowed: ['GET', 'POST'], current: req.method });
|
|
@@ -35,7 +36,10 @@ export const parseGraphQL = asyncHandler(async (req, res, next) => {
|
|
|
35
36
|
throw new InvalidPayloadError({ reason: 'Must provide query string' });
|
|
36
37
|
}
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
+
const env = useEnv();
|
|
40
|
+
document = parse(new Source(query), {
|
|
41
|
+
maxTokens: Number(env['GRAPHQL_QUERY_TOKEN_LIMIT']),
|
|
42
|
+
});
|
|
39
43
|
}
|
|
40
44
|
catch (err) {
|
|
41
45
|
throw new GraphQLValidationError({
|
|
@@ -52,7 +52,7 @@ export class ExtensionsService {
|
|
|
52
52
|
const points = version.bundled.length ?? 1;
|
|
53
53
|
const afterInstallCount = currentlyInstalledCount + points;
|
|
54
54
|
if (afterInstallCount >= limit) {
|
|
55
|
-
throw new LimitExceededError();
|
|
55
|
+
throw new LimitExceededError({ category: 'Extensions' });
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
return { extension, version };
|
package/dist/services/roles.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare class RolesService extends ItemsService {
|
|
|
7
7
|
private checkForOtherAdminUsers;
|
|
8
8
|
private isIpAccessValid;
|
|
9
9
|
private assertValidIpAccess;
|
|
10
|
+
private getRoleAccessType;
|
|
10
11
|
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
11
12
|
createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
12
13
|
updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
package/dist/services/roles.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
|
|
2
2
|
import { getMatch } from 'ip-matching';
|
|
3
|
+
import { omit } from 'lodash-es';
|
|
4
|
+
import { checkIncreasedUserLimits } from '../telemetry/utils/check-increased-user-limits.js';
|
|
5
|
+
import { getRoleCountsByUsers } from '../telemetry/utils/get-role-counts-by-users.js';
|
|
6
|
+
import {} from '../telemetry/utils/get-user-count.js';
|
|
7
|
+
import { getUserCountsByRoles } from '../telemetry/utils/get-user-counts-by-roles.js';
|
|
8
|
+
import { shouldCheckUserLimits } from '../telemetry/utils/should-check-user-limits.js';
|
|
9
|
+
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
3
10
|
import { transaction } from '../utils/transaction.js';
|
|
4
11
|
import { ItemsService } from './items.js';
|
|
5
12
|
import { PermissionsService } from './permissions/index.js';
|
|
@@ -24,8 +31,9 @@ export class RolesService extends ItemsService {
|
|
|
24
31
|
}
|
|
25
32
|
async checkForOtherAdminUsers(key, users) {
|
|
26
33
|
const role = await this.knex.select('admin_access').from('directus_roles').where('id', '=', key).first();
|
|
34
|
+
// No-op if role doesn't exist
|
|
27
35
|
if (!role)
|
|
28
|
-
|
|
36
|
+
return;
|
|
29
37
|
const usersBefore = (await this.knex.select('id').from('directus_users').where('role', '=', key)).map((user) => user.id);
|
|
30
38
|
const usersAdded = [];
|
|
31
39
|
const usersUpdated = [];
|
|
@@ -151,13 +159,67 @@ export class RolesService extends ItemsService {
|
|
|
151
159
|
});
|
|
152
160
|
}
|
|
153
161
|
}
|
|
162
|
+
getRoleAccessType(data) {
|
|
163
|
+
if ('admin_access' in data && data['admin_access'] === true) {
|
|
164
|
+
return 'admin';
|
|
165
|
+
}
|
|
166
|
+
else if (('app_access' in data && data['app_access'] === true) || 'app_access' in data === false) {
|
|
167
|
+
return 'app';
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
return 'api';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
154
173
|
async createOne(data, opts) {
|
|
155
174
|
this.assertValidIpAccess(data);
|
|
175
|
+
if (shouldCheckUserLimits()) {
|
|
176
|
+
const increasedCounts = {
|
|
177
|
+
admin: 0,
|
|
178
|
+
app: 0,
|
|
179
|
+
api: 0,
|
|
180
|
+
};
|
|
181
|
+
const existingIds = [];
|
|
182
|
+
if ('users' in data) {
|
|
183
|
+
const type = this.getRoleAccessType(data);
|
|
184
|
+
increasedCounts[type] += data['users'].length;
|
|
185
|
+
for (const user of data['users']) {
|
|
186
|
+
if (typeof user === 'string') {
|
|
187
|
+
existingIds.push(user);
|
|
188
|
+
}
|
|
189
|
+
else if (typeof user === 'object' && 'id' in user) {
|
|
190
|
+
existingIds.push(user['id']);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
|
|
195
|
+
}
|
|
156
196
|
return super.createOne(data, opts);
|
|
157
197
|
}
|
|
158
198
|
async createMany(data, opts) {
|
|
199
|
+
const needsUserLimitCheck = shouldCheckUserLimits();
|
|
200
|
+
const increasedCounts = {
|
|
201
|
+
admin: 0,
|
|
202
|
+
app: 0,
|
|
203
|
+
api: 0,
|
|
204
|
+
};
|
|
205
|
+
const existingIds = [];
|
|
159
206
|
for (const partialItem of data) {
|
|
160
207
|
this.assertValidIpAccess(partialItem);
|
|
208
|
+
if (needsUserLimitCheck && 'users' in partialItem) {
|
|
209
|
+
const type = this.getRoleAccessType(partialItem);
|
|
210
|
+
increasedCounts[type] += partialItem['users'].length;
|
|
211
|
+
for (const user of partialItem['users']) {
|
|
212
|
+
if (typeof user === 'string') {
|
|
213
|
+
existingIds.push(user);
|
|
214
|
+
}
|
|
215
|
+
else if (typeof user === 'object' && 'id' in user) {
|
|
216
|
+
existingIds.push(user['id']);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (needsUserLimitCheck) {
|
|
222
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
|
|
161
223
|
}
|
|
162
224
|
return super.createMany(data, opts);
|
|
163
225
|
}
|
|
@@ -167,28 +229,150 @@ export class RolesService extends ItemsService {
|
|
|
167
229
|
if ('users' in data) {
|
|
168
230
|
await this.checkForOtherAdminUsers(key, data['users']);
|
|
169
231
|
}
|
|
232
|
+
if (shouldCheckUserLimits()) {
|
|
233
|
+
const increasedCounts = {
|
|
234
|
+
admin: 0,
|
|
235
|
+
app: 0,
|
|
236
|
+
api: 0,
|
|
237
|
+
};
|
|
238
|
+
let increasedUsers = 0;
|
|
239
|
+
const existingIds = [];
|
|
240
|
+
let existingRole = await this.knex
|
|
241
|
+
.count('directus_users.id', { as: 'count' })
|
|
242
|
+
.select('directus_roles.admin_access', 'directus_roles.app_access')
|
|
243
|
+
.from('directus_users')
|
|
244
|
+
.where('directus_roles.id', '=', key)
|
|
245
|
+
.andWhere('directus_users.status', '=', 'active')
|
|
246
|
+
.leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
|
|
247
|
+
.groupBy('directus_roles.admin_access', 'directus_roles.app_access')
|
|
248
|
+
.first();
|
|
249
|
+
if (!existingRole) {
|
|
250
|
+
try {
|
|
251
|
+
const role = (await this.knex
|
|
252
|
+
.select('admin_access', 'app_access')
|
|
253
|
+
.from('directus_roles')
|
|
254
|
+
.where('id', '=', key)
|
|
255
|
+
.first()) ?? { admin_access: null, app_access: null };
|
|
256
|
+
existingRole = { count: 0, ...role };
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
existingRole = { count: 0, admin_access: null, app_access: null };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if ('users' in data) {
|
|
263
|
+
const users = data['users'];
|
|
264
|
+
if (Array.isArray(users)) {
|
|
265
|
+
increasedUsers = users.length - Number(existingRole.count);
|
|
266
|
+
for (const user of users) {
|
|
267
|
+
if (typeof user === 'string') {
|
|
268
|
+
existingIds.push(user);
|
|
269
|
+
}
|
|
270
|
+
else if (typeof user === 'object' && 'id' in user) {
|
|
271
|
+
existingIds.push(user['id']);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
increasedUsers += users.create.length;
|
|
277
|
+
increasedUsers -= users.delete.length;
|
|
278
|
+
const userIds = [];
|
|
279
|
+
for (const user of users.update) {
|
|
280
|
+
if ('status' in user) {
|
|
281
|
+
// account for users being activated and deactivated
|
|
282
|
+
if (user['status'] === 'active') {
|
|
283
|
+
increasedUsers++;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
increasedUsers--;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
userIds.push(user.id);
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const existingCounts = await getRoleCountsByUsers(this.knex, userIds);
|
|
293
|
+
if (existingRole.admin_access) {
|
|
294
|
+
increasedUsers += existingCounts.app + existingCounts.api;
|
|
295
|
+
}
|
|
296
|
+
else if (existingRole.app_access) {
|
|
297
|
+
increasedUsers += existingCounts.admin + existingCounts.api;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
increasedUsers += existingCounts.admin + existingCounts.app;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// ignore failed user call
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
let isAccessChanged = false;
|
|
309
|
+
let accessType = 'api';
|
|
310
|
+
if ('app_access' in data) {
|
|
311
|
+
if (data['app_access'] === true) {
|
|
312
|
+
accessType = 'app';
|
|
313
|
+
if (!existingRole.app_access)
|
|
314
|
+
isAccessChanged = true;
|
|
315
|
+
}
|
|
316
|
+
else if (existingRole.app_access) {
|
|
317
|
+
isAccessChanged = true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else if (existingRole.app_access) {
|
|
321
|
+
accessType = 'app';
|
|
322
|
+
}
|
|
323
|
+
if ('admin_access' in data) {
|
|
324
|
+
if (data['admin_access'] === true) {
|
|
325
|
+
accessType = 'admin';
|
|
326
|
+
if (!existingRole.admin_access)
|
|
327
|
+
isAccessChanged = true;
|
|
328
|
+
}
|
|
329
|
+
else if (existingRole.admin_access) {
|
|
330
|
+
isAccessChanged = true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (existingRole.admin_access) {
|
|
334
|
+
accessType = 'admin';
|
|
335
|
+
}
|
|
336
|
+
if (isAccessChanged) {
|
|
337
|
+
increasedCounts[accessType] += Number(existingRole.count);
|
|
338
|
+
}
|
|
339
|
+
increasedCounts[accessType] += increasedUsers;
|
|
340
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
|
|
341
|
+
}
|
|
170
342
|
}
|
|
171
343
|
catch (err) {
|
|
172
344
|
(opts || (opts = {})).preMutationError = err;
|
|
173
345
|
}
|
|
174
346
|
return super.updateOne(key, data, opts);
|
|
175
347
|
}
|
|
176
|
-
async updateBatch(data, opts) {
|
|
348
|
+
async updateBatch(data, opts = {}) {
|
|
177
349
|
for (const partialItem of data) {
|
|
178
350
|
this.assertValidIpAccess(partialItem);
|
|
179
351
|
}
|
|
180
352
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
181
|
-
|
|
182
|
-
|
|
353
|
+
if (!opts.mutationTracker) {
|
|
354
|
+
opts.mutationTracker = this.createMutationTracker();
|
|
355
|
+
}
|
|
356
|
+
const keys = [];
|
|
183
357
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
358
|
+
await transaction(this.knex, async (trx) => {
|
|
359
|
+
const service = new RolesService({
|
|
360
|
+
accountability: this.accountability,
|
|
361
|
+
knex: trx,
|
|
362
|
+
schema: this.schema,
|
|
363
|
+
});
|
|
364
|
+
for (const item of data) {
|
|
365
|
+
const combinedOpts = Object.assign({ autoPurgeCache: false }, opts);
|
|
366
|
+
keys.push(await service.updateOne(item[primaryKeyField], omit(item, primaryKeyField), combinedOpts));
|
|
367
|
+
}
|
|
368
|
+
});
|
|
187
369
|
}
|
|
188
|
-
|
|
189
|
-
(
|
|
370
|
+
finally {
|
|
371
|
+
if (shouldClearCache(this.cache, opts, this.collection)) {
|
|
372
|
+
await this.cache.clear();
|
|
373
|
+
}
|
|
190
374
|
}
|
|
191
|
-
return
|
|
375
|
+
return keys;
|
|
192
376
|
}
|
|
193
377
|
async updateMany(keys, data, opts) {
|
|
194
378
|
this.assertValidIpAccess(data);
|
|
@@ -196,6 +380,21 @@ export class RolesService extends ItemsService {
|
|
|
196
380
|
if ('admin_access' in data && data['admin_access'] === false) {
|
|
197
381
|
await this.checkForOtherAdminRoles(keys);
|
|
198
382
|
}
|
|
383
|
+
if (shouldCheckUserLimits() && ('admin_access' in data || 'app_access' in data)) {
|
|
384
|
+
const existingCounts = await getUserCountsByRoles(this.knex, keys);
|
|
385
|
+
const increasedCounts = {
|
|
386
|
+
admin: 0,
|
|
387
|
+
app: 0,
|
|
388
|
+
api: 0,
|
|
389
|
+
};
|
|
390
|
+
const type = this.getRoleAccessType(data);
|
|
391
|
+
for (const [existingType, existingCount] of Object.entries(existingCounts)) {
|
|
392
|
+
if (existingType === type)
|
|
393
|
+
continue;
|
|
394
|
+
increasedCounts[type] += existingCount;
|
|
395
|
+
}
|
|
396
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts);
|
|
397
|
+
}
|
|
199
398
|
}
|
|
200
399
|
catch (err) {
|
|
201
400
|
(opts || (opts = {})).preMutationError = err;
|
package/dist/services/users.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { ForbiddenError, InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '@directus/errors';
|
|
3
|
-
import { getSimpleHash, toArray, validatePayload } from '@directus/utils';
|
|
3
|
+
import { getSimpleHash, toArray, toBoolean, validatePayload } from '@directus/utils';
|
|
4
4
|
import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
|
|
5
5
|
import Joi from 'joi';
|
|
6
6
|
import jwt from 'jsonwebtoken';
|
|
7
|
-
import { cloneDeep, isEmpty } from 'lodash-es';
|
|
7
|
+
import { cloneDeep, isEmpty, mergeWith } from 'lodash-es';
|
|
8
8
|
import { performance } from 'perf_hooks';
|
|
9
9
|
import getDatabase from '../database/index.js';
|
|
10
10
|
import { useLogger } from '../logger.js';
|
|
11
|
+
import { checkIncreasedUserLimits } from '../telemetry/utils/check-increased-user-limits.js';
|
|
12
|
+
import { getRoleCountsByRoles } from '../telemetry/utils/get-role-counts-by-roles.js';
|
|
13
|
+
import { getRoleCountsByUsers } from '../telemetry/utils/get-role-counts-by-users.js';
|
|
14
|
+
import {} from '../telemetry/utils/get-user-count.js';
|
|
15
|
+
import { shouldCheckUserLimits } from '../telemetry/utils/should-check-user-limits.js';
|
|
11
16
|
import { getSecret } from '../utils/get-secret.js';
|
|
12
17
|
import isUrlAllowed from '../utils/is-url-allowed.js';
|
|
13
18
|
import { verifyJWT } from '../utils/jwt.js';
|
|
@@ -164,6 +169,7 @@ export class UsersService extends ItemsService {
|
|
|
164
169
|
async createMany(data, opts) {
|
|
165
170
|
const emails = data['map']((payload) => payload['email']).filter((email) => email);
|
|
166
171
|
const passwords = data['map']((payload) => payload['password']).filter((password) => password);
|
|
172
|
+
const roles = data['map']((payload) => payload['role']).filter((role) => role);
|
|
167
173
|
try {
|
|
168
174
|
if (emails.length) {
|
|
169
175
|
this.validateEmail(emails);
|
|
@@ -172,6 +178,33 @@ export class UsersService extends ItemsService {
|
|
|
172
178
|
if (passwords.length) {
|
|
173
179
|
await this.checkPasswordPolicy(passwords);
|
|
174
180
|
}
|
|
181
|
+
if (shouldCheckUserLimits() && roles.length) {
|
|
182
|
+
const increasedCounts = {
|
|
183
|
+
admin: 0,
|
|
184
|
+
app: 0,
|
|
185
|
+
api: 0,
|
|
186
|
+
};
|
|
187
|
+
const existingRoles = [];
|
|
188
|
+
for (const role of roles) {
|
|
189
|
+
if (typeof role === 'object') {
|
|
190
|
+
if ('admin_access' in role && role['admin_access'] === true) {
|
|
191
|
+
increasedCounts.admin++;
|
|
192
|
+
}
|
|
193
|
+
else if ('app_access' in role && role['app_access'] === true) {
|
|
194
|
+
increasedCounts.app++;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
increasedCounts.api++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
existingRoles.push(role);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const existingRoleCounts = await getRoleCountsByRoles(this.knex, existingRoles);
|
|
205
|
+
mergeWith(increasedCounts, existingRoleCounts, (x, y) => x + y);
|
|
206
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts);
|
|
207
|
+
}
|
|
175
208
|
}
|
|
176
209
|
catch (err) {
|
|
177
210
|
(opts || (opts = {})).preMutationError = err;
|
|
@@ -216,6 +249,7 @@ export class UsersService extends ItemsService {
|
|
|
216
249
|
*/
|
|
217
250
|
async updateMany(keys, data, opts) {
|
|
218
251
|
try {
|
|
252
|
+
const needsUserLimitCheck = shouldCheckUserLimits();
|
|
219
253
|
if (data['role']) {
|
|
220
254
|
/*
|
|
221
255
|
* data['role'] has the following cases:
|
|
@@ -226,7 +260,11 @@ export class UsersService extends ItemsService {
|
|
|
226
260
|
const role = data['role']?.id ?? data['role'];
|
|
227
261
|
let newRole;
|
|
228
262
|
if (typeof role === 'string') {
|
|
229
|
-
newRole = await this.knex
|
|
263
|
+
newRole = await this.knex
|
|
264
|
+
.select('admin_access', 'app_access')
|
|
265
|
+
.from('directus_roles')
|
|
266
|
+
.where('id', role)
|
|
267
|
+
.first();
|
|
230
268
|
}
|
|
231
269
|
else {
|
|
232
270
|
newRole = role;
|
|
@@ -234,10 +272,35 @@ export class UsersService extends ItemsService {
|
|
|
234
272
|
if (!newRole?.admin_access) {
|
|
235
273
|
await this.checkRemainingAdminExistence(keys);
|
|
236
274
|
}
|
|
275
|
+
if (needsUserLimitCheck && newRole) {
|
|
276
|
+
const existingCounts = await getRoleCountsByUsers(this.knex, keys);
|
|
277
|
+
const increasedCounts = {
|
|
278
|
+
admin: 0,
|
|
279
|
+
app: 0,
|
|
280
|
+
api: 0,
|
|
281
|
+
};
|
|
282
|
+
if (toBoolean(newRole.admin_access)) {
|
|
283
|
+
increasedCounts.admin = keys.length - existingCounts.admin;
|
|
284
|
+
}
|
|
285
|
+
else if (toBoolean(newRole.app_access)) {
|
|
286
|
+
increasedCounts.app = keys.length - existingCounts.app;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
increasedCounts.api = keys.length - existingCounts.api;
|
|
290
|
+
}
|
|
291
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (needsUserLimitCheck && data['role'] === null) {
|
|
295
|
+
await checkIncreasedUserLimits(this.knex, { admin: 0, app: 0, api: 1 });
|
|
237
296
|
}
|
|
238
297
|
if (data['status'] !== undefined && data['status'] !== 'active') {
|
|
239
298
|
await this.checkRemainingActiveAdmin(keys);
|
|
240
299
|
}
|
|
300
|
+
if (needsUserLimitCheck && data['status'] === 'active') {
|
|
301
|
+
const increasedCounts = await getRoleCountsByUsers(this.knex, keys, { inactiveUsers: true });
|
|
302
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts);
|
|
303
|
+
}
|
|
241
304
|
if (data['email']) {
|
|
242
305
|
if (keys.length > 1) {
|
|
243
306
|
throw new RecordNotUniqueError({
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { version } from 'directus/version';
|
|
3
|
+
import { getHelpers } from '../../database/helpers/index.js';
|
|
3
4
|
import { getDatabase, getDatabaseClient } from '../../database/index.js';
|
|
5
|
+
import { getExtensionCount } from '../utils/get-extension-count.js';
|
|
6
|
+
import { getFieldCount } from '../utils/get-field-count.js';
|
|
4
7
|
import { getItemCount } from '../utils/get-item-count.js';
|
|
5
8
|
import { getUserCount } from '../utils/get-user-count.js';
|
|
6
9
|
import { getUserItemCount } from '../utils/get-user-item-count.js';
|
|
7
|
-
const
|
|
8
|
-
'directus_dashboards',
|
|
9
|
-
'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const basicCountTasks = [
|
|
11
|
+
{ collection: 'directus_dashboards' },
|
|
12
|
+
{ collection: 'directus_files' },
|
|
13
|
+
{
|
|
14
|
+
collection: 'directus_flows',
|
|
15
|
+
where: ['status', '=', 'active'],
|
|
16
|
+
},
|
|
17
|
+
{ collection: 'directus_roles' },
|
|
18
|
+
{ collection: 'directus_shares' },
|
|
14
19
|
];
|
|
15
20
|
/**
|
|
16
21
|
* Create a telemetry report about the anonymous usage of the current installation
|
|
@@ -18,17 +23,20 @@ const basicCountCollections = [
|
|
|
18
23
|
export const getReport = async () => {
|
|
19
24
|
const db = getDatabase();
|
|
20
25
|
const env = useEnv();
|
|
21
|
-
const
|
|
22
|
-
|
|
26
|
+
const helpers = getHelpers(db);
|
|
27
|
+
const [basicCounts, userCounts, userItemCount, fieldsCounts, extensionsCounts, databaseSize] = await Promise.all([
|
|
28
|
+
getItemCount(db, basicCountTasks),
|
|
23
29
|
getUserCount(db),
|
|
24
30
|
getUserItemCount(db),
|
|
31
|
+
getFieldCount(db),
|
|
32
|
+
getExtensionCount(db),
|
|
33
|
+
helpers.schema.getDatabaseSize(),
|
|
25
34
|
]);
|
|
26
35
|
return {
|
|
27
36
|
url: env['PUBLIC_URL'],
|
|
28
37
|
version: version,
|
|
29
38
|
database: getDatabaseClient(),
|
|
30
39
|
dashboards: basicCounts.directus_dashboards,
|
|
31
|
-
extensions: basicCounts.directus_extensions,
|
|
32
40
|
files: basicCounts.directus_files,
|
|
33
41
|
flows: basicCounts.directus_flows,
|
|
34
42
|
roles: basicCounts.directus_roles,
|
|
@@ -38,5 +46,9 @@ export const getReport = async () => {
|
|
|
38
46
|
api_users: userCounts.api,
|
|
39
47
|
collections: userItemCount.collections,
|
|
40
48
|
items: userItemCount.items,
|
|
49
|
+
fields_max: fieldsCounts.max,
|
|
50
|
+
fields_total: fieldsCounts.total,
|
|
51
|
+
extensions: extensionsCounts.totalEnabled,
|
|
52
|
+
database_size: databaseSize ?? 0,
|
|
41
53
|
};
|
|
42
54
|
};
|
|
@@ -55,4 +55,16 @@ export interface TelemetryReport {
|
|
|
55
55
|
* Number of shares in the system
|
|
56
56
|
*/
|
|
57
57
|
shares: number;
|
|
58
|
+
/**
|
|
59
|
+
* Maximum number of fields in a collection
|
|
60
|
+
*/
|
|
61
|
+
fields_max: number;
|
|
62
|
+
/**
|
|
63
|
+
* Number of fields in the system
|
|
64
|
+
*/
|
|
65
|
+
fields_total: number;
|
|
66
|
+
/**
|
|
67
|
+
* Size of the database in bytes
|
|
68
|
+
*/
|
|
69
|
+
database_size: number;
|
|
58
70
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
import { type AccessTypeCount } from './get-user-count.js';
|
|
3
|
+
import type { PrimaryKey } from '@directus/types';
|
|
4
|
+
/**
|
|
5
|
+
* Ensure that user limits are not reached
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkIncreasedUserLimits(db: Knex, increasedUserCounts: AccessTypeCount, ignoreIds?: PrimaryKey[]): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { LimitExceededError } from '@directus/errors';
|
|
3
|
+
import { getUserCount } from './get-user-count.js';
|
|
4
|
+
const env = useEnv();
|
|
5
|
+
/**
|
|
6
|
+
* Ensure that user limits are not reached
|
|
7
|
+
*/
|
|
8
|
+
export async function checkIncreasedUserLimits(db, increasedUserCounts, ignoreIds = []) {
|
|
9
|
+
if (!increasedUserCounts.admin && !increasedUserCounts.app && !increasedUserCounts.api)
|
|
10
|
+
return;
|
|
11
|
+
const userCounts = await getUserCount(db, ignoreIds);
|
|
12
|
+
if (increasedUserCounts.admin > 0 &&
|
|
13
|
+
increasedUserCounts.admin + userCounts.admin > Number(env['USERS_ADMIN_ACCESS_LIMIT'])) {
|
|
14
|
+
throw new LimitExceededError({ category: 'Active Admin users' });
|
|
15
|
+
}
|
|
16
|
+
if (increasedUserCounts.app > 0 && increasedUserCounts.app + userCounts.app > Number(env['USERS_APP_ACCESS_LIMIT'])) {
|
|
17
|
+
throw new LimitExceededError({ category: 'Active App users' });
|
|
18
|
+
}
|
|
19
|
+
if (increasedUserCounts.api > 0 && increasedUserCounts.api + userCounts.api > Number(env['USERS_API_ACCESS_LIMIT'])) {
|
|
20
|
+
throw new LimitExceededError({ category: 'Active API users' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Knex } from 'knex';
|
|
2
|
+
export interface ExtensionCount {
|
|
3
|
+
/**
|
|
4
|
+
* Total count of enabled extensions excluding Bundle-Parents,
|
|
5
|
+
* meaning a Bundle extensions with one extension inside of it counts as one.
|
|
6
|
+
*/
|
|
7
|
+
totalEnabled: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const getExtensionCount: (db: Knex) => Promise<ExtensionCount>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {} from 'knex';
|
|
2
|
+
import { ExtensionsService } from '../../services/extensions.js';
|
|
3
|
+
import { getSchema } from '../../utils/get-schema.js';
|
|
4
|
+
export const getExtensionCount = async (db) => {
|
|
5
|
+
const extensionsService = new ExtensionsService({
|
|
6
|
+
knex: db,
|
|
7
|
+
schema: await getSchema({ database: db }),
|
|
8
|
+
});
|
|
9
|
+
const extensions = await extensionsService.readAll();
|
|
10
|
+
let totalEnabled = 0;
|
|
11
|
+
for (const extension of extensions) {
|
|
12
|
+
if (extension.meta.enabled && extension.schema && extension.schema.type !== 'bundle') {
|
|
13
|
+
totalEnabled++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
totalEnabled,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {} from 'knex';
|
|
2
|
+
export const getFieldCount = async (db) => {
|
|
3
|
+
const query = (await db
|
|
4
|
+
.max({ max: 'field_count' })
|
|
5
|
+
.sum({ total: 'field_count' })
|
|
6
|
+
.from(db.select('collection').count('* as field_count').from('directus_fields').groupBy('collection').as('inner'))
|
|
7
|
+
.first());
|
|
8
|
+
return {
|
|
9
|
+
max: query?.max ? Number(query.max) : 0,
|
|
10
|
+
total: query?.total ? Number(query.total) : 0,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -3,13 +3,17 @@ export interface CollectionCount {
|
|
|
3
3
|
collection: string;
|
|
4
4
|
count: number;
|
|
5
5
|
}
|
|
6
|
+
export interface CollectionCountTask {
|
|
7
|
+
collection: string;
|
|
8
|
+
where?: readonly [string, string, string | boolean | number];
|
|
9
|
+
}
|
|
6
10
|
/**
|
|
7
|
-
* Get the item count of the given
|
|
11
|
+
* Get the item count of the given task in the given database
|
|
8
12
|
* @param db Knex instance to count against
|
|
9
|
-
* @param
|
|
13
|
+
* @param task Task to count rows for
|
|
10
14
|
* @returns Collection name and count
|
|
11
15
|
*/
|
|
12
|
-
export declare const countCollection: (db: Knex,
|
|
16
|
+
export declare const countCollection: (db: Knex, task: CollectionCountTask) => Promise<CollectionCount>;
|
|
13
17
|
/**
|
|
14
18
|
* Merge the given collection count in the object accumulator
|
|
15
19
|
* Intended for use with .reduce()
|
|
@@ -19,8 +23,8 @@ export declare const countCollection: (db: Knex, collection: string) => Promise<
|
|
|
19
23
|
*/
|
|
20
24
|
export declare const mergeResults: (acc: Record<string, number>, value: CollectionCount) => Record<string, number>;
|
|
21
25
|
/**
|
|
22
|
-
* Get an object of item counts for the given
|
|
26
|
+
* Get an object of item counts for the given tasks
|
|
23
27
|
* @param db Database instance to get counts in
|
|
24
|
-
* @param
|
|
28
|
+
* @param tasks Array of tasks to get count for
|
|
25
29
|
*/
|
|
26
|
-
export declare const getItemCount: <T extends readonly
|
|
30
|
+
export declare const getItemCount: <T extends readonly CollectionCountTask[]>(db: Knex, tasks: T) => Promise<Record<T[number]["collection"], number>>;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import {} from 'knex';
|
|
2
2
|
import pLimit from 'p-limit';
|
|
3
3
|
/**
|
|
4
|
-
* Get the item count of the given
|
|
4
|
+
* Get the item count of the given task in the given database
|
|
5
5
|
* @param db Knex instance to count against
|
|
6
|
-
* @param
|
|
6
|
+
* @param task Task to count rows for
|
|
7
7
|
* @returns Collection name and count
|
|
8
8
|
*/
|
|
9
|
-
export const countCollection = async (db,
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
export const countCollection = async (db, task) => {
|
|
10
|
+
const query = db.count('*', { as: 'count' }).from(task.collection);
|
|
11
|
+
if (task.where) {
|
|
12
|
+
query.where(...task.where);
|
|
13
|
+
}
|
|
14
|
+
const count = await query.first();
|
|
15
|
+
return { collection: task.collection, count: Number(count?.['count'] ?? 0) };
|
|
12
16
|
};
|
|
13
17
|
/**
|
|
14
18
|
* Merge the given collection count in the object accumulator
|
|
@@ -22,15 +26,15 @@ export const mergeResults = (acc, value) => {
|
|
|
22
26
|
return acc;
|
|
23
27
|
};
|
|
24
28
|
/**
|
|
25
|
-
* Get an object of item counts for the given
|
|
29
|
+
* Get an object of item counts for the given tasks
|
|
26
30
|
* @param db Database instance to get counts in
|
|
27
|
-
* @param
|
|
31
|
+
* @param tasks Array of tasks to get count for
|
|
28
32
|
*/
|
|
29
|
-
export const getItemCount = async (db,
|
|
33
|
+
export const getItemCount = async (db, tasks) => {
|
|
30
34
|
// Counts can be a little heavy if the table is very large, so we'll only ever execute 3 of these
|
|
31
35
|
// queries simultaneously to not overload the database
|
|
32
36
|
const limit = pLimit(3);
|
|
33
|
-
const calls =
|
|
37
|
+
const calls = tasks.map((task) => limit(countCollection, db, task));
|
|
34
38
|
const results = await Promise.all(calls);
|
|
35
39
|
return results.reduce(mergeResults, {});
|
|
36
40
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { toBoolean } from '@directus/utils';
|
|
2
|
+
import {} from './get-user-count.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get the role type counts by role IDs
|
|
5
|
+
*/
|
|
6
|
+
export async function getRoleCountsByRoles(db, roles) {
|
|
7
|
+
const counts = {
|
|
8
|
+
admin: 0,
|
|
9
|
+
app: 0,
|
|
10
|
+
api: 0,
|
|
11
|
+
};
|
|
12
|
+
const result = (await db.select('id', 'admin_access', 'app_access').from('directus_roles').whereIn('id', roles));
|
|
13
|
+
for (const role of result) {
|
|
14
|
+
const adminAccess = toBoolean(role.admin_access);
|
|
15
|
+
const appAccess = toBoolean(role.app_access);
|
|
16
|
+
if (adminAccess) {
|
|
17
|
+
counts.admin++;
|
|
18
|
+
}
|
|
19
|
+
else if (appAccess) {
|
|
20
|
+
counts.app++;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
counts.api++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return counts;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PrimaryKey } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
import type { AccessTypeCount } from './get-user-count.js';
|
|
4
|
+
type CountOptions = {
|
|
5
|
+
inactiveUsers?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Get the role type counts by user IDs
|
|
9
|
+
*/
|
|
10
|
+
export declare function getRoleCountsByUsers(db: Knex, userIds: PrimaryKey[], options?: CountOptions): Promise<AccessTypeCount>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { toBoolean } from '@directus/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Get the role type counts by user IDs
|
|
4
|
+
*/
|
|
5
|
+
export async function getRoleCountsByUsers(db, userIds, options = {}) {
|
|
6
|
+
const counts = {
|
|
7
|
+
admin: 0,
|
|
8
|
+
app: 0,
|
|
9
|
+
api: 0,
|
|
10
|
+
};
|
|
11
|
+
const result = await db
|
|
12
|
+
.count('directus_users.id', { as: 'count' })
|
|
13
|
+
.select('directus_roles.admin_access', 'directus_roles.app_access')
|
|
14
|
+
.from('directus_users')
|
|
15
|
+
.whereIn('directus_users.id', userIds)
|
|
16
|
+
.andWhere('directus_users.status', options.inactiveUsers ? '!=' : '=', 'active')
|
|
17
|
+
.leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
|
|
18
|
+
.groupBy('directus_roles.admin_access', 'directus_roles.app_access');
|
|
19
|
+
for (const record of result) {
|
|
20
|
+
const adminAccess = toBoolean(record.admin_access);
|
|
21
|
+
const appAccess = toBoolean(record.app_access);
|
|
22
|
+
const count = Number(record.count);
|
|
23
|
+
if (adminAccess) {
|
|
24
|
+
counts.admin += count;
|
|
25
|
+
}
|
|
26
|
+
else if (appAccess) {
|
|
27
|
+
counts.app += count;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
counts.api += count;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return counts;
|
|
34
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { PrimaryKey } from '@directus/types';
|
|
1
2
|
import { type Knex } from 'knex';
|
|
2
|
-
export interface
|
|
3
|
+
export interface AccessTypeCount {
|
|
3
4
|
admin: number;
|
|
4
5
|
app: number;
|
|
5
6
|
api: number;
|
|
6
7
|
}
|
|
7
|
-
export declare const getUserCount: (db: Knex) => Promise<
|
|
8
|
+
export declare const getUserCount: (db: Knex, ignoreIds?: PrimaryKey[]) => Promise<AccessTypeCount>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { toBoolean } from '@directus/utils';
|
|
2
2
|
import {} from 'knex';
|
|
3
|
-
export const getUserCount = async (db) => {
|
|
3
|
+
export const getUserCount = async (db, ignoreIds = []) => {
|
|
4
4
|
const counts = {
|
|
5
5
|
admin: 0,
|
|
6
6
|
app: 0,
|
|
@@ -10,20 +10,23 @@ export const getUserCount = async (db) => {
|
|
|
10
10
|
.count('directus_users.id', { as: 'count' })
|
|
11
11
|
.select('directus_roles.admin_access', 'directus_roles.app_access')
|
|
12
12
|
.from('directus_users')
|
|
13
|
+
.whereNotIn('directus_users.id', ignoreIds)
|
|
14
|
+
.andWhere('directus_users.status', 'active')
|
|
13
15
|
.leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
|
|
16
|
+
.where('directus_users.status', '=', 'active')
|
|
14
17
|
.groupBy('directus_roles.admin_access', 'directus_roles.app_access'));
|
|
15
18
|
for (const record of result) {
|
|
16
19
|
const adminAccess = toBoolean(record.admin_access);
|
|
17
20
|
const appAccess = toBoolean(record.app_access);
|
|
18
21
|
const count = Number(record.count);
|
|
19
22
|
if (adminAccess) {
|
|
20
|
-
counts.admin
|
|
23
|
+
counts.admin += count;
|
|
21
24
|
}
|
|
22
25
|
else if (appAccess) {
|
|
23
|
-
counts.app
|
|
26
|
+
counts.app += count;
|
|
24
27
|
}
|
|
25
28
|
else {
|
|
26
|
-
counts.api
|
|
29
|
+
counts.api += count;
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
return counts;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PrimaryKey } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
import { type AccessTypeCount } from './get-user-count.js';
|
|
4
|
+
/**
|
|
5
|
+
* Get the user type counts by role IDs
|
|
6
|
+
*/
|
|
7
|
+
export declare function getUserCountsByRoles(db: Knex, roleIds: PrimaryKey[]): Promise<AccessTypeCount>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { toBoolean } from '@directus/utils';
|
|
2
|
+
import {} from './get-user-count.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get the user type counts by role IDs
|
|
5
|
+
*/
|
|
6
|
+
export async function getUserCountsByRoles(db, roleIds) {
|
|
7
|
+
const counts = {
|
|
8
|
+
admin: 0,
|
|
9
|
+
app: 0,
|
|
10
|
+
api: 0,
|
|
11
|
+
};
|
|
12
|
+
const result = (await db
|
|
13
|
+
.count('directus_users.id', { as: 'count' })
|
|
14
|
+
.select('directus_roles.admin_access', 'directus_roles.app_access')
|
|
15
|
+
.from('directus_users')
|
|
16
|
+
.whereIn('directus_roles.id', roleIds)
|
|
17
|
+
.andWhere('directus_users.status', '=', 'active')
|
|
18
|
+
.leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
|
|
19
|
+
.groupBy('directus_roles.admin_access', 'directus_roles.app_access'));
|
|
20
|
+
for (const record of result) {
|
|
21
|
+
const adminAccess = toBoolean(record.admin_access);
|
|
22
|
+
const appAccess = toBoolean(record.app_access);
|
|
23
|
+
const count = Number(record.count);
|
|
24
|
+
if (adminAccess) {
|
|
25
|
+
counts.admin += count;
|
|
26
|
+
}
|
|
27
|
+
else if (appAccess) {
|
|
28
|
+
counts.app += count;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
counts.api += count;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return counts;
|
|
35
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { isSystemCollection } from '@directus/system-data';
|
|
1
2
|
import {} from 'knex';
|
|
2
3
|
import { getSchema } from '../../utils/get-schema.js';
|
|
3
4
|
import { getItemCount } from './get-item-count.js';
|
|
4
|
-
import { isSystemCollection } from '@directus/system-data';
|
|
5
5
|
/**
|
|
6
6
|
* Sum all passed values together. Meant to be used with .reduce()
|
|
7
7
|
*/
|
|
@@ -11,7 +11,9 @@ export const sum = (acc, val) => (acc += val);
|
|
|
11
11
|
*/
|
|
12
12
|
export const getUserItemCount = async (db) => {
|
|
13
13
|
const schema = await getSchema({ database: db });
|
|
14
|
-
const userCollections = Object.keys(schema.collections)
|
|
14
|
+
const userCollections = Object.keys(schema.collections)
|
|
15
|
+
.filter((collection) => isSystemCollection(collection) === false)
|
|
16
|
+
.map((collection) => ({ collection }));
|
|
15
17
|
const counts = await getItemCount(db, userCollections);
|
|
16
18
|
const collections = userCollections.length;
|
|
17
19
|
const items = Object.values(counts).reduce(sum, 0);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
/**
|
|
3
|
+
* Confirm whether user limits needs to be checked
|
|
4
|
+
*/
|
|
5
|
+
export function shouldCheckUserLimits() {
|
|
6
|
+
const env = useEnv();
|
|
7
|
+
if (Number(env['USERS_ADMIN_ACCESS_LIMIT']) !== Infinity ||
|
|
8
|
+
Number(env['USERS_APP_ACCESS_LIMIT']) !== Infinity ||
|
|
9
|
+
Number(env['USERS_API_ACCESS_LIMIT']) !== Infinity) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "19.
|
|
3
|
+
"version": "19.3.1",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@rollup/plugin-node-resolve": "15.2.3",
|
|
66
66
|
"@rollup/plugin-virtual": "3.0.2",
|
|
67
67
|
"@types/cookie": "0.6.0",
|
|
68
|
-
"argon2": "0.40.
|
|
68
|
+
"argon2": "0.40.3",
|
|
69
69
|
"async": "3.2.5",
|
|
70
70
|
"axios": "1.7.2",
|
|
71
71
|
"busboy": "1.6.0",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"graphql-ws": "5.16.0",
|
|
97
97
|
"helmet": "7.1.0",
|
|
98
98
|
"icc": "3.0.0",
|
|
99
|
-
"inquirer": "9.2.
|
|
99
|
+
"inquirer": "9.2.23",
|
|
100
100
|
"ioredis": "5.4.1",
|
|
101
101
|
"ip-matching": "2.1.2",
|
|
102
102
|
"isolated-vm": "4.7.2",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"keyv": "4.5.4",
|
|
109
109
|
"knex": "3.1.0",
|
|
110
110
|
"ldapjs": "2.3.3",
|
|
111
|
-
"liquidjs": "10.
|
|
111
|
+
"liquidjs": "10.13.1",
|
|
112
112
|
"lodash-es": "4.17.21",
|
|
113
113
|
"marked": "12.0.2",
|
|
114
114
|
"micromustache": "8.0.3",
|
|
@@ -121,14 +121,14 @@
|
|
|
121
121
|
"node-schedule": "2.1.1",
|
|
122
122
|
"nodemailer": "6.9.13",
|
|
123
123
|
"object-hash": "3.0.0",
|
|
124
|
-
"openapi3-ts": "4.3.
|
|
124
|
+
"openapi3-ts": "4.3.2",
|
|
125
125
|
"openid-client": "5.6.5",
|
|
126
126
|
"ora": "8.0.1",
|
|
127
127
|
"otplib": "12.0.1",
|
|
128
128
|
"p-limit": "5.0.0",
|
|
129
129
|
"p-queue": "8.0.1",
|
|
130
130
|
"papaparse": "5.4.1",
|
|
131
|
-
"pino": "9.
|
|
131
|
+
"pino": "9.1.0",
|
|
132
132
|
"pino-http": "9.0.0",
|
|
133
133
|
"pino-http-print": "3.1.0",
|
|
134
134
|
"pino-pretty": "11.0.0",
|
|
@@ -140,35 +140,35 @@
|
|
|
140
140
|
"sharp": "0.33.3",
|
|
141
141
|
"snappy": "7.2.2",
|
|
142
142
|
"stream-json": "1.8.0",
|
|
143
|
-
"tar": "7.
|
|
144
|
-
"tsx": "4.
|
|
143
|
+
"tar": "7.2.0",
|
|
144
|
+
"tsx": "4.12.0",
|
|
145
145
|
"wellknown": "0.5.0",
|
|
146
146
|
"ws": "8.17.0",
|
|
147
147
|
"zod": "3.23.8",
|
|
148
148
|
"zod-validation-error": "3.2.0",
|
|
149
|
-
"@directus/app": "12.1.2",
|
|
150
149
|
"@directus/constants": "11.0.4",
|
|
151
|
-
"@directus/
|
|
152
|
-
"@directus/
|
|
153
|
-
"@directus/extensions": "1.0.
|
|
154
|
-
"@directus/
|
|
155
|
-
"@directus/extensions-
|
|
156
|
-
"@directus/
|
|
157
|
-
"@directus/pressure": "1.0.19",
|
|
158
|
-
"@directus/schema": "11.0.2",
|
|
159
|
-
"@directus/specs": "10.2.9",
|
|
150
|
+
"@directus/app": "12.1.4",
|
|
151
|
+
"@directus/env": "1.1.6",
|
|
152
|
+
"@directus/extensions": "1.0.8",
|
|
153
|
+
"@directus/errors": "0.3.2",
|
|
154
|
+
"@directus/extensions-registry": "1.0.8",
|
|
155
|
+
"@directus/extensions-sdk": "11.0.8",
|
|
160
156
|
"@directus/format-title": "10.1.2",
|
|
157
|
+
"@directus/pressure": "1.0.20",
|
|
158
|
+
"@directus/memory": "1.0.9",
|
|
159
|
+
"@directus/schema": "11.0.3",
|
|
160
|
+
"@directus/specs": "10.2.10",
|
|
161
161
|
"@directus/storage": "10.0.13",
|
|
162
|
-
"@directus/storage-driver-
|
|
163
|
-
"@directus/storage-driver-
|
|
164
|
-
"@directus/storage-driver-gcs": "10.0.22",
|
|
162
|
+
"@directus/storage-driver-azure": "10.0.22",
|
|
163
|
+
"@directus/storage-driver-gcs": "10.0.23",
|
|
165
164
|
"@directus/storage-driver-local": "10.0.20",
|
|
166
|
-
"@directus/storage-driver-
|
|
167
|
-
"@directus/
|
|
168
|
-
"@directus/storage-driver-
|
|
169
|
-
"@directus/
|
|
170
|
-
"directus": "
|
|
171
|
-
"@directus/validation": "0.0.
|
|
165
|
+
"@directus/storage-driver-cloudinary": "10.0.22",
|
|
166
|
+
"@directus/storage-driver-s3": "10.0.23",
|
|
167
|
+
"@directus/storage-driver-supabase": "1.0.14",
|
|
168
|
+
"@directus/system-data": "1.0.4",
|
|
169
|
+
"@directus/utils": "11.0.9",
|
|
170
|
+
"@directus/validation": "0.0.17",
|
|
171
|
+
"directus": "10.12.1"
|
|
172
172
|
},
|
|
173
173
|
"devDependencies": {
|
|
174
174
|
"@ngneat/falso": "7.2.0",
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
"@types/destroy": "1.0.3",
|
|
183
183
|
"@types/encodeurl": "1.0.2",
|
|
184
184
|
"@types/express": "4.17.21",
|
|
185
|
-
"@types/express-serve-static-core": "4.19.
|
|
185
|
+
"@types/express-serve-static-core": "4.19.3",
|
|
186
186
|
"@types/fs-extra": "11.0.4",
|
|
187
187
|
"@types/glob-to-regexp": "0.4.4",
|
|
188
188
|
"@types/inquirer": "9.0.7",
|
|
@@ -210,8 +210,8 @@
|
|
|
210
210
|
"typescript": "5.4.5",
|
|
211
211
|
"vitest": "1.5.3",
|
|
212
212
|
"@directus/random": "0.2.8",
|
|
213
|
-
"@directus/
|
|
214
|
-
"@directus/
|
|
213
|
+
"@directus/tsconfig": "1.0.1",
|
|
214
|
+
"@directus/types": "11.1.3"
|
|
215
215
|
},
|
|
216
216
|
"optionalDependencies": {
|
|
217
217
|
"@keyv/redis": "2.8.4",
|