@directus/api 19.2.0 → 19.3.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/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 +200 -11
- package/dist/services/users.js +64 -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/package.json +25 -25
|
@@ -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,10 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
|
|
2
2
|
import { getMatch } from 'ip-matching';
|
|
3
|
+
import { checkIncreasedUserLimits } from '../telemetry/utils/check-increased-user-limits.js';
|
|
4
|
+
import { getRoleCountsByUsers } from '../telemetry/utils/get-role-counts-by-users.js';
|
|
5
|
+
import {} from '../telemetry/utils/get-user-count.js';
|
|
6
|
+
import { getUserCountsByRoles } from '../telemetry/utils/get-user-counts-by-roles.js';
|
|
3
7
|
import { transaction } from '../utils/transaction.js';
|
|
4
8
|
import { ItemsService } from './items.js';
|
|
5
9
|
import { PermissionsService } from './permissions/index.js';
|
|
6
10
|
import { PresetsService } from './presets.js';
|
|
7
11
|
import { UsersService } from './users.js';
|
|
12
|
+
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
13
|
+
import { omit } from 'lodash-es';
|
|
8
14
|
export class RolesService extends ItemsService {
|
|
9
15
|
constructor(options) {
|
|
10
16
|
super('directus_roles', options);
|
|
@@ -24,8 +30,9 @@ export class RolesService extends ItemsService {
|
|
|
24
30
|
}
|
|
25
31
|
async checkForOtherAdminUsers(key, users) {
|
|
26
32
|
const role = await this.knex.select('admin_access').from('directus_roles').where('id', '=', key).first();
|
|
33
|
+
// No-op if role doesn't exist
|
|
27
34
|
if (!role)
|
|
28
|
-
|
|
35
|
+
return;
|
|
29
36
|
const usersBefore = (await this.knex.select('id').from('directus_users').where('role', '=', key)).map((user) => user.id);
|
|
30
37
|
const usersAdded = [];
|
|
31
38
|
const usersUpdated = [];
|
|
@@ -151,44 +158,211 @@ export class RolesService extends ItemsService {
|
|
|
151
158
|
});
|
|
152
159
|
}
|
|
153
160
|
}
|
|
161
|
+
getRoleAccessType(data) {
|
|
162
|
+
if ('admin_access' in data && data['admin_access'] === true) {
|
|
163
|
+
return 'admin';
|
|
164
|
+
}
|
|
165
|
+
else if (('app_access' in data && data['app_access'] === true) || 'app_access' in data === false) {
|
|
166
|
+
return 'app';
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
return 'api';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
154
172
|
async createOne(data, opts) {
|
|
155
173
|
this.assertValidIpAccess(data);
|
|
174
|
+
const increasedCounts = {
|
|
175
|
+
admin: 0,
|
|
176
|
+
app: 0,
|
|
177
|
+
api: 0,
|
|
178
|
+
};
|
|
179
|
+
const existingIds = [];
|
|
180
|
+
if ('users' in data) {
|
|
181
|
+
const type = this.getRoleAccessType(data);
|
|
182
|
+
increasedCounts[type] += data['users'].length;
|
|
183
|
+
for (const user of data['users']) {
|
|
184
|
+
if (typeof user === 'string') {
|
|
185
|
+
existingIds.push(user);
|
|
186
|
+
}
|
|
187
|
+
else if (typeof user === 'object' && 'id' in user) {
|
|
188
|
+
existingIds.push(user['id']);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
|
|
156
193
|
return super.createOne(data, opts);
|
|
157
194
|
}
|
|
158
195
|
async createMany(data, opts) {
|
|
196
|
+
const increasedCounts = {
|
|
197
|
+
admin: 0,
|
|
198
|
+
app: 0,
|
|
199
|
+
api: 0,
|
|
200
|
+
};
|
|
201
|
+
const existingIds = [];
|
|
159
202
|
for (const partialItem of data) {
|
|
160
203
|
this.assertValidIpAccess(partialItem);
|
|
204
|
+
if ('users' in partialItem) {
|
|
205
|
+
const type = this.getRoleAccessType(partialItem);
|
|
206
|
+
increasedCounts[type] += partialItem['users'].length;
|
|
207
|
+
for (const user of partialItem['users']) {
|
|
208
|
+
if (typeof user === 'string') {
|
|
209
|
+
existingIds.push(user);
|
|
210
|
+
}
|
|
211
|
+
else if (typeof user === 'object' && 'id' in user) {
|
|
212
|
+
existingIds.push(user['id']);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
161
216
|
}
|
|
217
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
|
|
162
218
|
return super.createMany(data, opts);
|
|
163
219
|
}
|
|
164
220
|
async updateOne(key, data, opts) {
|
|
165
221
|
this.assertValidIpAccess(data);
|
|
166
222
|
try {
|
|
223
|
+
const increasedCounts = {
|
|
224
|
+
admin: 0,
|
|
225
|
+
app: 0,
|
|
226
|
+
api: 0,
|
|
227
|
+
};
|
|
228
|
+
let increasedUsers = 0;
|
|
229
|
+
const existingIds = [];
|
|
230
|
+
let existingRole = await this.knex
|
|
231
|
+
.count('directus_users.id', { as: 'count' })
|
|
232
|
+
.select('directus_roles.admin_access', 'directus_roles.app_access')
|
|
233
|
+
.from('directus_users')
|
|
234
|
+
.where('directus_roles.id', '=', key)
|
|
235
|
+
.andWhere('directus_users.status', '=', 'active')
|
|
236
|
+
.leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
|
|
237
|
+
.groupBy('directus_roles.admin_access', 'directus_roles.app_access')
|
|
238
|
+
.first();
|
|
239
|
+
if (!existingRole) {
|
|
240
|
+
try {
|
|
241
|
+
const role = (await this.knex
|
|
242
|
+
.select('admin_access', 'app_access')
|
|
243
|
+
.from('directus_roles')
|
|
244
|
+
.where('id', '=', key)
|
|
245
|
+
.first()) ?? { admin_access: null, app_access: null };
|
|
246
|
+
existingRole = { count: 0, ...role };
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
existingRole = { count: 0, admin_access: null, app_access: null };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
167
252
|
if ('users' in data) {
|
|
168
253
|
await this.checkForOtherAdminUsers(key, data['users']);
|
|
254
|
+
const users = data['users'];
|
|
255
|
+
if (Array.isArray(users)) {
|
|
256
|
+
increasedUsers = users.length - Number(existingRole.count);
|
|
257
|
+
for (const user of users) {
|
|
258
|
+
if (typeof user === 'string') {
|
|
259
|
+
existingIds.push(user);
|
|
260
|
+
}
|
|
261
|
+
else if (typeof user === 'object' && 'id' in user) {
|
|
262
|
+
existingIds.push(user['id']);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
increasedUsers += users.create.length;
|
|
268
|
+
increasedUsers -= users.delete.length;
|
|
269
|
+
const userIds = [];
|
|
270
|
+
for (const user of users.update) {
|
|
271
|
+
if ('status' in user) {
|
|
272
|
+
// account for users being activated and deactivated
|
|
273
|
+
if (user['status'] === 'active') {
|
|
274
|
+
increasedUsers++;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
increasedUsers--;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
userIds.push(user.id);
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const existingCounts = await getRoleCountsByUsers(this.knex, userIds);
|
|
284
|
+
if (existingRole.admin_access) {
|
|
285
|
+
increasedUsers += existingCounts.app + existingCounts.api;
|
|
286
|
+
}
|
|
287
|
+
else if (existingRole.app_access) {
|
|
288
|
+
increasedUsers += existingCounts.admin + existingCounts.api;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
increasedUsers += existingCounts.admin + existingCounts.app;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// ignore failed user call
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
let isAccessChanged = false;
|
|
300
|
+
let accessType = 'api';
|
|
301
|
+
if ('app_access' in data) {
|
|
302
|
+
if (data['app_access'] === true) {
|
|
303
|
+
accessType = 'app';
|
|
304
|
+
if (!existingRole.app_access)
|
|
305
|
+
isAccessChanged = true;
|
|
306
|
+
}
|
|
307
|
+
else if (existingRole.app_access) {
|
|
308
|
+
isAccessChanged = true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (existingRole.app_access) {
|
|
312
|
+
accessType = 'app';
|
|
313
|
+
}
|
|
314
|
+
if ('admin_access' in data) {
|
|
315
|
+
if (data['admin_access'] === true) {
|
|
316
|
+
accessType = 'admin';
|
|
317
|
+
if (!existingRole.admin_access)
|
|
318
|
+
isAccessChanged = true;
|
|
319
|
+
}
|
|
320
|
+
else if (existingRole.admin_access) {
|
|
321
|
+
isAccessChanged = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else if (existingRole.admin_access) {
|
|
325
|
+
accessType = 'admin';
|
|
326
|
+
}
|
|
327
|
+
if (isAccessChanged) {
|
|
328
|
+
increasedCounts[accessType] += Number(existingRole.count);
|
|
169
329
|
}
|
|
330
|
+
increasedCounts[accessType] += increasedUsers;
|
|
331
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
|
|
170
332
|
}
|
|
171
333
|
catch (err) {
|
|
172
334
|
(opts || (opts = {})).preMutationError = err;
|
|
173
335
|
}
|
|
174
336
|
return super.updateOne(key, data, opts);
|
|
175
337
|
}
|
|
176
|
-
async updateBatch(data, opts) {
|
|
338
|
+
async updateBatch(data, opts = {}) {
|
|
177
339
|
for (const partialItem of data) {
|
|
178
340
|
this.assertValidIpAccess(partialItem);
|
|
179
341
|
}
|
|
180
342
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
181
|
-
|
|
182
|
-
|
|
343
|
+
if (!opts.mutationTracker) {
|
|
344
|
+
opts.mutationTracker = this.createMutationTracker();
|
|
345
|
+
}
|
|
346
|
+
const keys = [];
|
|
183
347
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
348
|
+
await transaction(this.knex, async (trx) => {
|
|
349
|
+
const service = new RolesService({
|
|
350
|
+
accountability: this.accountability,
|
|
351
|
+
knex: trx,
|
|
352
|
+
schema: this.schema,
|
|
353
|
+
});
|
|
354
|
+
for (const item of data) {
|
|
355
|
+
const combinedOpts = Object.assign({ autoPurgeCache: false }, opts);
|
|
356
|
+
keys.push(await service.updateOne(item[primaryKeyField], omit(item, primaryKeyField), combinedOpts));
|
|
357
|
+
}
|
|
358
|
+
});
|
|
187
359
|
}
|
|
188
|
-
|
|
189
|
-
(
|
|
360
|
+
finally {
|
|
361
|
+
if (shouldClearCache(this.cache, opts, this.collection)) {
|
|
362
|
+
await this.cache.clear();
|
|
363
|
+
}
|
|
190
364
|
}
|
|
191
|
-
return
|
|
365
|
+
return keys;
|
|
192
366
|
}
|
|
193
367
|
async updateMany(keys, data, opts) {
|
|
194
368
|
this.assertValidIpAccess(data);
|
|
@@ -196,6 +370,21 @@ export class RolesService extends ItemsService {
|
|
|
196
370
|
if ('admin_access' in data && data['admin_access'] === false) {
|
|
197
371
|
await this.checkForOtherAdminRoles(keys);
|
|
198
372
|
}
|
|
373
|
+
if ('admin_access' in data || 'app_access' in data) {
|
|
374
|
+
const existingCounts = await getUserCountsByRoles(this.knex, keys);
|
|
375
|
+
const increasedCounts = {
|
|
376
|
+
admin: 0,
|
|
377
|
+
app: 0,
|
|
378
|
+
api: 0,
|
|
379
|
+
};
|
|
380
|
+
const type = this.getRoleAccessType(data);
|
|
381
|
+
for (const [existingType, existingCount] of Object.entries(existingCounts)) {
|
|
382
|
+
if (existingType === type)
|
|
383
|
+
continue;
|
|
384
|
+
increasedCounts[type] += existingCount;
|
|
385
|
+
}
|
|
386
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts);
|
|
387
|
+
}
|
|
199
388
|
}
|
|
200
389
|
catch (err) {
|
|
201
390
|
(opts || (opts = {})).preMutationError = err;
|
package/dist/services/users.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
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';
|
|
11
15
|
import { getSecret } from '../utils/get-secret.js';
|
|
12
16
|
import isUrlAllowed from '../utils/is-url-allowed.js';
|
|
13
17
|
import { verifyJWT } from '../utils/jwt.js';
|
|
@@ -164,6 +168,7 @@ export class UsersService extends ItemsService {
|
|
|
164
168
|
async createMany(data, opts) {
|
|
165
169
|
const emails = data['map']((payload) => payload['email']).filter((email) => email);
|
|
166
170
|
const passwords = data['map']((payload) => payload['password']).filter((password) => password);
|
|
171
|
+
const roles = data['map']((payload) => payload['role']).filter((role) => role);
|
|
167
172
|
try {
|
|
168
173
|
if (emails.length) {
|
|
169
174
|
this.validateEmail(emails);
|
|
@@ -172,6 +177,33 @@ export class UsersService extends ItemsService {
|
|
|
172
177
|
if (passwords.length) {
|
|
173
178
|
await this.checkPasswordPolicy(passwords);
|
|
174
179
|
}
|
|
180
|
+
if (roles.length) {
|
|
181
|
+
const increasedCounts = {
|
|
182
|
+
admin: 0,
|
|
183
|
+
app: 0,
|
|
184
|
+
api: 0,
|
|
185
|
+
};
|
|
186
|
+
const existingRoles = [];
|
|
187
|
+
for (const role of roles) {
|
|
188
|
+
if (typeof role === 'object') {
|
|
189
|
+
if ('admin_access' in role && role['admin_access'] === true) {
|
|
190
|
+
increasedCounts.admin++;
|
|
191
|
+
}
|
|
192
|
+
else if ('app_access' in role && role['app_access'] === true) {
|
|
193
|
+
increasedCounts.app++;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
increasedCounts.api++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
existingRoles.push(role);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const existingRoleCounts = await getRoleCountsByRoles(this.knex, existingRoles);
|
|
204
|
+
mergeWith(increasedCounts, existingRoleCounts, (x, y) => x + y);
|
|
205
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts);
|
|
206
|
+
}
|
|
175
207
|
}
|
|
176
208
|
catch (err) {
|
|
177
209
|
(opts || (opts = {})).preMutationError = err;
|
|
@@ -226,7 +258,11 @@ export class UsersService extends ItemsService {
|
|
|
226
258
|
const role = data['role']?.id ?? data['role'];
|
|
227
259
|
let newRole;
|
|
228
260
|
if (typeof role === 'string') {
|
|
229
|
-
newRole = await this.knex
|
|
261
|
+
newRole = await this.knex
|
|
262
|
+
.select('admin_access', 'app_access')
|
|
263
|
+
.from('directus_roles')
|
|
264
|
+
.where('id', role)
|
|
265
|
+
.first();
|
|
230
266
|
}
|
|
231
267
|
else {
|
|
232
268
|
newRole = role;
|
|
@@ -234,10 +270,35 @@ export class UsersService extends ItemsService {
|
|
|
234
270
|
if (!newRole?.admin_access) {
|
|
235
271
|
await this.checkRemainingAdminExistence(keys);
|
|
236
272
|
}
|
|
273
|
+
if (newRole) {
|
|
274
|
+
const existingCounts = await getRoleCountsByUsers(this.knex, keys);
|
|
275
|
+
const increasedCounts = {
|
|
276
|
+
admin: 0,
|
|
277
|
+
app: 0,
|
|
278
|
+
api: 0,
|
|
279
|
+
};
|
|
280
|
+
if (toBoolean(newRole.admin_access)) {
|
|
281
|
+
increasedCounts.admin = keys.length - existingCounts.admin;
|
|
282
|
+
}
|
|
283
|
+
else if (toBoolean(newRole.app_access)) {
|
|
284
|
+
increasedCounts.app = keys.length - existingCounts.app;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
increasedCounts.api = keys.length - existingCounts.api;
|
|
288
|
+
}
|
|
289
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (data['role'] === null) {
|
|
293
|
+
await checkIncreasedUserLimits(this.knex, { admin: 0, app: 0, api: 1 });
|
|
237
294
|
}
|
|
238
295
|
if (data['status'] !== undefined && data['status'] !== 'active') {
|
|
239
296
|
await this.checkRemainingActiveAdmin(keys);
|
|
240
297
|
}
|
|
298
|
+
if (data['status'] === 'active') {
|
|
299
|
+
const increasedCounts = await getRoleCountsByUsers(this.knex, keys, { inactiveUsers: true });
|
|
300
|
+
await checkIncreasedUserLimits(this.knex, increasedCounts);
|
|
301
|
+
}
|
|
241
302
|
if (data['email']) {
|
|
242
303
|
if (keys.length > 1) {
|
|
243
304
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "19.
|
|
3
|
+
"version": "19.3.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -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",
|
|
@@ -146,29 +146,29 @@
|
|
|
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.
|
|
149
|
+
"@directus/app": "12.1.3",
|
|
150
|
+
"@directus/env": "1.1.6",
|
|
150
151
|
"@directus/constants": "11.0.4",
|
|
151
|
-
"@directus/
|
|
152
|
-
"@directus/
|
|
153
|
-
"@directus/extensions": "1.0.
|
|
154
|
-
"@directus/extensions-registry": "1.0.6",
|
|
155
|
-
"@directus/extensions-sdk": "11.0.6",
|
|
156
|
-
"@directus/memory": "1.0.8",
|
|
157
|
-
"@directus/pressure": "1.0.19",
|
|
158
|
-
"@directus/schema": "11.0.2",
|
|
159
|
-
"@directus/specs": "10.2.9",
|
|
152
|
+
"@directus/errors": "0.3.2",
|
|
153
|
+
"@directus/extensions": "1.0.7",
|
|
154
|
+
"@directus/extensions-registry": "1.0.7",
|
|
160
155
|
"@directus/format-title": "10.1.2",
|
|
156
|
+
"@directus/extensions-sdk": "11.0.7",
|
|
157
|
+
"@directus/pressure": "1.0.20",
|
|
158
|
+
"@directus/memory": "1.0.9",
|
|
159
|
+
"@directus/schema": "11.0.2",
|
|
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.
|
|
162
|
+
"@directus/storage-driver-azure": "10.0.22",
|
|
163
|
+
"@directus/storage-driver-cloudinary": "10.0.22",
|
|
164
|
+
"@directus/storage-driver-gcs": "10.0.23",
|
|
165
165
|
"@directus/storage-driver-local": "10.0.20",
|
|
166
|
-
"@directus/storage-driver-supabase": "1.0.
|
|
167
|
-
"@directus/
|
|
168
|
-
"@directus/
|
|
169
|
-
"@directus/utils": "11.0.
|
|
170
|
-
"directus": "10.
|
|
171
|
-
"@directus/validation": "0.0.
|
|
166
|
+
"@directus/storage-driver-supabase": "1.0.14",
|
|
167
|
+
"@directus/storage-driver-s3": "10.0.23",
|
|
168
|
+
"@directus/system-data": "1.0.4",
|
|
169
|
+
"@directus/utils": "11.0.9",
|
|
170
|
+
"directus": "10.12.0",
|
|
171
|
+
"@directus/validation": "0.0.17"
|
|
172
172
|
},
|
|
173
173
|
"devDependencies": {
|
|
174
174
|
"@ngneat/falso": "7.2.0",
|
|
@@ -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.2"
|
|
215
215
|
},
|
|
216
216
|
"optionalDependencies": {
|
|
217
217
|
"@keyv/redis": "2.8.4",
|