@directus/api 14.0.1 → 14.1.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/__utils__/mock-env.d.ts +18 -0
- package/dist/__utils__/mock-env.js +41 -0
- package/dist/auth/drivers/oauth2.js +12 -5
- package/dist/auth/drivers/openid.js +11 -5
- package/dist/cli/load-extensions.js +1 -2
- package/dist/controllers/assets.js +10 -10
- package/dist/controllers/files.js +1 -5
- package/dist/database/index.js +2 -1
- package/dist/database/migrations/run.js +2 -2
- package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +5 -0
- package/dist/database/system-data/collections/collections.yaml +1 -1
- package/dist/database/system-data/fields/settings.yaml +12 -13
- package/dist/database/system-data/fields/users.yaml +10 -10
- package/dist/env.d.ts +2 -4
- package/dist/env.js +12 -9
- package/dist/extensions/lib/get-extensions-path.d.ts +1 -0
- package/dist/extensions/lib/get-extensions-path.js +8 -0
- package/dist/extensions/lib/get-extensions.js +3 -2
- package/dist/extensions/lib/sync-extensions.d.ts +1 -0
- package/dist/extensions/lib/sync-extensions.js +59 -0
- package/dist/extensions/lib/sync-status.d.ts +10 -0
- package/dist/extensions/lib/sync-status.js +27 -0
- package/dist/extensions/manager.js +16 -6
- package/dist/logger.d.ts +2 -1
- package/dist/logger.js +13 -2
- package/dist/messenger.js +1 -3
- package/dist/middleware/validate-batch.js +2 -0
- package/dist/request/validate-ip.js +1 -2
- package/dist/services/extensions.js +1 -1
- package/dist/services/files.d.ts +2 -2
- package/dist/services/files.js +93 -26
- package/dist/services/mail/index.js +5 -4
- package/dist/services/mail/templates/base.liquid +383 -138
- package/dist/services/mail/templates/password-reset.liquid +35 -17
- package/dist/services/mail/templates/user-invitation.liquid +32 -13
- package/dist/services/server.js +4 -0
- package/dist/storage/register-drivers.js +1 -2
- package/dist/storage/register-locations.js +1 -2
- package/dist/utils/delete-from-require-cache.js +8 -1
- package/dist/utils/get-auth-providers.d.ts +1 -1
- package/dist/utils/get-config-from-env.js +1 -2
- package/dist/utils/merge-permissions.js +11 -19
- package/dist/utils/sanitize-query.js +2 -2
- package/dist/utils/should-clear-cache.js +1 -2
- package/dist/utils/should-skip-cache.js +3 -4
- package/dist/utils/validate-env.js +1 -2
- package/dist/utils/validate-storage.js +12 -9
- package/package.json +16 -15
- package/dist/__mocks__/cache.d.mts +0 -5
- package/dist/__mocks__/cache.mjs +0 -7
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function setEnv(newEnv: Record<string, string>): void;
|
|
2
|
+
/** Static env mock - to be used inside `vi.mock` */
|
|
3
|
+
export declare function mockEnv(options?: {
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
withDefaults?: boolean;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
readonly default: Record<string, any>;
|
|
8
|
+
}>;
|
|
9
|
+
/** Dynamic env mock */
|
|
10
|
+
export declare function doMockEnv(options?: {
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
withDefaults?: boolean;
|
|
13
|
+
}): {
|
|
14
|
+
env: {
|
|
15
|
+
[x: string]: string;
|
|
16
|
+
};
|
|
17
|
+
setEnv: (newEnv: Record<string, string>) => void;
|
|
18
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { afterEach, vi } from 'vitest';
|
|
2
|
+
let env = {};
|
|
3
|
+
export function setEnv(newEnv) {
|
|
4
|
+
env = { ...env, ...newEnv };
|
|
5
|
+
}
|
|
6
|
+
/** Static env mock - to be used inside `vi.mock` */
|
|
7
|
+
export async function mockEnv(options) {
|
|
8
|
+
const initialEnv = options?.env ?? {};
|
|
9
|
+
const withDefaults = options && 'withDefaults' in options ? options.withDefaults : true;
|
|
10
|
+
env = { ...initialEnv };
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
env = { ...initialEnv };
|
|
13
|
+
});
|
|
14
|
+
const { defaults, processValues } = await vi.importActual('../env.js');
|
|
15
|
+
return {
|
|
16
|
+
get default() {
|
|
17
|
+
return processValues({ ...(withDefaults && defaults), ...env });
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Dynamic env mock */
|
|
22
|
+
export function doMockEnv(options) {
|
|
23
|
+
const initialEnv = options?.env ?? {};
|
|
24
|
+
const withDefaults = options && 'withDefaults' in options ? options.withDefaults : true;
|
|
25
|
+
let env = { ...initialEnv };
|
|
26
|
+
vi.doMock('../env.js', async () => {
|
|
27
|
+
const { defaults, processValues } = await vi.importActual('../env.js');
|
|
28
|
+
return {
|
|
29
|
+
get default() {
|
|
30
|
+
return processValues({ ...(withDefaults && defaults), ...env });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
env = { ...initialEnv };
|
|
36
|
+
});
|
|
37
|
+
return { env, setEnv };
|
|
38
|
+
function setEnv(newEnv) {
|
|
39
|
+
env = { ...env, ...newEnv };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -54,8 +54,9 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
54
54
|
return generators.codeVerifier();
|
|
55
55
|
}
|
|
56
56
|
generateAuthUrl(codeVerifier, prompt = false) {
|
|
57
|
+
const { plainCodeChallenge } = this.config;
|
|
57
58
|
try {
|
|
58
|
-
const codeChallenge = generators.codeChallenge(codeVerifier);
|
|
59
|
+
const codeChallenge = plainCodeChallenge ? codeVerifier : generators.codeChallenge(codeVerifier);
|
|
59
60
|
const paramsConfig = typeof this.config['params'] === 'object' ? this.config['params'] : {};
|
|
60
61
|
return this.client.authorizationUrl({
|
|
61
62
|
scope: this.config['scope'] ?? 'email',
|
|
@@ -63,7 +64,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
63
64
|
prompt: prompt ? 'consent' : undefined,
|
|
64
65
|
...paramsConfig,
|
|
65
66
|
code_challenge: codeChallenge,
|
|
66
|
-
code_challenge_method: 'S256',
|
|
67
|
+
code_challenge_method: plainCodeChallenge ? 'plain' : 'S256',
|
|
67
68
|
// Some providers require state even with PKCE
|
|
68
69
|
state: codeChallenge,
|
|
69
70
|
});
|
|
@@ -85,10 +86,14 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
85
86
|
logger.warn('[OAuth2] No code, codeVerifier or state in payload');
|
|
86
87
|
throw new InvalidCredentialsError();
|
|
87
88
|
}
|
|
89
|
+
const { plainCodeChallenge } = this.config;
|
|
88
90
|
let tokenSet;
|
|
89
91
|
let userInfo;
|
|
90
92
|
try {
|
|
91
|
-
|
|
93
|
+
const codeChallenge = plainCodeChallenge
|
|
94
|
+
? payload['codeVerifier']
|
|
95
|
+
: generators.codeChallenge(payload['codeVerifier']);
|
|
96
|
+
tokenSet = await this.client.oauthCallback(this.redirectUrl, { code: payload['code'], state: payload['state'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge });
|
|
92
97
|
userInfo = await this.client.userinfo(tokenSet.access_token);
|
|
93
98
|
}
|
|
94
99
|
catch (e) {
|
|
@@ -117,13 +122,15 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
117
122
|
if (userId) {
|
|
118
123
|
// Run hook so the end user has the chance to augment the
|
|
119
124
|
// user that is about to be updated
|
|
120
|
-
const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data
|
|
125
|
+
const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
|
|
121
126
|
identifier,
|
|
122
127
|
provider: this.config['provider'],
|
|
123
128
|
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
124
129
|
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
125
130
|
// Update user to update refresh_token and other properties that might have changed
|
|
126
|
-
|
|
131
|
+
if (Object.values(updatedUserPayload).some((value) => value !== undefined)) {
|
|
132
|
+
await this.usersService.updateOne(userId, updatedUserPayload);
|
|
133
|
+
}
|
|
127
134
|
return userId;
|
|
128
135
|
}
|
|
129
136
|
// Is public registration allowed?
|
|
@@ -64,9 +64,10 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
64
64
|
return generators.codeVerifier();
|
|
65
65
|
}
|
|
66
66
|
async generateAuthUrl(codeVerifier, prompt = false) {
|
|
67
|
+
const { plainCodeChallenge } = this.config;
|
|
67
68
|
try {
|
|
68
69
|
const client = await this.client;
|
|
69
|
-
const codeChallenge = generators.codeChallenge(codeVerifier);
|
|
70
|
+
const codeChallenge = plainCodeChallenge ? codeVerifier : generators.codeChallenge(codeVerifier);
|
|
70
71
|
const paramsConfig = typeof this.config['params'] === 'object' ? this.config['params'] : {};
|
|
71
72
|
return client.authorizationUrl({
|
|
72
73
|
scope: this.config['scope'] ?? 'openid profile email',
|
|
@@ -74,7 +75,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
74
75
|
prompt: prompt ? 'consent' : undefined,
|
|
75
76
|
...paramsConfig,
|
|
76
77
|
code_challenge: codeChallenge,
|
|
77
|
-
code_challenge_method: 'S256',
|
|
78
|
+
code_challenge_method: plainCodeChallenge ? 'plain' : 'S256',
|
|
78
79
|
// Some providers require state even with PKCE
|
|
79
80
|
state: codeChallenge,
|
|
80
81
|
nonce: codeChallenge,
|
|
@@ -97,11 +98,14 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
97
98
|
logger.warn('[OpenID] No code, codeVerifier or state in payload');
|
|
98
99
|
throw new InvalidCredentialsError();
|
|
99
100
|
}
|
|
101
|
+
const { plainCodeChallenge } = this.config;
|
|
100
102
|
let tokenSet;
|
|
101
103
|
let userInfo;
|
|
102
104
|
try {
|
|
103
105
|
const client = await this.client;
|
|
104
|
-
const codeChallenge =
|
|
106
|
+
const codeChallenge = plainCodeChallenge
|
|
107
|
+
? payload['codeVerifier']
|
|
108
|
+
: generators.codeChallenge(payload['codeVerifier']);
|
|
105
109
|
tokenSet = await client.callback(this.redirectUrl, { code: payload['code'], state: payload['state'], iss: payload['iss'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge, nonce: codeChallenge });
|
|
106
110
|
userInfo = tokenSet.claims();
|
|
107
111
|
if (client.issuer.metadata['userinfo_endpoint']) {
|
|
@@ -137,13 +141,15 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
137
141
|
if (userId) {
|
|
138
142
|
// Run hook so the end user has the chance to augment the
|
|
139
143
|
// user that is about to be updated
|
|
140
|
-
const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data
|
|
144
|
+
const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
|
|
141
145
|
identifier,
|
|
142
146
|
provider: this.config['provider'],
|
|
143
147
|
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
144
148
|
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
145
149
|
// Update user to update refresh_token and other properties that might have changed
|
|
146
|
-
|
|
150
|
+
if (Object.values(updatedUserPayload).some((value) => value !== undefined)) {
|
|
151
|
+
await this.usersService.updateOne(userId, updatedUserPayload);
|
|
152
|
+
}
|
|
147
153
|
return userId;
|
|
148
154
|
}
|
|
149
155
|
const isEmailVerified = !requireVerifiedEmail || userInfo['email_verified'];
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { isInstalled, validateMigrations } from '../database/index.js';
|
|
2
|
-
import
|
|
2
|
+
import env from '../env.js';
|
|
3
3
|
import { getExtensionManager } from '../extensions/index.js';
|
|
4
4
|
import logger from '../logger.js';
|
|
5
5
|
export const loadExtensions = async () => {
|
|
6
|
-
const env = getEnv();
|
|
7
6
|
if (!('DB_CLIENT' in env))
|
|
8
7
|
return;
|
|
9
8
|
const installed = await isInstalled();
|
|
@@ -32,11 +32,6 @@ asyncHandler(async (req, res, next) => {
|
|
|
32
32
|
}
|
|
33
33
|
const assetSettings = savedAssetSettings || defaults;
|
|
34
34
|
const transformation = pick(req.query, ASSET_TRANSFORM_QUERY_KEYS);
|
|
35
|
-
if ('key' in transformation && Object.keys(transformation).length > 1) {
|
|
36
|
-
throw new InvalidQueryError({
|
|
37
|
-
reason: `You can't combine the "key" query parameter with any other transformation`,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
35
|
if ('transforms' in transformation) {
|
|
41
36
|
let transforms;
|
|
42
37
|
// Try parse the JSON array
|
|
@@ -90,13 +85,17 @@ asyncHandler(async (req, res, next) => {
|
|
|
90
85
|
return next();
|
|
91
86
|
}
|
|
92
87
|
else if (assetSettings.storage_asset_transform === 'presets') {
|
|
93
|
-
if (allKeys.includes(transformation['key']))
|
|
88
|
+
if (allKeys.includes(transformation['key']) && Object.keys(transformation).length === 1) {
|
|
94
89
|
return next();
|
|
90
|
+
}
|
|
95
91
|
throw new InvalidQueryError({ reason: `Only configured presets can be used in asset generation` });
|
|
96
92
|
}
|
|
97
93
|
else {
|
|
98
|
-
if (transformation['key'] &&
|
|
94
|
+
if (transformation['key'] &&
|
|
95
|
+
systemKeys.includes(transformation['key']) &&
|
|
96
|
+
Object.keys(transformation).length === 1) {
|
|
99
97
|
return next();
|
|
98
|
+
}
|
|
100
99
|
throw new InvalidQueryError({ reason: `Dynamic asset generation has been disabled for this project` });
|
|
101
100
|
}
|
|
102
101
|
}), asyncHandler(async (req, res, next) => {
|
|
@@ -116,9 +115,10 @@ asyncHandler(async (req, res) => {
|
|
|
116
115
|
schema: req.schema,
|
|
117
116
|
});
|
|
118
117
|
const vary = ['Origin', 'Cache-Control'];
|
|
119
|
-
const transformationParams =
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
const transformationParams = {
|
|
119
|
+
...res.locals['shortcuts'].find((transformation) => transformation['key'] === res.locals['transformation']?.key),
|
|
120
|
+
...res.locals['transformation'],
|
|
121
|
+
};
|
|
122
122
|
let acceptFormat;
|
|
123
123
|
if (transformationParams.format === 'auto') {
|
|
124
124
|
if (req.headers.accept?.includes('image/avif')) {
|
|
@@ -8,7 +8,7 @@ import Joi from 'joi';
|
|
|
8
8
|
import { minimatch } from 'minimatch';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import env from '../env.js';
|
|
11
|
-
import {
|
|
11
|
+
import { ErrorCode, InvalidPayloadError } from '@directus/errors';
|
|
12
12
|
import { respond } from '../middleware/respond.js';
|
|
13
13
|
import useCollection from '../middleware/use-collection.js';
|
|
14
14
|
import { validateBatch } from '../middleware/validate-batch.js';
|
|
@@ -85,10 +85,6 @@ export const multipartHandler = (req, res, next) => {
|
|
|
85
85
|
};
|
|
86
86
|
// Clear the payload for the next to-be-uploaded file
|
|
87
87
|
payload = {};
|
|
88
|
-
fileStream.on('limit', () => {
|
|
89
|
-
const error = new ContentTooLargeError();
|
|
90
|
-
next(error);
|
|
91
|
-
});
|
|
92
88
|
try {
|
|
93
89
|
const primaryKey = await service.uploadOne(fileStream, payloadWithRequiredFields, existingPrimaryKey);
|
|
94
90
|
savedFiles.push(primaryKey);
|
package/dist/database/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'path';
|
|
|
8
8
|
import { performance } from 'perf_hooks';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
10
|
import env from '../env.js';
|
|
11
|
+
import { getExtensionsPath } from '../extensions/lib/get-extensions-path.js';
|
|
11
12
|
import logger from '../logger.js';
|
|
12
13
|
import { getConfigFromEnv } from '../utils/get-config-from-env.js';
|
|
13
14
|
import { validateEnv } from '../utils/validate-env.js';
|
|
@@ -209,7 +210,7 @@ export async function validateMigrations() {
|
|
|
209
210
|
const database = getDatabase();
|
|
210
211
|
try {
|
|
211
212
|
let migrationFiles = await fse.readdir(path.join(__dirname, 'migrations'));
|
|
212
|
-
const customMigrationsPath = path.resolve(
|
|
213
|
+
const customMigrationsPath = path.resolve(getExtensionsPath(), 'migrations');
|
|
213
214
|
let customMigrationFiles = ((await fse.pathExists(customMigrationsPath)) && (await fse.readdir(customMigrationsPath))) || [];
|
|
214
215
|
migrationFiles = migrationFiles.filter((file) => file.startsWith('run') === false && file.endsWith('.d.ts') === false);
|
|
215
216
|
customMigrationFiles = customMigrationFiles.filter((file) => file.endsWith('.js'));
|
|
@@ -5,13 +5,13 @@ import { dirname } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { flushCaches } from '../../cache.js';
|
|
8
|
-
import
|
|
8
|
+
import { getExtensionsPath } from '../../extensions/lib/get-extensions-path.js';
|
|
9
9
|
import logger from '../../logger.js';
|
|
10
10
|
import getModuleDefault from '../../utils/get-module-default.js';
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
export default async function run(database, direction, log = true) {
|
|
13
13
|
let migrationFiles = await fse.readdir(__dirname);
|
|
14
|
-
const customMigrationsPath = path.resolve(
|
|
14
|
+
const customMigrationsPath = path.resolve(getExtensionsPath(), 'migrations');
|
|
15
15
|
let customMigrationFiles = ((await fse.pathExists(customMigrationsPath)) && (await fse.readdir(customMigrationsPath))) || [];
|
|
16
16
|
migrationFiles = migrationFiles.filter((file) => /^[0-9]+[A-Z]-[^.]+\.(?:js|ts)$/.test(file));
|
|
17
17
|
customMigrationFiles = customMigrationFiles.filter((file) => file.includes('-') && /\.(c|m)?js$/.test(file));
|
|
@@ -86,8 +86,20 @@ fields:
|
|
|
86
86
|
width: half
|
|
87
87
|
group: theming_group
|
|
88
88
|
|
|
89
|
+
- field: theming_divider
|
|
90
|
+
interface: presentation-divider
|
|
91
|
+
options:
|
|
92
|
+
icon: palette
|
|
93
|
+
title: $t:fields.directus_settings.theming
|
|
94
|
+
special:
|
|
95
|
+
- alias
|
|
96
|
+
- no-data
|
|
97
|
+
width: full
|
|
98
|
+
group: theming_group
|
|
99
|
+
|
|
89
100
|
- field: default_appearance
|
|
90
101
|
interface: select-dropdown
|
|
102
|
+
width: half
|
|
91
103
|
options:
|
|
92
104
|
choices:
|
|
93
105
|
- value: auto
|
|
@@ -98,24 +110,12 @@ fields:
|
|
|
98
110
|
text: $t:appearance_dark
|
|
99
111
|
group: theming_group
|
|
100
112
|
|
|
101
|
-
- field: theming_divider
|
|
102
|
-
interface: presentation-divider
|
|
103
|
-
options:
|
|
104
|
-
icon: palette
|
|
105
|
-
title: $t:fields.directus_settings.theming
|
|
106
|
-
special:
|
|
107
|
-
- alias
|
|
108
|
-
- no-data
|
|
109
|
-
width: full
|
|
110
|
-
group: theming_group
|
|
111
|
-
|
|
112
113
|
- field: default_theme_light
|
|
113
114
|
width: full
|
|
114
115
|
interface: system-theme
|
|
115
116
|
options:
|
|
116
117
|
appearance: light
|
|
117
118
|
group: theming_group
|
|
118
|
-
hidden: true
|
|
119
119
|
|
|
120
120
|
- field: theme_light_overrides
|
|
121
121
|
width: full
|
|
@@ -130,7 +130,6 @@ fields:
|
|
|
130
130
|
options:
|
|
131
131
|
appearance: dark
|
|
132
132
|
group: theming_group
|
|
133
|
-
hidden: true
|
|
134
133
|
|
|
135
134
|
- field: theme_dark_overrides
|
|
136
135
|
width: full
|
|
@@ -105,7 +105,7 @@ fields:
|
|
|
105
105
|
options:
|
|
106
106
|
choices:
|
|
107
107
|
- value: null
|
|
108
|
-
text: $t:
|
|
108
|
+
text: $t:default_sync_with_project
|
|
109
109
|
- value: auto
|
|
110
110
|
text: $t:appearance_auto
|
|
111
111
|
- value: light
|
|
@@ -115,18 +115,11 @@ fields:
|
|
|
115
115
|
width: half
|
|
116
116
|
|
|
117
117
|
- field: theme_light
|
|
118
|
-
width:
|
|
118
|
+
width: full
|
|
119
119
|
interface: system-theme
|
|
120
120
|
options:
|
|
121
121
|
appearance: light
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
- field: theme_dark
|
|
125
|
-
width: half
|
|
126
|
-
interface: system-theme
|
|
127
|
-
options:
|
|
128
|
-
appearance: dark
|
|
129
|
-
hidden: true
|
|
122
|
+
includeNull: true
|
|
130
123
|
|
|
131
124
|
- field: theme_light_overrides
|
|
132
125
|
width: full
|
|
@@ -134,6 +127,13 @@ fields:
|
|
|
134
127
|
special:
|
|
135
128
|
- cast-json
|
|
136
129
|
|
|
130
|
+
- field: theme_dark
|
|
131
|
+
width: full
|
|
132
|
+
interface: system-theme
|
|
133
|
+
options:
|
|
134
|
+
appearance: dark
|
|
135
|
+
includeNull: true
|
|
136
|
+
|
|
137
137
|
- field: theme_dark_overrides
|
|
138
138
|
width: full
|
|
139
139
|
interface: system-theme-overrides
|
package/dist/env.d.ts
CHANGED
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
* @NOTE
|
|
3
3
|
* For all possible keys, see: https://docs.directus.io/self-hosted/config-options/
|
|
4
4
|
*/
|
|
5
|
+
export declare const defaults: Record<string, any>;
|
|
5
6
|
declare let env: Record<string, any>;
|
|
6
7
|
export default env;
|
|
7
|
-
/**
|
|
8
|
-
* Small wrapper function that makes it easier to write unit tests against changing environments
|
|
9
|
-
*/
|
|
10
|
-
export declare const getEnv: () => Record<string, any>;
|
|
11
8
|
/**
|
|
12
9
|
* When changes have been made during runtime, like in the CLI, we can refresh the env object with
|
|
13
10
|
* the newly created variables
|
|
14
11
|
*/
|
|
15
12
|
export declare function refreshEnv(): Promise<void>;
|
|
13
|
+
export declare function processValues(env: Record<string, any>): Record<string, any>;
|
package/dist/env.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @NOTE
|
|
3
3
|
* For all possible keys, see: https://docs.directus.io/self-hosted/config-options/
|
|
4
4
|
*/
|
|
5
|
-
import { parseJSON, toArray } from '@directus/utils';
|
|
6
5
|
import { JAVASCRIPT_FILE_EXTS } from '@directus/constants';
|
|
6
|
+
import { parseJSON, toArray } from '@directus/utils';
|
|
7
7
|
import dotenv from 'dotenv';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import { clone, toNumber, toString } from 'lodash-es';
|
|
@@ -23,6 +23,7 @@ const allowedEnvironmentVars = [
|
|
|
23
23
|
'PUBLIC_URL',
|
|
24
24
|
'LOG_LEVEL',
|
|
25
25
|
'LOG_STYLE',
|
|
26
|
+
'LOG_HTTP_IGNORE_PATHS',
|
|
26
27
|
'MAX_PAYLOAD_SIZE',
|
|
27
28
|
'ROOT_REDIRECT',
|
|
28
29
|
'SERVE_APP',
|
|
@@ -32,6 +33,7 @@ const allowedEnvironmentVars = [
|
|
|
32
33
|
'QUERY_LIMIT_MAX',
|
|
33
34
|
'QUERY_LIMIT_DEFAULT',
|
|
34
35
|
'ROBOTS_TXT',
|
|
36
|
+
'TEMP_PATH',
|
|
35
37
|
// server
|
|
36
38
|
'SERVER_.+',
|
|
37
39
|
// database
|
|
@@ -156,6 +158,7 @@ const allowedEnvironmentVars = [
|
|
|
156
158
|
'AUTH_.+_SP.+',
|
|
157
159
|
// extensions
|
|
158
160
|
'PACKAGE_FILE_LOCATION',
|
|
161
|
+
'EXTENSIONS_LOCATION',
|
|
159
162
|
'EXTENSIONS_PATH',
|
|
160
163
|
'EXTENSIONS_AUTO_RELOAD',
|
|
161
164
|
'EXTENSIONS_CACHE_TTL',
|
|
@@ -204,7 +207,7 @@ const allowedEnvironmentVars = [
|
|
|
204
207
|
'WEBSOCKETS_.+',
|
|
205
208
|
].map((name) => new RegExp(`^${name}$`));
|
|
206
209
|
const acceptedEnvTypes = ['string', 'number', 'regex', 'array', 'json'];
|
|
207
|
-
const defaults = {
|
|
210
|
+
export const defaults = {
|
|
208
211
|
CONFIG_PATH: path.resolve(process.cwd(), '.env'),
|
|
209
212
|
HOST: '0.0.0.0',
|
|
210
213
|
PORT: 8055,
|
|
@@ -214,6 +217,7 @@ const defaults = {
|
|
|
214
217
|
QUERY_LIMIT_DEFAULT: 100,
|
|
215
218
|
MAX_BATCH_MUTATION: Infinity,
|
|
216
219
|
ROBOTS_TXT: 'User-agent: *\nDisallow: /',
|
|
220
|
+
TEMP_PATH: './node_modules/.directus',
|
|
217
221
|
DB_EXCLUDE_TABLES: 'spatial_ref_sys,sysdiagrams',
|
|
218
222
|
STORAGE_LOCATIONS: 'local',
|
|
219
223
|
STORAGE_LOCAL_DRIVER: 'local',
|
|
@@ -302,8 +306,10 @@ const defaults = {
|
|
|
302
306
|
PRESSURE_LIMITER_RETRY_AFTER: false,
|
|
303
307
|
FILES_MIME_TYPE_ALLOW_LIST: '*/*',
|
|
304
308
|
};
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Allows us to force certain environment variable into a type, instead of relying
|
|
311
|
+
* on the auto-parsed type in processValues.
|
|
312
|
+
*/
|
|
307
313
|
const typeMap = {
|
|
308
314
|
HOST: 'string',
|
|
309
315
|
PORT: 'string',
|
|
@@ -320,6 +326,7 @@ const typeMap = {
|
|
|
320
326
|
GRAPHQL_INTROSPECTION: 'boolean',
|
|
321
327
|
MAX_BATCH_MUTATION: 'number',
|
|
322
328
|
SERVER_SHUTDOWN_TIMEOUT: 'number',
|
|
329
|
+
LOG_HTTP_IGNORE_PATHS: 'array',
|
|
323
330
|
};
|
|
324
331
|
let env = {
|
|
325
332
|
...defaults,
|
|
@@ -329,10 +336,6 @@ let env = {
|
|
|
329
336
|
process.env = env;
|
|
330
337
|
env = processValues(env);
|
|
331
338
|
export default env;
|
|
332
|
-
/**
|
|
333
|
-
* Small wrapper function that makes it easier to write unit tests against changing environments
|
|
334
|
-
*/
|
|
335
|
-
export const getEnv = () => env;
|
|
336
339
|
/**
|
|
337
340
|
* When changes have been made during runtime, like in the CLI, we can refresh the env object with
|
|
338
341
|
* the newly created variables
|
|
@@ -408,7 +411,7 @@ function getEnvironmentValueByType(envVariableString) {
|
|
|
408
411
|
function isEnvSyntaxPrefixPresent(value) {
|
|
409
412
|
return acceptedEnvTypes.some((envType) => value.includes(`${envType}:`));
|
|
410
413
|
}
|
|
411
|
-
function processValues(env) {
|
|
414
|
+
export function processValues(env) {
|
|
412
415
|
env = clone(env);
|
|
413
416
|
for (let [key, value] of Object.entries(env)) {
|
|
414
417
|
// If key ends with '_FILE', try to get the value from the file defined in this variable
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getExtensionsPath: () => any;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getLocalExtensions, getPackageExtensions, resolvePackageExtensions } from '@directus/extensions/node';
|
|
2
2
|
import env from '../../env.js';
|
|
3
|
+
import { getExtensionsPath } from './get-extensions-path.js';
|
|
3
4
|
export const getExtensions = async () => {
|
|
4
|
-
const localExtensions = await getLocalExtensions(
|
|
5
|
+
const localExtensions = await getLocalExtensions(getExtensionsPath());
|
|
5
6
|
const loadedNames = localExtensions.map(({ name }) => name);
|
|
6
7
|
const filterDuplicates = ({ name }) => loadedNames.includes(name) === false;
|
|
7
|
-
const localPackageExtensions = (await resolvePackageExtensions(
|
|
8
|
+
const localPackageExtensions = (await resolvePackageExtensions(getExtensionsPath())).filter((extension) => filterDuplicates(extension));
|
|
8
9
|
loadedNames.push(...localPackageExtensions.map(({ name }) => name));
|
|
9
10
|
const packageExtensions = (await getPackageExtensions(env['PACKAGE_FILE_LOCATION'])).filter((extension) => filterDuplicates(extension));
|
|
10
11
|
return [...packageExtensions, ...localPackageExtensions, ...localExtensions];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const syncExtensions: () => Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NESTED_EXTENSION_TYPES } from '@directus/extensions';
|
|
2
|
+
import { ensureExtensionDirs } from '@directus/extensions/node';
|
|
3
|
+
import mid from 'node-machine-id';
|
|
4
|
+
import { createWriteStream } from 'node:fs';
|
|
5
|
+
import { mkdir } from 'node:fs/promises';
|
|
6
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import Queue from 'p-queue';
|
|
9
|
+
import env from '../../env.js';
|
|
10
|
+
import logger from '../../logger.js';
|
|
11
|
+
import { getMessenger } from '../../messenger.js';
|
|
12
|
+
import { getStorage } from '../../storage/index.js';
|
|
13
|
+
import { getExtensionsPath } from './get-extensions-path.js';
|
|
14
|
+
import { SyncStatus, getSyncStatus, setSyncStatus } from './sync-status.js';
|
|
15
|
+
export const syncExtensions = async () => {
|
|
16
|
+
const extensionsPath = getExtensionsPath();
|
|
17
|
+
if (!env['EXTENSIONS_LOCATION']) {
|
|
18
|
+
// Safe to run with multiple instances since dirs are created with `recursive: true`
|
|
19
|
+
return ensureExtensionDirs(extensionsPath, NESTED_EXTENSION_TYPES);
|
|
20
|
+
}
|
|
21
|
+
const messenger = getMessenger();
|
|
22
|
+
const isPrimaryProcess = String(process.env['NODE_APP_INSTANCE']) === '0' || process.env['NODE_APP_INSTANCE'] === undefined;
|
|
23
|
+
const id = await mid.machineId();
|
|
24
|
+
const message = `extensions-sync/${id}`;
|
|
25
|
+
if (isPrimaryProcess === false) {
|
|
26
|
+
const isDone = (await getSyncStatus()) === SyncStatus.DONE;
|
|
27
|
+
if (isDone)
|
|
28
|
+
return;
|
|
29
|
+
logger.trace('Extensions already being synced to this machine from another process.');
|
|
30
|
+
/**
|
|
31
|
+
* Wait until the process that called the lock publishes a message that the syncing is complete
|
|
32
|
+
*/
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
messenger.subscribe(message, () => resolve());
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// Ensure that the local extensions cache path exists
|
|
38
|
+
await mkdir(extensionsPath, { recursive: true });
|
|
39
|
+
await setSyncStatus(SyncStatus.SYNCING);
|
|
40
|
+
logger.trace('Syncing extensions from configured storage location...');
|
|
41
|
+
const storage = await getStorage();
|
|
42
|
+
const disk = storage.location(env['EXTENSIONS_LOCATION']);
|
|
43
|
+
// Make sure we don't overload the file handles
|
|
44
|
+
const queue = new Queue({ concurrency: 1000 });
|
|
45
|
+
for await (const filepath of disk.list(env['EXTENSIONS_PATH'])) {
|
|
46
|
+
const readStream = await disk.read(filepath);
|
|
47
|
+
// We want files to be stored in the root of `$TEMP_PATH/extensions`, so gotta remove the
|
|
48
|
+
// extensions path on disk from the start of the file path
|
|
49
|
+
const destPath = join(extensionsPath, relative(resolve(sep, env['EXTENSIONS_PATH']), resolve(sep, filepath)));
|
|
50
|
+
// Ensure that the directory path exists
|
|
51
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
52
|
+
const writeStream = createWriteStream(destPath);
|
|
53
|
+
queue.add(() => pipeline(readStream, writeStream));
|
|
54
|
+
}
|
|
55
|
+
await queue.onIdle();
|
|
56
|
+
await ensureExtensionDirs(extensionsPath, NESTED_EXTENSION_TYPES);
|
|
57
|
+
await setSyncStatus(SyncStatus.DONE);
|
|
58
|
+
messenger.publish(message, { ready: true });
|
|
59
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare enum SyncStatus {
|
|
2
|
+
UNKNOWN = "UNKNOWN",
|
|
3
|
+
SYNCING = "SYNCING",
|
|
4
|
+
DONE = "DONE"
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves the sync status from the `.status` file in the local extensions folder
|
|
8
|
+
*/
|
|
9
|
+
export declare const getSyncStatus: () => Promise<string>;
|
|
10
|
+
export declare const setSyncStatus: (status: SyncStatus) => Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { exists } from 'fs-extra';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getExtensionsPath } from './get-extensions-path.js';
|
|
5
|
+
export var SyncStatus;
|
|
6
|
+
(function (SyncStatus) {
|
|
7
|
+
SyncStatus["UNKNOWN"] = "UNKNOWN";
|
|
8
|
+
SyncStatus["SYNCING"] = "SYNCING";
|
|
9
|
+
SyncStatus["DONE"] = "DONE";
|
|
10
|
+
})(SyncStatus || (SyncStatus = {}));
|
|
11
|
+
/**
|
|
12
|
+
* Retrieves the sync status from the `.status` file in the local extensions folder
|
|
13
|
+
*/
|
|
14
|
+
export const getSyncStatus = async () => {
|
|
15
|
+
const statusFilePath = join(getExtensionsPath(), '.status');
|
|
16
|
+
if (await exists(statusFilePath)) {
|
|
17
|
+
const status = await readFile(statusFilePath, 'utf8');
|
|
18
|
+
return status;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return SyncStatus.UNKNOWN;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export const setSyncStatus = async (status) => {
|
|
25
|
+
const statusFilePath = join(getExtensionsPath(), '.status');
|
|
26
|
+
await writeFile(statusFilePath, status);
|
|
27
|
+
};
|