@directus/api 19.0.2 → 19.1.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/app.js +8 -7
- package/dist/auth/drivers/oauth2.js +3 -2
- package/dist/auth/drivers/openid.js +3 -2
- package/dist/cli/utils/create-env/env-stub.liquid +0 -3
- package/dist/cli/utils/create-env/index.js +0 -2
- package/dist/controllers/auth.js +3 -2
- package/dist/controllers/extensions.js +30 -19
- package/dist/controllers/users.js +25 -0
- package/dist/database/helpers/fn/types.js +13 -4
- package/dist/database/helpers/index.d.ts +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/number/dialects/default.d.ts +3 -0
- package/dist/database/helpers/number/dialects/default.js +3 -0
- package/dist/database/helpers/number/dialects/mssql.d.ts +7 -0
- package/dist/database/helpers/number/dialects/mssql.js +11 -0
- package/dist/database/helpers/number/dialects/oracle.d.ts +6 -0
- package/dist/database/helpers/number/dialects/oracle.js +7 -0
- package/dist/database/helpers/number/dialects/postgres.d.ts +5 -0
- package/dist/database/helpers/number/dialects/postgres.js +15 -0
- package/dist/database/helpers/number/dialects/sqlite.d.ts +6 -0
- package/dist/database/helpers/number/dialects/sqlite.js +7 -0
- package/dist/database/helpers/number/index.d.ts +7 -0
- package/dist/database/helpers/number/index.js +7 -0
- package/dist/database/helpers/number/types.d.ts +12 -0
- package/dist/database/helpers/number/types.js +9 -0
- package/dist/database/helpers/number/utils/decimal-limit.d.ts +4 -0
- package/dist/database/helpers/number/utils/decimal-limit.js +10 -0
- package/dist/database/helpers/number/utils/maybe-stringify-big-int.d.ts +1 -0
- package/dist/database/helpers/number/utils/maybe-stringify-big-int.js +6 -0
- package/dist/database/helpers/number/utils/number-in-range.d.ts +3 -0
- package/dist/database/helpers/number/utils/number-in-range.js +20 -0
- package/dist/database/migrations/20240422A-public-registration.d.ts +3 -0
- package/dist/database/migrations/20240422A-public-registration.js +14 -0
- package/dist/database/migrations/20240515A-add-session-window.d.ts +3 -0
- package/dist/database/migrations/20240515A-add-session-window.js +10 -0
- package/dist/database/run-ast.js +5 -4
- package/dist/extensions/lib/get-extensions-settings.js +48 -11
- package/dist/extensions/lib/installation/manager.js +2 -2
- package/dist/middleware/authenticate.d.ts +1 -1
- package/dist/middleware/authenticate.js +17 -2
- package/dist/middleware/rate-limiter-global.js +1 -1
- package/dist/middleware/rate-limiter-registration.d.ts +5 -0
- package/dist/middleware/rate-limiter-registration.js +32 -0
- package/dist/services/authentication.d.ts +1 -0
- package/dist/services/authentication.js +63 -10
- package/dist/services/authorization.js +4 -4
- package/dist/services/fields.js +2 -2
- package/dist/services/graphql/index.js +41 -2
- package/dist/services/mail/templates/user-registration.liquid +37 -0
- package/dist/services/meta.js +1 -1
- package/dist/services/payload.d.ts +2 -0
- package/dist/services/payload.js +16 -4
- package/dist/services/server.js +3 -1
- package/dist/services/shares.js +2 -1
- package/dist/services/users.d.ts +3 -1
- package/dist/services/users.js +92 -5
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +61 -34
- package/dist/utils/get-accountability-for-token.js +6 -3
- package/dist/utils/get-secret.d.ts +4 -0
- package/dist/utils/get-secret.js +14 -0
- package/dist/utils/parse-filter-key.d.ts +7 -0
- package/dist/utils/parse-filter-key.js +22 -0
- package/dist/utils/parse-numeric-string.d.ts +2 -0
- package/dist/utils/parse-numeric-string.js +21 -0
- package/dist/utils/sanitize-query.js +10 -5
- package/dist/utils/transaction.d.ts +1 -1
- package/dist/utils/transaction.js +39 -2
- package/dist/utils/validate-query.js +0 -2
- package/dist/utils/verify-session-jwt.d.ts +7 -0
- package/dist/utils/verify-session-jwt.js +22 -0
- package/dist/websocket/messages.d.ts +78 -50
- package/package.json +60 -61
- package/dist/utils/strip-function.d.ts +0 -4
- package/dist/utils/strip-function.js +0 -12
|
@@ -15,9 +15,33 @@ export const getExtensionsSettings = async ({ local, module, registry, }) => {
|
|
|
15
15
|
});
|
|
16
16
|
const existingSettings = await service.extensionsItemService.readByQuery({ limit: -1 });
|
|
17
17
|
const newSettings = [];
|
|
18
|
+
const removedSettingIds = [];
|
|
18
19
|
const localSettings = existingSettings.filter(({ source }) => source === 'local');
|
|
19
20
|
const registrySettings = existingSettings.filter(({ source }) => source === 'registry');
|
|
20
21
|
const moduleSettings = existingSettings.filter(({ source }) => source === 'module');
|
|
22
|
+
const updateBundleEntriesSettings = (bundle, bundleSettings, allSettings) => {
|
|
23
|
+
const bundleEntriesSettings = allSettings.filter(({ bundle }) => bundle === bundleSettings.id);
|
|
24
|
+
// Remove settings of removed bundle entries from the DB
|
|
25
|
+
for (const entry of bundleEntriesSettings) {
|
|
26
|
+
const entryInBundle = bundle.entries.some(({ name }) => name === entry.folder);
|
|
27
|
+
if (entryInBundle)
|
|
28
|
+
continue;
|
|
29
|
+
removedSettingIds.push(entry.id);
|
|
30
|
+
}
|
|
31
|
+
// Add new bundle entries to the settings
|
|
32
|
+
for (const entry of bundle.entries) {
|
|
33
|
+
const settingsExist = bundleEntriesSettings.some(({ folder }) => folder === entry.name);
|
|
34
|
+
if (settingsExist)
|
|
35
|
+
continue;
|
|
36
|
+
newSettings.push({
|
|
37
|
+
id: randomUUID(),
|
|
38
|
+
enabled: bundleSettings.enabled,
|
|
39
|
+
source: bundleSettings.source,
|
|
40
|
+
bundle: bundleSettings.id,
|
|
41
|
+
folder: entry.name,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
21
45
|
const generateSettingsEntry = (folder, extension, source) => {
|
|
22
46
|
if (extension.type === 'bundle') {
|
|
23
47
|
const bundleId = randomUUID();
|
|
@@ -49,9 +73,13 @@ export const getExtensionsSettings = async ({ local, module, registry, }) => {
|
|
|
49
73
|
}
|
|
50
74
|
};
|
|
51
75
|
for (const [folder, extension] of local.entries()) {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
76
|
+
const existingSettings = localSettings.find((settings) => settings.folder === folder);
|
|
77
|
+
if (existingSettings) {
|
|
78
|
+
if (extension.type === 'bundle') {
|
|
79
|
+
updateBundleEntriesSettings(extension, existingSettings, localSettings);
|
|
80
|
+
}
|
|
54
81
|
continue;
|
|
82
|
+
}
|
|
55
83
|
const settingsForName = localSettings.find((settings) => settings.folder === extension.name);
|
|
56
84
|
/*
|
|
57
85
|
* TODO: Consider removing this in follow-up versions after v10.10.0
|
|
@@ -66,22 +94,31 @@ export const getExtensionsSettings = async ({ local, module, registry, }) => {
|
|
|
66
94
|
await service.extensionsItemService.updateOne(settingsForName.id, { folder });
|
|
67
95
|
continue;
|
|
68
96
|
}
|
|
69
|
-
|
|
70
|
-
generateSettingsEntry(folder, extension, 'local');
|
|
97
|
+
generateSettingsEntry(folder, extension, 'local');
|
|
71
98
|
}
|
|
72
99
|
for (const [folder, extension] of module.entries()) {
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
100
|
+
const existingSettings = moduleSettings.find((settings) => settings.folder === folder);
|
|
101
|
+
if (!existingSettings) {
|
|
75
102
|
generateSettingsEntry(folder, extension, 'module');
|
|
103
|
+
}
|
|
104
|
+
else if (extension.type === 'bundle') {
|
|
105
|
+
updateBundleEntriesSettings(extension, existingSettings, moduleSettings);
|
|
106
|
+
}
|
|
76
107
|
}
|
|
77
108
|
for (const [folder, extension] of registry.entries()) {
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
109
|
+
const existingSettings = registrySettings.find((settings) => settings.folder === folder);
|
|
110
|
+
if (!existingSettings) {
|
|
80
111
|
generateSettingsEntry(folder, extension, 'registry');
|
|
112
|
+
}
|
|
113
|
+
else if (extension.type === 'bundle') {
|
|
114
|
+
updateBundleEntriesSettings(extension, existingSettings, registrySettings);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (removedSettingIds.length > 0) {
|
|
118
|
+
await service.extensionsItemService.deleteMany(removedSettingIds);
|
|
81
119
|
}
|
|
82
120
|
if (newSettings.length > 0) {
|
|
83
|
-
await
|
|
121
|
+
await service.extensionsItemService.createMany(newSettings);
|
|
84
122
|
}
|
|
85
|
-
|
|
86
|
-
return settings;
|
|
123
|
+
return [...existingSettings.filter(({ id }) => !removedSettingIds.includes(id)), ...newSettings];
|
|
87
124
|
};
|
|
@@ -8,7 +8,7 @@ import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
|
8
8
|
import { Readable } from 'node:stream';
|
|
9
9
|
import Queue from 'p-queue';
|
|
10
10
|
import { join } from 'path';
|
|
11
|
-
import
|
|
11
|
+
import { extract } from 'tar';
|
|
12
12
|
import { useLogger } from '../../../logger.js';
|
|
13
13
|
import { getStorage } from '../../../storage/index.js';
|
|
14
14
|
import { getExtensionsPath } from '../get-extensions-path.js';
|
|
@@ -36,7 +36,7 @@ export class InstallationManager {
|
|
|
36
36
|
* NPM modules that are packed are always tarballed in a folder called "package"
|
|
37
37
|
*/
|
|
38
38
|
const extractedPath = 'package';
|
|
39
|
-
await
|
|
39
|
+
await extract({
|
|
40
40
|
file: tarPath,
|
|
41
41
|
cwd: tempDir,
|
|
42
42
|
});
|
|
@@ -4,6 +4,6 @@ import type { NextFunction, Request, Response } from 'express';
|
|
|
4
4
|
/**
|
|
5
5
|
* Verify the passed JWT and assign the user ID and role to `req`
|
|
6
6
|
*/
|
|
7
|
-
export declare const handler: (req: Request,
|
|
7
|
+
export declare const handler: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
8
8
|
declare const _default: (req: Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: NextFunction) => Promise<void>;
|
|
9
9
|
export default _default;
|
|
@@ -4,10 +4,14 @@ import emitter from '../emitter.js';
|
|
|
4
4
|
import asyncHandler from '../utils/async-handler.js';
|
|
5
5
|
import { getAccountabilityForToken } from '../utils/get-accountability-for-token.js';
|
|
6
6
|
import { getIPFromReq } from '../utils/get-ip-from-req.js';
|
|
7
|
+
import { ErrorCode, isDirectusError } from '@directus/errors';
|
|
8
|
+
import { useEnv } from '@directus/env';
|
|
9
|
+
import { SESSION_COOKIE_OPTIONS } from '../constants.js';
|
|
7
10
|
/**
|
|
8
11
|
* Verify the passed JWT and assign the user ID and role to `req`
|
|
9
12
|
*/
|
|
10
|
-
export const handler = async (req,
|
|
13
|
+
export const handler = async (req, res, next) => {
|
|
14
|
+
const env = useEnv();
|
|
11
15
|
const defaultAccountability = {
|
|
12
16
|
user: null,
|
|
13
17
|
role: null,
|
|
@@ -33,7 +37,18 @@ export const handler = async (req, _res, next) => {
|
|
|
33
37
|
req.accountability = customAccountability;
|
|
34
38
|
return next();
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
try {
|
|
41
|
+
req.accountability = await getAccountabilityForToken(req.token, defaultAccountability);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (isDirectusError(err, ErrorCode.InvalidCredentials) || isDirectusError(err, ErrorCode.InvalidToken)) {
|
|
45
|
+
if (req.cookies[env['SESSION_COOKIE_NAME']] === req.token) {
|
|
46
|
+
// clear the session token if ended up in an invalid state
|
|
47
|
+
res.clearCookie(env['SESSION_COOKIE_NAME'], SESSION_COOKIE_OPTIONS);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
37
52
|
return next();
|
|
38
53
|
};
|
|
39
54
|
export default asyncHandler(handler);
|
|
@@ -10,7 +10,7 @@ const logger = useLogger();
|
|
|
10
10
|
let checkRateLimit = (_req, _res, next) => next();
|
|
11
11
|
export let rateLimiterGlobal;
|
|
12
12
|
if (env['RATE_LIMITER_GLOBAL_ENABLED'] === true) {
|
|
13
|
-
validateEnv(['
|
|
13
|
+
validateEnv(['RATE_LIMITER_GLOBAL_DURATION', 'RATE_LIMITER_GLOBAL_POINTS']);
|
|
14
14
|
validateConfiguration();
|
|
15
15
|
rateLimiterGlobal = createRateLimiter('RATE_LIMITER_GLOBAL');
|
|
16
16
|
checkRateLimit = asyncHandler(async (_req, res, next) => {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import type { RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';
|
|
3
|
+
declare let checkRateLimit: RequestHandler;
|
|
4
|
+
export declare let rateLimiter: RateLimiterRedis | RateLimiterMemory;
|
|
5
|
+
export default checkRateLimit;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { HitRateLimitError } from '@directus/errors';
|
|
3
|
+
import { createRateLimiter } from '../rate-limiter.js';
|
|
4
|
+
import asyncHandler from '../utils/async-handler.js';
|
|
5
|
+
import { getIPFromReq } from '../utils/get-ip-from-req.js';
|
|
6
|
+
import { validateEnv } from '../utils/validate-env.js';
|
|
7
|
+
let checkRateLimit = (_req, _res, next) => next();
|
|
8
|
+
export let rateLimiter;
|
|
9
|
+
const env = useEnv();
|
|
10
|
+
if (env['RATE_LIMITER_REGISTRATION_ENABLED'] === true) {
|
|
11
|
+
validateEnv(['RATE_LIMITER_REGISTRATION_DURATION', 'RATE_LIMITER_REGISTRATION_POINTS']);
|
|
12
|
+
rateLimiter = createRateLimiter('RATE_LIMITER_REGISTRATION');
|
|
13
|
+
checkRateLimit = asyncHandler(async (req, res, next) => {
|
|
14
|
+
const ip = getIPFromReq(req);
|
|
15
|
+
if (ip) {
|
|
16
|
+
try {
|
|
17
|
+
await rateLimiter.consume(ip, 1);
|
|
18
|
+
}
|
|
19
|
+
catch (rateLimiterRes) {
|
|
20
|
+
if (rateLimiterRes instanceof Error)
|
|
21
|
+
throw rateLimiterRes;
|
|
22
|
+
res.set('Retry-After', String(Math.round(rateLimiterRes.msBeforeNext / 1000)));
|
|
23
|
+
throw new HitRateLimitError({
|
|
24
|
+
limit: +env['RATE_LIMITER_REGISTRATION_POINTS'],
|
|
25
|
+
reset: new Date(Date.now() + rateLimiterRes.msBeforeNext),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
next();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export default checkRateLimit;
|
|
@@ -21,6 +21,7 @@ export declare class AuthenticationService {
|
|
|
21
21
|
refresh(refreshToken: string, options?: Partial<{
|
|
22
22
|
session: boolean;
|
|
23
23
|
}>): Promise<LoginResult>;
|
|
24
|
+
private updateStatefulSession;
|
|
24
25
|
logout(refreshToken: string): Promise<void>;
|
|
25
26
|
verifyPassword(userID: string, password: string): Promise<void>;
|
|
26
27
|
}
|
|
@@ -10,6 +10,7 @@ import getDatabase from '../database/index.js';
|
|
|
10
10
|
import emitter from '../emitter.js';
|
|
11
11
|
import { RateLimiterRes, createRateLimiter } from '../rate-limiter.js';
|
|
12
12
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
13
|
+
import { getSecret } from '../utils/get-secret.js';
|
|
13
14
|
import { stall } from '../utils/stall.js';
|
|
14
15
|
import { ActivityService } from './activity.js';
|
|
15
16
|
import { SettingsService } from './settings.js';
|
|
@@ -159,7 +160,7 @@ export class AuthenticationService {
|
|
|
159
160
|
accountability: this.accountability,
|
|
160
161
|
});
|
|
161
162
|
const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'];
|
|
162
|
-
const accessToken = jwt.sign(customClaims,
|
|
163
|
+
const accessToken = jwt.sign(customClaims, getSecret(), {
|
|
163
164
|
expiresIn: TTL,
|
|
164
165
|
issuer: 'directus',
|
|
165
166
|
});
|
|
@@ -206,6 +207,7 @@ export class AuthenticationService {
|
|
|
206
207
|
const record = await this.knex
|
|
207
208
|
.select({
|
|
208
209
|
session_expires: 's.expires',
|
|
210
|
+
session_next_token: 's.next_token',
|
|
209
211
|
user_id: 'u.id',
|
|
210
212
|
user_first_name: 'u.first_name',
|
|
211
213
|
user_last_name: 'u.last_name',
|
|
@@ -273,8 +275,9 @@ export class AuthenticationService {
|
|
|
273
275
|
admin_access: record.role_admin_access,
|
|
274
276
|
});
|
|
275
277
|
}
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
+
let newRefreshToken = record.session_next_token ?? nanoid(64);
|
|
279
|
+
const sessionDuration = env[options?.session ? 'SESSION_COOKIE_TTL' : 'REFRESH_TOKEN_TTL'];
|
|
280
|
+
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(sessionDuration, 0));
|
|
278
281
|
const tokenPayload = {
|
|
279
282
|
id: record.user_id,
|
|
280
283
|
role: record.role_id,
|
|
@@ -282,8 +285,18 @@ export class AuthenticationService {
|
|
|
282
285
|
admin_access: record.role_admin_access,
|
|
283
286
|
};
|
|
284
287
|
if (options?.session) {
|
|
288
|
+
newRefreshToken = await this.updateStatefulSession(record, refreshToken, newRefreshToken, refreshTokenExpiration);
|
|
285
289
|
tokenPayload.session = newRefreshToken;
|
|
286
290
|
}
|
|
291
|
+
else {
|
|
292
|
+
// Original stateless token behavior
|
|
293
|
+
await this.knex('directus_sessions')
|
|
294
|
+
.update({
|
|
295
|
+
token: newRefreshToken,
|
|
296
|
+
expires: refreshTokenExpiration,
|
|
297
|
+
})
|
|
298
|
+
.where({ token: refreshToken });
|
|
299
|
+
}
|
|
287
300
|
if (record.share_id) {
|
|
288
301
|
tokenPayload.share = record.share_id;
|
|
289
302
|
tokenPayload.role = record.share_role;
|
|
@@ -306,19 +319,18 @@ export class AuthenticationService {
|
|
|
306
319
|
accountability: this.accountability,
|
|
307
320
|
});
|
|
308
321
|
const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'];
|
|
309
|
-
const accessToken = jwt.sign(customClaims,
|
|
322
|
+
const accessToken = jwt.sign(customClaims, getSecret(), {
|
|
310
323
|
expiresIn: TTL,
|
|
311
324
|
issuer: 'directus',
|
|
312
325
|
});
|
|
313
|
-
await this.knex('directus_sessions')
|
|
314
|
-
.update({
|
|
315
|
-
token: newRefreshToken,
|
|
316
|
-
expires: refreshTokenExpiration,
|
|
317
|
-
})
|
|
318
|
-
.where({ token: refreshToken });
|
|
319
326
|
if (record.user_id) {
|
|
320
327
|
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: record.user_id });
|
|
321
328
|
}
|
|
329
|
+
// Clear expired sessions for the current user
|
|
330
|
+
await this.knex('directus_sessions')
|
|
331
|
+
.delete()
|
|
332
|
+
.where('user', '=', record.user_id)
|
|
333
|
+
.andWhere('expires', '<', new Date());
|
|
322
334
|
return {
|
|
323
335
|
accessToken,
|
|
324
336
|
refreshToken: newRefreshToken,
|
|
@@ -326,6 +338,47 @@ export class AuthenticationService {
|
|
|
326
338
|
id: record.user_id,
|
|
327
339
|
};
|
|
328
340
|
}
|
|
341
|
+
async updateStatefulSession(sessionRecord, oldSessionToken, newSessionToken, sessionExpiration) {
|
|
342
|
+
if (sessionRecord['session_next_token']) {
|
|
343
|
+
// The current session token was already refreshed and has a reference
|
|
344
|
+
// to the new session, update the new session timeout for the new refresh
|
|
345
|
+
await this.knex('directus_sessions')
|
|
346
|
+
.update({
|
|
347
|
+
expires: sessionExpiration,
|
|
348
|
+
})
|
|
349
|
+
.where({ token: newSessionToken });
|
|
350
|
+
return newSessionToken;
|
|
351
|
+
}
|
|
352
|
+
// Keep the old session active for a short period of time
|
|
353
|
+
const GRACE_PERIOD = getMilliseconds(env['SESSION_REFRESH_GRACE_PERIOD'], 10_000);
|
|
354
|
+
// Update the existing session record to have a short safety timeout
|
|
355
|
+
// before expiring, and add the reference to the new session token
|
|
356
|
+
const updatedSession = await this.knex('directus_sessions')
|
|
357
|
+
.update({
|
|
358
|
+
next_token: newSessionToken,
|
|
359
|
+
expires: new Date(Date.now() + GRACE_PERIOD),
|
|
360
|
+
}, ['next_token'])
|
|
361
|
+
.where({ token: oldSessionToken, next_token: null });
|
|
362
|
+
if (updatedSession.length === 0) {
|
|
363
|
+
// Don't create a new session record, we already have a "next_token" reference
|
|
364
|
+
const { next_token } = await this.knex('directus_sessions')
|
|
365
|
+
.select('next_token')
|
|
366
|
+
.where({ token: oldSessionToken })
|
|
367
|
+
.first();
|
|
368
|
+
return next_token;
|
|
369
|
+
}
|
|
370
|
+
// Instead of updating the current session record with a new token,
|
|
371
|
+
// create a new copy with the new token
|
|
372
|
+
await this.knex('directus_sessions').insert({
|
|
373
|
+
token: newSessionToken,
|
|
374
|
+
user: sessionRecord['user_id'],
|
|
375
|
+
expires: sessionExpiration,
|
|
376
|
+
ip: this.accountability?.ip,
|
|
377
|
+
user_agent: this.accountability?.userAgent,
|
|
378
|
+
origin: this.accountability?.origin,
|
|
379
|
+
});
|
|
380
|
+
return newSessionToken;
|
|
381
|
+
}
|
|
329
382
|
async logout(refreshToken) {
|
|
330
383
|
const record = await this.knex
|
|
331
384
|
.select('u.id', 'u.first_name', 'u.last_name', 'u.email', 'u.password', 'u.status', 'u.role', 'u.provider', 'u.external_identifier', 'u.auth_data')
|
|
@@ -5,7 +5,7 @@ import { cloneDeep, flatten, isArray, isNil, merge, reduce, uniq, uniqWith } fro
|
|
|
5
5
|
import { GENERATE_SPECIAL } from '../constants.js';
|
|
6
6
|
import getDatabase from '../database/index.js';
|
|
7
7
|
import { getRelationInfo } from '../utils/get-relation-info.js';
|
|
8
|
-
import {
|
|
8
|
+
import { parseFilterKey } from '../utils/parse-filter-key.js';
|
|
9
9
|
import { ItemsService } from './items.js';
|
|
10
10
|
import { PayloadService } from './payload.js';
|
|
11
11
|
export class AuthorizationService {
|
|
@@ -105,8 +105,8 @@ export class AuthorizationService {
|
|
|
105
105
|
}
|
|
106
106
|
if (allowedFields.includes('*'))
|
|
107
107
|
continue;
|
|
108
|
-
const
|
|
109
|
-
if (allowedFields.includes(
|
|
108
|
+
const { fieldName } = parseFilterKey(childNode.name);
|
|
109
|
+
if (allowedFields.includes(fieldName) === false) {
|
|
110
110
|
throw new ForbiddenError();
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -282,7 +282,7 @@ export class AuthorizationService {
|
|
|
282
282
|
for (const field of requiredPermissions[collection]) {
|
|
283
283
|
if (field.startsWith('$FOLLOW'))
|
|
284
284
|
continue;
|
|
285
|
-
const fieldName =
|
|
285
|
+
const { fieldName } = parseFilterKey(field);
|
|
286
286
|
let originalFieldName = fieldName;
|
|
287
287
|
if (collection === rootCollection && aliasMap?.[fieldName]) {
|
|
288
288
|
originalFieldName = aliasMap[fieldName];
|
package/dist/services/fields.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KNEX_TYPES, REGEX_BETWEEN_PARENS } from '@directus/constants';
|
|
1
|
+
import { KNEX_TYPES, REGEX_BETWEEN_PARENS, DEFAULT_NUMERIC_PRECISION, DEFAULT_NUMERIC_SCALE, } from '@directus/constants';
|
|
2
2
|
import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
|
|
3
3
|
import { createInspector } from '@directus/schema';
|
|
4
4
|
import { addFieldFlag, toArray } from '@directus/utils';
|
|
@@ -586,7 +586,7 @@ export class FieldsService {
|
|
|
586
586
|
}
|
|
587
587
|
else if (['float', 'decimal'].includes(field.type)) {
|
|
588
588
|
const type = field.type;
|
|
589
|
-
column = table[type](field.field, field.schema?.numeric_precision ??
|
|
589
|
+
column = table[type](field.field, field.schema?.numeric_precision ?? DEFAULT_NUMERIC_PRECISION, field.schema?.numeric_scale ?? DEFAULT_NUMERIC_SCALE);
|
|
590
590
|
}
|
|
591
591
|
else if (field.type === 'csv') {
|
|
592
592
|
column = table.text(field.field);
|
|
@@ -10,8 +10,11 @@ import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from '
|
|
|
10
10
|
import { clearSystemCache, getCache } from '../../cache.js';
|
|
11
11
|
import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS, } from '../../constants.js';
|
|
12
12
|
import getDatabase from '../../database/index.js';
|
|
13
|
+
import { rateLimiter } from '../../middleware/rate-limiter-registration.js';
|
|
13
14
|
import { generateHash } from '../../utils/generate-hash.js';
|
|
14
15
|
import { getGraphQLType } from '../../utils/get-graphql-type.js';
|
|
16
|
+
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
17
|
+
import { getSecret } from '../../utils/get-secret.js';
|
|
15
18
|
import { getService } from '../../utils/get-service.js';
|
|
16
19
|
import isDirectusJWT from '../../utils/is-directus-jwt.js';
|
|
17
20
|
import { verifyAccessJWT } from '../../utils/jwt.js';
|
|
@@ -1637,6 +1640,8 @@ export class GraphQLService {
|
|
|
1637
1640
|
public_background: { type: GraphQLString },
|
|
1638
1641
|
public_note: { type: GraphQLString },
|
|
1639
1642
|
custom_css: { type: GraphQLString },
|
|
1643
|
+
public_registration: { type: GraphQLBoolean },
|
|
1644
|
+
public_registration_verify_email: { type: GraphQLBoolean },
|
|
1640
1645
|
},
|
|
1641
1646
|
}),
|
|
1642
1647
|
},
|
|
@@ -1872,7 +1877,7 @@ export class GraphQLService {
|
|
|
1872
1877
|
else if (mode === 'session') {
|
|
1873
1878
|
const token = req?.cookies[env['SESSION_COOKIE_NAME']];
|
|
1874
1879
|
if (isDirectusJWT(token)) {
|
|
1875
|
-
const payload = verifyAccessJWT(token,
|
|
1880
|
+
const payload = verifyAccessJWT(token, getSecret());
|
|
1876
1881
|
currentRefreshToken = payload.session;
|
|
1877
1882
|
}
|
|
1878
1883
|
}
|
|
@@ -1930,7 +1935,7 @@ export class GraphQLService {
|
|
|
1930
1935
|
else if (mode === 'session') {
|
|
1931
1936
|
const token = req?.cookies[env['SESSION_COOKIE_NAME']];
|
|
1932
1937
|
if (isDirectusJWT(token)) {
|
|
1933
|
-
const payload = verifyAccessJWT(token,
|
|
1938
|
+
const payload = verifyAccessJWT(token, getSecret());
|
|
1934
1939
|
currentRefreshToken = payload.session;
|
|
1935
1940
|
}
|
|
1936
1941
|
}
|
|
@@ -2152,6 +2157,40 @@ export class GraphQLService {
|
|
|
2152
2157
|
return true;
|
|
2153
2158
|
},
|
|
2154
2159
|
},
|
|
2160
|
+
users_register: {
|
|
2161
|
+
type: GraphQLBoolean,
|
|
2162
|
+
args: {
|
|
2163
|
+
email: new GraphQLNonNull(GraphQLString),
|
|
2164
|
+
password: new GraphQLNonNull(GraphQLString),
|
|
2165
|
+
first_name: GraphQLString,
|
|
2166
|
+
last_name: GraphQLString,
|
|
2167
|
+
},
|
|
2168
|
+
resolve: async (_, args, { req }) => {
|
|
2169
|
+
const service = new UsersService({ accountability: null, schema: this.schema });
|
|
2170
|
+
const ip = req ? getIPFromReq(req) : null;
|
|
2171
|
+
if (ip) {
|
|
2172
|
+
await rateLimiter.consume(ip);
|
|
2173
|
+
}
|
|
2174
|
+
await service.registerUser({
|
|
2175
|
+
email: args.email,
|
|
2176
|
+
password: args.password,
|
|
2177
|
+
first_name: args.first_name,
|
|
2178
|
+
last_name: args.last_name,
|
|
2179
|
+
});
|
|
2180
|
+
return true;
|
|
2181
|
+
},
|
|
2182
|
+
},
|
|
2183
|
+
users_register_verify: {
|
|
2184
|
+
type: GraphQLBoolean,
|
|
2185
|
+
args: {
|
|
2186
|
+
token: new GraphQLNonNull(GraphQLString),
|
|
2187
|
+
},
|
|
2188
|
+
resolve: async (_, args) => {
|
|
2189
|
+
const service = new UsersService({ accountability: null, schema: this.schema });
|
|
2190
|
+
await service.verifyRegistration(args.token);
|
|
2191
|
+
return true;
|
|
2192
|
+
},
|
|
2193
|
+
},
|
|
2155
2194
|
});
|
|
2156
2195
|
if ('directus_collections' in schema.read.collections) {
|
|
2157
2196
|
Collection.addFields({
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{% layout 'base' %} {% block content %}
|
|
2
|
+
|
|
3
|
+
<h1>Verify your email address</h1>
|
|
4
|
+
|
|
5
|
+
<p style='padding-bottom: 30px'>
|
|
6
|
+
Thanks for registering at <i>{{ projectName }}</i>.
|
|
7
|
+
To complete your registration you need to verify your email address by opening the following verification-link.
|
|
8
|
+
Please feel free to ignore this email if you have not personally initiated the registration.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<a
|
|
12
|
+
class='button'
|
|
13
|
+
rel='noopener'
|
|
14
|
+
target='_blank'
|
|
15
|
+
href='{{url}}'
|
|
16
|
+
style='
|
|
17
|
+
font-size: 16px;
|
|
18
|
+
font-weight: 600;
|
|
19
|
+
color: #ffffff;
|
|
20
|
+
text-decoration: none;
|
|
21
|
+
display: inline-block;
|
|
22
|
+
padding: 11px 24px;
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
background: #171717;
|
|
25
|
+
border: 1px solid #ffffff;
|
|
26
|
+
'
|
|
27
|
+
>
|
|
28
|
+
<!--[if mso]>
|
|
29
|
+
<i style="letter-spacing: 25px; mso-font-width: -100%; mso-text-raise: 30pt"
|
|
30
|
+
> </i
|
|
31
|
+
>
|
|
32
|
+
<![endif]-->
|
|
33
|
+
<span style='mso-text-raise: 15pt'>Verify email</span>
|
|
34
|
+
<!--[if mso]> <i style="letter-spacing: 25px; mso-font-width: -100%"> </i> <![endif]-->
|
|
35
|
+
</a>
|
|
36
|
+
|
|
37
|
+
{% endblock %}
|
package/dist/services/meta.js
CHANGED
|
@@ -63,7 +63,7 @@ export class MetaService {
|
|
|
63
63
|
({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}));
|
|
64
64
|
}
|
|
65
65
|
if (query.search) {
|
|
66
|
-
applySearch(this.schema, dbQuery, query.search, collection);
|
|
66
|
+
applySearch(this.knex, this.schema, dbQuery, query.search, collection);
|
|
67
67
|
}
|
|
68
68
|
if (hasJoins) {
|
|
69
69
|
const primaryKeyName = this.schema.collections[collection].primary;
|
|
@@ -27,6 +27,8 @@ export declare class PayloadService {
|
|
|
27
27
|
transformers: Transformers;
|
|
28
28
|
processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
|
29
29
|
processValues(action: Action, payload: Partial<Item>): Promise<Partial<Item>>;
|
|
30
|
+
processValues(action: Action, payloads: Partial<Item>[], aliasMap: Record<string, string>): Promise<Partial<Item>[]>;
|
|
31
|
+
processValues(action: Action, payload: Partial<Item>, aliasMap: Record<string, string>): Promise<Partial<Item>>;
|
|
30
32
|
processAggregates(payload: Partial<Item>[]): void;
|
|
31
33
|
processField(field: SchemaOverview['collections'][string]['fields'][string], payload: Partial<Item>, action: Action, accountability: Accountability | null): Promise<any>;
|
|
32
34
|
/**
|
package/dist/services/payload.js
CHANGED
|
@@ -121,19 +121,31 @@ export class PayloadService {
|
|
|
121
121
|
return value;
|
|
122
122
|
},
|
|
123
123
|
};
|
|
124
|
-
async processValues(action, payload) {
|
|
124
|
+
async processValues(action, payload, aliasMap = {}) {
|
|
125
125
|
const processedPayload = toArray(payload);
|
|
126
126
|
if (processedPayload.length === 0)
|
|
127
127
|
return [];
|
|
128
128
|
const fieldsInPayload = Object.keys(processedPayload[0]);
|
|
129
|
-
|
|
129
|
+
const fieldEntries = Object.entries(this.schema.collections[this.collection].fields);
|
|
130
|
+
const aliasEntries = Object.entries(aliasMap);
|
|
131
|
+
let specialFields = [];
|
|
132
|
+
for (const [name, field] of fieldEntries) {
|
|
133
|
+
if (field.special && field.special.length > 0) {
|
|
134
|
+
specialFields.push([name, field]);
|
|
135
|
+
for (const [aliasName, fieldName] of aliasEntries) {
|
|
136
|
+
if (fieldName === name) {
|
|
137
|
+
specialFields.push([aliasName, { ...field, field: aliasName }]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
130
142
|
if (action === 'read') {
|
|
131
|
-
|
|
143
|
+
specialFields = specialFields.filter(([name]) => {
|
|
132
144
|
return fieldsInPayload.includes(name);
|
|
133
145
|
});
|
|
134
146
|
}
|
|
135
147
|
await Promise.all(processedPayload.map(async (record) => {
|
|
136
|
-
await Promise.all(
|
|
148
|
+
await Promise.all(specialFields.map(async ([name, field]) => {
|
|
137
149
|
const newValue = await this.processField(field, record, action, this.accountability);
|
|
138
150
|
if (newValue !== undefined)
|
|
139
151
|
record[name] = newValue;
|
package/dist/services/server.js
CHANGED
|
@@ -46,6 +46,8 @@ export class ServerService {
|
|
|
46
46
|
'public_favicon',
|
|
47
47
|
'public_note',
|
|
48
48
|
'custom_css',
|
|
49
|
+
'public_registration',
|
|
50
|
+
'public_registration_verify_email',
|
|
49
51
|
],
|
|
50
52
|
});
|
|
51
53
|
info['project'] = projectInfo;
|
|
@@ -106,7 +108,7 @@ export class ServerService {
|
|
|
106
108
|
const data = {
|
|
107
109
|
status: 'ok',
|
|
108
110
|
releaseId: version,
|
|
109
|
-
serviceId: env['
|
|
111
|
+
serviceId: env['PUBLIC_URL'],
|
|
110
112
|
checks: merge(...(await Promise.all([
|
|
111
113
|
testDatabase(),
|
|
112
114
|
testCache(),
|
package/dist/services/shares.js
CHANGED
|
@@ -4,6 +4,7 @@ import argon2 from 'argon2';
|
|
|
4
4
|
import jwt from 'jsonwebtoken';
|
|
5
5
|
import { useLogger } from '../logger.js';
|
|
6
6
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
7
|
+
import { getSecret } from '../utils/get-secret.js';
|
|
7
8
|
import { md } from '../utils/md.js';
|
|
8
9
|
import { Url } from '../utils/url.js';
|
|
9
10
|
import { userName } from '../utils/user-name.js';
|
|
@@ -78,7 +79,7 @@ export class SharesService extends ItemsService {
|
|
|
78
79
|
tokenPayload.session = refreshToken;
|
|
79
80
|
}
|
|
80
81
|
const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'];
|
|
81
|
-
const accessToken = jwt.sign(tokenPayload,
|
|
82
|
+
const accessToken = jwt.sign(tokenPayload, getSecret(), {
|
|
82
83
|
expiresIn: TTL,
|
|
83
84
|
issuer: 'directus',
|
|
84
85
|
});
|
package/dist/services/users.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Item, PrimaryKey, Query } from '@directus/types';
|
|
1
|
+
import type { Item, PrimaryKey, Query, RegisterUserInput } from '@directus/types';
|
|
2
2
|
import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
|
|
3
3
|
import { ItemsService } from './items.js';
|
|
4
4
|
export declare class UsersService extends ItemsService {
|
|
@@ -62,6 +62,8 @@ export declare class UsersService extends ItemsService {
|
|
|
62
62
|
deleteByQuery(query: Query, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
63
63
|
inviteUser(email: string | string[], role: string, url: string | null, subject?: string | null): Promise<void>;
|
|
64
64
|
acceptInvite(token: string, password: string): Promise<void>;
|
|
65
|
+
registerUser(input: RegisterUserInput): Promise<void>;
|
|
66
|
+
verifyRegistration(token: string): Promise<string>;
|
|
65
67
|
requestPasswordReset(email: string, url: string | null, subject?: string | null): Promise<void>;
|
|
66
68
|
resetPassword(token: string, password: string): Promise<void>;
|
|
67
69
|
}
|