@directus/api 19.1.1 → 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/controllers/users.js +1 -0
- package/dist/controllers/utils.js +7 -5
- 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 +11 -8
- package/dist/services/graphql/index.js +15 -11
- package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +8 -0
- package/dist/services/graphql/utils/sanitize-gql-schema.js +80 -0
- package/dist/services/roles.d.ts +1 -0
- package/dist/services/roles.js +200 -11
- package/dist/services/users.d.ts +1 -1
- package/dist/services/users.js +86 -17
- 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 +28 -28
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.d.ts
CHANGED
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';
|
|
@@ -126,14 +130,14 @@ export class UsersService extends ItemsService {
|
|
|
126
130
|
.first();
|
|
127
131
|
}
|
|
128
132
|
/**
|
|
129
|
-
* Create
|
|
133
|
+
* Create URL for inviting users
|
|
130
134
|
*/
|
|
131
135
|
inviteUrl(email, url) {
|
|
132
136
|
const payload = { email, scope: 'invite' };
|
|
133
137
|
const token = jwt.sign(payload, getSecret(), { expiresIn: '7d', issuer: 'directus' });
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
return (url ? new Url(url) : new Url(env['PUBLIC_URL']).addPath('admin', 'accept-invite'))
|
|
139
|
+
.setQuery('token', token)
|
|
140
|
+
.toString();
|
|
137
141
|
}
|
|
138
142
|
/**
|
|
139
143
|
* Validate array of emails. Intended to be used with create/update users
|
|
@@ -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({
|
|
@@ -314,7 +375,7 @@ export class UsersService extends ItemsService {
|
|
|
314
375
|
const opts = {};
|
|
315
376
|
try {
|
|
316
377
|
if (url && isUrlAllowed(url, env['USER_INVITE_URL_ALLOW_LIST']) === false) {
|
|
317
|
-
throw new InvalidPayloadError({ reason: `
|
|
378
|
+
throw new InvalidPayloadError({ reason: `URL "${url}" can't be used to invite users` });
|
|
318
379
|
}
|
|
319
380
|
}
|
|
320
381
|
catch (err) {
|
|
@@ -373,6 +434,12 @@ export class UsersService extends ItemsService {
|
|
|
373
434
|
await service.updateOne(user.id, { password, status: 'active' });
|
|
374
435
|
}
|
|
375
436
|
async registerUser(input) {
|
|
437
|
+
if (input.verification_url &&
|
|
438
|
+
isUrlAllowed(input.verification_url, env['USER_REGISTER_URL_ALLOW_LIST']) === false) {
|
|
439
|
+
throw new InvalidPayloadError({
|
|
440
|
+
reason: `URL "${input.verification_url}" can't be used to verify registered users`,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
376
443
|
const STALL_TIME = env['REGISTER_STALL_TIME'];
|
|
377
444
|
const timeStart = performance.now();
|
|
378
445
|
const serviceOptions = { accountability: this.accountability, schema: this.schema };
|
|
@@ -424,9 +491,11 @@ export class UsersService extends ItemsService {
|
|
|
424
491
|
expiresIn: env['EMAIL_VERIFICATION_TOKEN_TTL'],
|
|
425
492
|
issuer: 'directus',
|
|
426
493
|
});
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
.
|
|
494
|
+
const verificationUrl = (input.verification_url
|
|
495
|
+
? new Url(input.verification_url)
|
|
496
|
+
: new Url(env['PUBLIC_URL']).addPath('users', 'register', 'verify-email'))
|
|
497
|
+
.setQuery('token', token)
|
|
498
|
+
.toString();
|
|
430
499
|
mailService
|
|
431
500
|
.send({
|
|
432
501
|
to: input.email,
|
|
@@ -434,7 +503,7 @@ export class UsersService extends ItemsService {
|
|
|
434
503
|
template: {
|
|
435
504
|
name: 'user-registration',
|
|
436
505
|
data: {
|
|
437
|
-
url:
|
|
506
|
+
url: verificationUrl,
|
|
438
507
|
email: input.email,
|
|
439
508
|
first_name,
|
|
440
509
|
last_name,
|
|
@@ -467,7 +536,7 @@ export class UsersService extends ItemsService {
|
|
|
467
536
|
throw new ForbiddenError();
|
|
468
537
|
}
|
|
469
538
|
if (url && isUrlAllowed(url, env['PASSWORD_RESET_URL_ALLOW_LIST']) === false) {
|
|
470
|
-
throw new InvalidPayloadError({ reason: `
|
|
539
|
+
throw new InvalidPayloadError({ reason: `URL "${url}" can't be used to reset passwords` });
|
|
471
540
|
}
|
|
472
541
|
const mailService = new MailService({
|
|
473
542
|
schema: this.schema,
|
|
@@ -476,9 +545,9 @@ export class UsersService extends ItemsService {
|
|
|
476
545
|
});
|
|
477
546
|
const payload = { email: user.email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
|
|
478
547
|
const token = jwt.sign(payload, getSecret(), { expiresIn: '1d', issuer: 'directus' });
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
548
|
+
const acceptUrl = (url ? new Url(url) : new Url(env['PUBLIC_URL']).addPath('admin', 'reset-password'))
|
|
549
|
+
.setQuery('token', token)
|
|
550
|
+
.toString();
|
|
482
551
|
const subjectLine = subject ? subject : 'Password Reset Request';
|
|
483
552
|
mailService
|
|
484
553
|
.send({
|
|
@@ -487,7 +556,7 @@ export class UsersService extends ItemsService {
|
|
|
487
556
|
template: {
|
|
488
557
|
name: 'password-reset',
|
|
489
558
|
data: {
|
|
490
|
-
url:
|
|
559
|
+
url: acceptUrl,
|
|
491
560
|
email: user.email,
|
|
492
561
|
},
|
|
493
562
|
},
|
|
@@ -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
|
+
};
|