@directus/api 22.1.1 → 23.0.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/app.js +1 -1
- package/dist/auth/drivers/ldap.js +14 -3
- package/dist/auth/drivers/oauth2.js +13 -2
- package/dist/auth/drivers/openid.js +13 -2
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +6 -6
- package/dist/cli/commands/init/questions.d.ts +5 -5
- package/dist/cli/commands/schema/apply.d.ts +1 -0
- package/dist/cli/commands/schema/apply.js +20 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/utils/create-env/env-stub.liquid +1 -4
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/database/helpers/index.d.ts +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
- package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
- package/dist/database/helpers/nullable-update/index.d.ts +7 -0
- package/dist/database/helpers/nullable-update/index.js +7 -0
- package/dist/database/helpers/nullable-update/types.d.ts +7 -0
- package/dist/database/helpers/nullable-update/types.js +12 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +20 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +33 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/oracle.js +21 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/postgres.js +23 -0
- package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
- package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
- package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
- package/dist/database/migrations/20240806A-permissions-policies.js +1 -1
- package/dist/database/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
- package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
- package/dist/database/run-ast/lib/get-db-query.js +14 -8
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +0 -3
- package/dist/extensions/lib/sandbox/register/route.d.ts +1 -2
- package/dist/extensions/manager.js +2 -2
- package/dist/logger/index.d.ts +8 -3
- package/dist/logger/index.js +79 -28
- package/dist/logger/logs-stream.d.ts +10 -0
- package/dist/logger/logs-stream.js +41 -0
- package/dist/mailer.js +0 -6
- package/dist/middleware/authenticate.d.ts +1 -3
- package/dist/middleware/error-handler.d.ts +0 -1
- package/dist/middleware/respond.js +1 -0
- package/dist/middleware/validate-batch.d.ts +1 -4
- package/dist/permissions/lib/fetch-permissions.d.ts +11 -1
- package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +2 -2
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +1 -2
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +14 -6
- package/dist/permissions/utils/process-permissions.d.ts +11 -1
- package/dist/permissions/utils/process-permissions.js +6 -4
- package/dist/request/agent-with-ip-validation.d.ts +0 -1
- package/dist/request/is-denied-ip.js +7 -1
- package/dist/server.d.ts +0 -3
- package/dist/server.js +4 -2
- package/dist/services/assets.d.ts +0 -1
- package/dist/services/fields.js +52 -20
- package/dist/services/files/utils/get-metadata.d.ts +0 -1
- package/dist/services/files/utils/parse-image-metadata.d.ts +0 -1
- package/dist/services/files.d.ts +0 -1
- package/dist/services/import-export.d.ts +0 -1
- package/dist/services/mail/index.js +1 -5
- package/dist/services/notifications.d.ts +0 -4
- package/dist/services/notifications.js +8 -6
- package/dist/services/server.js +8 -1
- package/dist/services/specifications.js +7 -7
- package/dist/services/tus/data-store.d.ts +0 -1
- package/dist/services/users.js +6 -3
- package/dist/types/graphql.d.ts +0 -1
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/compress.d.ts +0 -1
- package/dist/utils/delete-from-require-cache.js +1 -1
- package/dist/utils/fetch-user-count/fetch-user-count.d.ts +1 -2
- package/dist/utils/generate-hash.js +2 -2
- package/dist/utils/get-address.d.ts +1 -4
- package/dist/utils/get-address.js +6 -1
- package/dist/utils/get-allowed-log-levels.d.ts +3 -0
- package/dist/utils/get-allowed-log-levels.js +11 -0
- package/dist/utils/get-cache-headers.d.ts +0 -1
- package/dist/utils/get-cache-key.d.ts +0 -1
- package/dist/utils/get-column.d.ts +1 -1
- package/dist/utils/get-graphql-query-and-variables.d.ts +0 -1
- package/dist/utils/get-ip-from-req.d.ts +0 -1
- package/dist/utils/get-schema.js +19 -24
- package/dist/utils/get-snapshot.js +1 -1
- package/dist/utils/parse-filter-key.js +1 -5
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/utils/should-skip-cache.d.ts +0 -1
- package/dist/websocket/authenticate.js +1 -1
- package/dist/websocket/controllers/base.d.ts +6 -15
- package/dist/websocket/controllers/base.js +17 -4
- package/dist/websocket/controllers/graphql.d.ts +0 -3
- package/dist/websocket/controllers/graphql.js +3 -1
- package/dist/websocket/controllers/index.d.ts +4 -3
- package/dist/websocket/controllers/index.js +12 -0
- package/dist/websocket/controllers/logs.d.ts +17 -0
- package/dist/websocket/controllers/logs.js +54 -0
- package/dist/websocket/controllers/rest.d.ts +0 -3
- package/dist/websocket/controllers/rest.js +4 -2
- package/dist/websocket/handlers/index.d.ts +1 -0
- package/dist/websocket/handlers/index.js +21 -3
- package/dist/websocket/handlers/logs.d.ts +31 -0
- package/dist/websocket/handlers/logs.js +121 -0
- package/dist/websocket/messages.d.ts +26 -0
- package/dist/websocket/messages.js +9 -0
- package/dist/websocket/types.d.ts +6 -5
- package/package.json +48 -49
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
1
2
|
import type { Sql } from '../types.js';
|
|
2
3
|
export type PreprocessBindingsOptions = {
|
|
3
4
|
format(index: number): string;
|
|
4
5
|
};
|
|
6
|
+
/**
|
|
7
|
+
* Preprocess a SQL query, such that repeated binding values are bound to the same binding index.
|
|
8
|
+
**/
|
|
5
9
|
export declare function preprocessBindings(queryParams: (Partial<Sql> & Pick<Sql, 'sql'>) | string, options: PreprocessBindingsOptions): {
|
|
6
10
|
sql: string;
|
|
7
|
-
bindings:
|
|
11
|
+
bindings: Knex.Value[];
|
|
8
12
|
};
|
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import { isString } from 'lodash-es';
|
|
2
|
+
/**
|
|
3
|
+
* Preprocess a SQL query, such that repeated binding values are bound to the same binding index.
|
|
4
|
+
**/
|
|
2
5
|
export function preprocessBindings(queryParams, options) {
|
|
3
6
|
const query = { bindings: [], ...(isString(queryParams) ? { sql: queryParams } : queryParams) };
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (prevIndex !== -1) {
|
|
9
|
-
bindingIndices[i] = prevIndex;
|
|
10
|
-
}
|
|
11
|
-
else {
|
|
12
|
-
bindingIndices[i] = i;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
7
|
+
// bindingIndices[i] is the index of the first occurrence of query.bindings[i]
|
|
8
|
+
const bindingIndices = new Map();
|
|
9
|
+
// The new, deduplicated bindings
|
|
10
|
+
const bindings = [];
|
|
15
11
|
let matchIndex = 0;
|
|
16
|
-
let
|
|
17
|
-
const sql = query.sql.replace(/(\\*)(\?)/g,
|
|
12
|
+
let nextBindingIndex = 0;
|
|
13
|
+
const sql = query.sql.replace(/(\\*)(\?)/g, (_, escapes) => {
|
|
18
14
|
if (escapes.length % 2) {
|
|
19
15
|
// Return an escaped question mark, so it stays escaped
|
|
20
16
|
return `${'\\'.repeat(escapes.length)}?`;
|
|
21
17
|
}
|
|
18
|
+
const binding = query.bindings[matchIndex];
|
|
19
|
+
let bindingIndex;
|
|
20
|
+
if (bindingIndices.has(binding)) {
|
|
21
|
+
// This index belongs to a binding that has been encountered before.
|
|
22
|
+
bindingIndex = bindingIndices.get(binding);
|
|
23
|
+
}
|
|
22
24
|
else {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// The first time the value is encountered, set the index lookup to the current index
|
|
26
|
+
// Use the nextBindingIndex to get the next unused binding index that is used in the new, deduplicated bindings
|
|
27
|
+
bindingIndex = nextBindingIndex++;
|
|
28
|
+
bindingIndices.set(binding, bindingIndex);
|
|
29
|
+
bindings.push(binding);
|
|
26
30
|
}
|
|
31
|
+
// Increment the loop counter
|
|
32
|
+
matchIndex++;
|
|
33
|
+
return options.format(bindingIndex);
|
|
27
34
|
});
|
|
28
|
-
const bindings = query.bindings.filter((_, i) => bindingIndices[i] === i);
|
|
29
35
|
return { ...query, sql, bindings };
|
|
30
36
|
}
|
|
@@ -43,7 +43,7 @@ export async function up(knex) {
|
|
|
43
43
|
.update({ [constraint.many_field]: null })
|
|
44
44
|
.whereIn(currentPrimaryKeyField, ids);
|
|
45
45
|
}
|
|
46
|
-
catch
|
|
46
|
+
catch {
|
|
47
47
|
logger.error(`${constraint.many_collection}.${constraint.many_field} contains illegal foreign keys which couldn't be set to NULL. Please fix these references and rerun this migration to complete the upgrade.`);
|
|
48
48
|
if (ids.length < 25) {
|
|
49
49
|
logger.error(`Items with illegal foreign keys: ${ids.join(', ')}`);
|
|
@@ -198,7 +198,7 @@ export async function up(knex) {
|
|
|
198
198
|
table.dropForeign('role', foreignConstraint);
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
|
-
catch
|
|
201
|
+
catch {
|
|
202
202
|
logger.warn('Failed to drop foreign key constraint on `role` column in `directus_permissions` table');
|
|
203
203
|
}
|
|
204
204
|
await knex('directus_permissions')
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getHelpers } from '../helpers/index.js';
|
|
2
|
+
export async function up(knex) {
|
|
3
|
+
const helper = getHelpers(knex).schema;
|
|
4
|
+
await helper.changeToType('directus_collections', 'icon', 'string', {
|
|
5
|
+
length: 64,
|
|
6
|
+
});
|
|
7
|
+
await helper.changeToType('directus_dashboards', 'icon', 'string', {
|
|
8
|
+
nullable: false,
|
|
9
|
+
default: 'dashboard',
|
|
10
|
+
length: 64,
|
|
11
|
+
});
|
|
12
|
+
await helper.changeToType('directus_flows', 'icon', 'string', {
|
|
13
|
+
length: 64,
|
|
14
|
+
});
|
|
15
|
+
await helper.changeToType('directus_panels', 'icon', 'string', {
|
|
16
|
+
default: null,
|
|
17
|
+
length: 64,
|
|
18
|
+
});
|
|
19
|
+
await helper.changeToType('directus_presets', 'icon', 'string', {
|
|
20
|
+
default: 'bookmark',
|
|
21
|
+
length: 64,
|
|
22
|
+
});
|
|
23
|
+
await helper.changeToType('directus_roles', 'icon', 'string', {
|
|
24
|
+
nullable: false,
|
|
25
|
+
default: 'supervised_user_circle',
|
|
26
|
+
length: 64,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export async function down(knex) {
|
|
30
|
+
const helper = getHelpers(knex).schema;
|
|
31
|
+
await helper.changeToType('directus_collections', 'icon', 'string', {
|
|
32
|
+
length: 30,
|
|
33
|
+
});
|
|
34
|
+
await helper.changeToType('directus_dashboards', 'icon', 'string', {
|
|
35
|
+
nullable: false,
|
|
36
|
+
default: 'dashboard',
|
|
37
|
+
length: 30,
|
|
38
|
+
});
|
|
39
|
+
await helper.changeToType('directus_flows', 'icon', 'string', {
|
|
40
|
+
length: 30,
|
|
41
|
+
});
|
|
42
|
+
await helper.changeToType('directus_panels', 'icon', 'string', {
|
|
43
|
+
default: null,
|
|
44
|
+
length: 30,
|
|
45
|
+
});
|
|
46
|
+
await helper.changeToType('directus_presets', 'icon', 'string', {
|
|
47
|
+
default: 'bookmark',
|
|
48
|
+
length: 30,
|
|
49
|
+
});
|
|
50
|
+
await helper.changeToType('directus_roles', 'icon', 'string', {
|
|
51
|
+
nullable: false,
|
|
52
|
+
default: 'supervised_user_circle',
|
|
53
|
+
length: 30,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -86,19 +86,21 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
86
86
|
orderByString += ', ';
|
|
87
87
|
}
|
|
88
88
|
const sortAlias = `sort_${generateAlias()}`;
|
|
89
|
+
let orderByColumn;
|
|
89
90
|
if (sortRecord.column.includes('.')) {
|
|
90
91
|
const [alias, field] = sortRecord.column.split('.');
|
|
91
92
|
const originalCollectionName = getCollectionFromAlias(alias, aliasMap);
|
|
92
93
|
dbQuery.select(getColumn(knex, alias, field, sortAlias, schema, { originalCollectionName }));
|
|
93
94
|
orderByString += `?? ${sortRecord.order}`;
|
|
94
|
-
|
|
95
|
+
orderByColumn = getColumn(knex, alias, field, false, schema, { originalCollectionName });
|
|
95
96
|
}
|
|
96
97
|
else {
|
|
97
98
|
dbQuery.select(getColumn(knex, table, sortRecord.column, sortAlias, schema));
|
|
98
99
|
orderByString += `?? ${sortRecord.order}`;
|
|
99
|
-
|
|
100
|
+
orderByColumn = getColumn(knex, table, sortRecord.column, false, schema);
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
+
orderByFields.push(orderByColumn);
|
|
103
|
+
innerQuerySortRecords.push({ alias: sortAlias, order: sortRecord.order, column: orderByColumn });
|
|
102
104
|
});
|
|
103
105
|
if (hasMultiRelationalSort) {
|
|
104
106
|
dbQuery = helpers.schema.applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields);
|
|
@@ -171,11 +173,15 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
171
173
|
// based on the case/when of that field.
|
|
172
174
|
dbQuery.select(o2mNodes.map(innerPreprocess).filter((x) => x !== null));
|
|
173
175
|
const groupByFields = [knex.raw('??.??', [table, primaryKey])];
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
// For some DB vendors sort fields need to be included in the group by clause, otherwise this causes problems those DBs
|
|
177
|
+
// since sort fields are selected in the inner query, and they expect all selected columns to be in
|
|
178
|
+
// the group by clause or aggregated over.
|
|
179
|
+
// For some DBs the field needs to be the actual raw column expression, since aliases are not available in the
|
|
180
|
+
// group by clause.
|
|
181
|
+
// Since the fields are expected to be the same for a single primary key it is safe to include them in the
|
|
182
|
+
// group by without influencing the result.
|
|
183
|
+
// This inclusion depends on the DB vendor, as such it is handled in a dialect specific helper.
|
|
184
|
+
helpers.schema.addInnerSortFieldsToGroupBy(groupByFields, innerQuerySortRecords, hasMultiRelationalSort ?? false);
|
|
179
185
|
dbQuery.groupBy(groupByFields);
|
|
180
186
|
}
|
|
181
187
|
const wrapperQuery = knex
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
1
|
import type { Router } from 'express';
|
|
3
2
|
import type { Reference } from 'isolated-vm';
|
|
4
3
|
import type { IncomingHttpHeaders } from 'node:http';
|
|
5
4
|
export declare function registerRouteGenerator(endpointName: string, endpointRouter: Router): {
|
|
6
|
-
register: (path: Reference<string>, method: Reference<
|
|
5
|
+
register: (path: Reference<string>, method: Reference<"GET" | "POST" | "PUT" | "PATCH" | "DELETE">, cb: Reference<(req: {
|
|
7
6
|
url: string;
|
|
8
7
|
headers: IncomingHttpHeaders;
|
|
9
8
|
body: string;
|
|
@@ -156,7 +156,7 @@ export class ExtensionManager {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
if (this.options.watch && !wasWatcherInitialized) {
|
|
159
|
-
this.updateWatchedExtensions(
|
|
159
|
+
this.updateWatchedExtensions([...this.extensions]);
|
|
160
160
|
}
|
|
161
161
|
this.messenger.subscribe(this.reloadChannel, (payload) => {
|
|
162
162
|
// Ignore requests for reloading that were published by the current process
|
|
@@ -327,7 +327,7 @@ export class ExtensionManager {
|
|
|
327
327
|
const extensionDir = path.resolve(getExtensionsPath());
|
|
328
328
|
const registryDir = path.join(extensionDir, '.registry');
|
|
329
329
|
const toPackageExtensionPaths = (extensions) => extensions
|
|
330
|
-
.filter((extension) => extension.local &&
|
|
330
|
+
.filter((extension) => extension.local && !extension.path.startsWith(registryDir))
|
|
331
331
|
.flatMap((extension) => isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle'
|
|
332
332
|
? [
|
|
333
333
|
path.resolve(extension.path, extension.entrypoint.app),
|
package/dist/logger/index.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
/// <reference types="qs" />
|
|
2
1
|
import type { RequestHandler } from 'express';
|
|
3
2
|
import { type Logger } from 'pino';
|
|
3
|
+
import { LogsStream } from './logs-stream.js';
|
|
4
4
|
export declare const _cache: {
|
|
5
5
|
logger: Logger<never> | undefined;
|
|
6
|
+
logsStream: LogsStream | undefined;
|
|
7
|
+
httpLogsStream: LogsStream | undefined;
|
|
6
8
|
};
|
|
7
9
|
export declare const useLogger: () => Logger<never>;
|
|
8
|
-
export declare const
|
|
9
|
-
export declare const
|
|
10
|
+
export declare const getLogsStream: (pretty: boolean) => LogsStream;
|
|
11
|
+
export declare const getHttpLogsStream: (pretty: boolean) => LogsStream;
|
|
12
|
+
export declare const getLoggerLevelValue: (level: string) => number;
|
|
13
|
+
export declare const createLogger: () => Logger<never, boolean>;
|
|
14
|
+
export declare const createExpressLogger: () => RequestHandler;
|
package/dist/logger/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import { REDACTED_TEXT, toArray } from '@directus/utils';
|
|
2
|
+
import { REDACTED_TEXT, toArray, toBoolean } from '@directus/utils';
|
|
3
3
|
import { merge } from 'lodash-es';
|
|
4
4
|
import { URL } from 'node:url';
|
|
5
5
|
import { pino } from 'pino';
|
|
6
6
|
import { pinoHttp, stdSerializers } from 'pino-http';
|
|
7
|
+
import { httpPrintFactory } from 'pino-http-print';
|
|
8
|
+
import { build as pinoPretty } from 'pino-pretty';
|
|
7
9
|
import { getConfigFromEnv } from '../utils/get-config-from-env.js';
|
|
10
|
+
import { LogsStream } from './logs-stream.js';
|
|
8
11
|
import { redactQuery } from './redact-query.js';
|
|
9
|
-
export const _cache = { logger: undefined };
|
|
12
|
+
export const _cache = { logger: undefined, logsStream: undefined, httpLogsStream: undefined };
|
|
10
13
|
export const useLogger = () => {
|
|
11
14
|
if (_cache.logger) {
|
|
12
15
|
return _cache.logger;
|
|
@@ -14,6 +17,23 @@ export const useLogger = () => {
|
|
|
14
17
|
_cache.logger = createLogger();
|
|
15
18
|
return _cache.logger;
|
|
16
19
|
};
|
|
20
|
+
export const getLogsStream = (pretty) => {
|
|
21
|
+
if (_cache.logsStream) {
|
|
22
|
+
return _cache.logsStream;
|
|
23
|
+
}
|
|
24
|
+
_cache.logsStream = new LogsStream(pretty ? 'basic' : false);
|
|
25
|
+
return _cache.logsStream;
|
|
26
|
+
};
|
|
27
|
+
export const getHttpLogsStream = (pretty) => {
|
|
28
|
+
if (_cache.httpLogsStream) {
|
|
29
|
+
return _cache.httpLogsStream;
|
|
30
|
+
}
|
|
31
|
+
_cache.httpLogsStream = new LogsStream(pretty ? 'http' : false);
|
|
32
|
+
return _cache.httpLogsStream;
|
|
33
|
+
};
|
|
34
|
+
export const getLoggerLevelValue = (level) => {
|
|
35
|
+
return pino.levels.values[level] || pino.levels.values['info'];
|
|
36
|
+
};
|
|
17
37
|
export const createLogger = () => {
|
|
18
38
|
const env = useEnv();
|
|
19
39
|
const pinoOptions = {
|
|
@@ -23,15 +43,6 @@ export const createLogger = () => {
|
|
|
23
43
|
censor: REDACTED_TEXT,
|
|
24
44
|
},
|
|
25
45
|
};
|
|
26
|
-
if (env['LOG_STYLE'] !== 'raw') {
|
|
27
|
-
pinoOptions.transport = {
|
|
28
|
-
target: 'pino-pretty',
|
|
29
|
-
options: {
|
|
30
|
-
ignore: 'hostname,pid',
|
|
31
|
-
sync: true,
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
46
|
const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
|
|
36
47
|
// Expose custom log levels into formatter function
|
|
37
48
|
if (loggerEnvConfig['levels']) {
|
|
@@ -50,7 +61,33 @@ export const createLogger = () => {
|
|
|
50
61
|
};
|
|
51
62
|
delete loggerEnvConfig['levels'];
|
|
52
63
|
}
|
|
53
|
-
|
|
64
|
+
const mergedOptions = merge(pinoOptions, loggerEnvConfig);
|
|
65
|
+
const streams = [];
|
|
66
|
+
// Console Logs
|
|
67
|
+
if (env['LOG_STYLE'] !== 'raw') {
|
|
68
|
+
streams.push({
|
|
69
|
+
level: mergedOptions.level,
|
|
70
|
+
stream: pinoPretty({
|
|
71
|
+
ignore: 'hostname,pid',
|
|
72
|
+
sync: true,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
streams.push({ level: mergedOptions.level, stream: process.stdout });
|
|
78
|
+
}
|
|
79
|
+
// WebSocket Logs
|
|
80
|
+
if (toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) {
|
|
81
|
+
const wsLevel = env['WEBSOCKETS_LOGS_LEVEL'] || 'info';
|
|
82
|
+
if (getLoggerLevelValue(wsLevel) < getLoggerLevelValue(mergedOptions.level)) {
|
|
83
|
+
mergedOptions.level = wsLevel;
|
|
84
|
+
}
|
|
85
|
+
streams.push({
|
|
86
|
+
level: wsLevel,
|
|
87
|
+
stream: getLogsStream(env['WEBSOCKETS_LOGS_STYLE'] !== 'raw'),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return pino(mergedOptions, pino.multistream(streams));
|
|
54
91
|
};
|
|
55
92
|
export const createExpressLogger = () => {
|
|
56
93
|
const env = useEnv();
|
|
@@ -63,21 +100,7 @@ export const createExpressLogger = () => {
|
|
|
63
100
|
censor: REDACTED_TEXT,
|
|
64
101
|
},
|
|
65
102
|
};
|
|
66
|
-
if (env['LOG_STYLE']
|
|
67
|
-
httpLoggerOptions.transport = {
|
|
68
|
-
target: 'pino-http-print',
|
|
69
|
-
options: {
|
|
70
|
-
all: true,
|
|
71
|
-
translateTime: 'SYS:HH:MM:ss',
|
|
72
|
-
relativeUrl: true,
|
|
73
|
-
prettyOptions: {
|
|
74
|
-
ignore: 'hostname,pid',
|
|
75
|
-
sync: true,
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
if (env['LOG_STYLE'] === 'raw') {
|
|
103
|
+
if (env['LOG_STYLE'] === 'raw' || toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) {
|
|
81
104
|
httpLoggerOptions.redact = {
|
|
82
105
|
paths: ['req.headers.authorization', 'req.headers.cookie', 'res.headers', 'req.query.access_token'],
|
|
83
106
|
censor: (value, pathParts) => {
|
|
@@ -120,8 +143,36 @@ export const createExpressLogger = () => {
|
|
|
120
143
|
},
|
|
121
144
|
};
|
|
122
145
|
}
|
|
146
|
+
const mergedHttpOptions = merge(httpLoggerOptions, loggerEnvConfig);
|
|
147
|
+
const streams = [];
|
|
148
|
+
if (env['LOG_STYLE'] !== 'raw') {
|
|
149
|
+
const pinoHttpPretty = httpPrintFactory({
|
|
150
|
+
all: true,
|
|
151
|
+
translateTime: 'SYS:HH:MM:ss',
|
|
152
|
+
relativeUrl: true,
|
|
153
|
+
prettyOptions: {
|
|
154
|
+
ignore: 'hostname,pid',
|
|
155
|
+
sync: true,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
streams.push({ level: mergedHttpOptions.level, stream: pinoHttpPretty(process.stdout) });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
streams.push({ level: mergedHttpOptions.level, stream: process.stdout });
|
|
162
|
+
}
|
|
163
|
+
// WebSocket Logs
|
|
164
|
+
if (toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) {
|
|
165
|
+
const wsLevel = env['WEBSOCKETS_LOGS_LEVEL'] || 'info';
|
|
166
|
+
if (getLoggerLevelValue(wsLevel) < getLoggerLevelValue(mergedHttpOptions.level)) {
|
|
167
|
+
mergedHttpOptions.level = wsLevel;
|
|
168
|
+
}
|
|
169
|
+
streams.push({
|
|
170
|
+
level: wsLevel,
|
|
171
|
+
stream: getHttpLogsStream(env['WEBSOCKETS_LOGS_STYLE'] !== 'raw'),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
123
174
|
return pinoHttp({
|
|
124
|
-
logger: pino(
|
|
175
|
+
logger: pino(mergedHttpOptions, pino.multistream(streams)),
|
|
125
176
|
...httpLoggerEnvConfig,
|
|
126
177
|
serializers: {
|
|
127
178
|
req(request) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Bus } from '@directus/memory';
|
|
2
|
+
import { Writable } from 'stream';
|
|
3
|
+
type PrettyType = 'basic' | 'http' | false;
|
|
4
|
+
export declare class LogsStream extends Writable {
|
|
5
|
+
messenger: Bus;
|
|
6
|
+
pretty: PrettyType;
|
|
7
|
+
constructor(pretty: PrettyType);
|
|
8
|
+
_write(chunk: string, _encoding: string, callback: (error?: Error | null) => void): void;
|
|
9
|
+
}
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { Writable } from 'stream';
|
|
3
|
+
import { useBus } from '../bus/index.js';
|
|
4
|
+
const nodeId = nanoid(8);
|
|
5
|
+
export class LogsStream extends Writable {
|
|
6
|
+
messenger;
|
|
7
|
+
pretty;
|
|
8
|
+
constructor(pretty) {
|
|
9
|
+
super({ objectMode: true });
|
|
10
|
+
this.messenger = useBus();
|
|
11
|
+
this.pretty = pretty;
|
|
12
|
+
}
|
|
13
|
+
_write(chunk, _encoding, callback) {
|
|
14
|
+
if (!this.pretty) {
|
|
15
|
+
// keeping this string interpolation for performance on RAW logs
|
|
16
|
+
this.messenger.publish('logs', `{"log":${chunk},"nodeId":"${nodeId}"}`);
|
|
17
|
+
return callback();
|
|
18
|
+
}
|
|
19
|
+
const log = JSON.parse(chunk);
|
|
20
|
+
if (this.pretty === 'http' && log.req?.method && log.req?.url && log.res?.statusCode && log.responseTime) {
|
|
21
|
+
this.messenger.publish('logs', JSON.stringify({
|
|
22
|
+
log: {
|
|
23
|
+
level: log['level'],
|
|
24
|
+
time: log['time'],
|
|
25
|
+
msg: `${log.req.method} ${log.req.url} ${log.res.statusCode} ${log.responseTime}ms`,
|
|
26
|
+
},
|
|
27
|
+
nodeId: nodeId,
|
|
28
|
+
}));
|
|
29
|
+
return callback();
|
|
30
|
+
}
|
|
31
|
+
this.messenger.publish('logs', JSON.stringify({
|
|
32
|
+
log: {
|
|
33
|
+
level: log['level'],
|
|
34
|
+
time: log['time'],
|
|
35
|
+
msg: log['msg'],
|
|
36
|
+
},
|
|
37
|
+
nodeId: nodeId,
|
|
38
|
+
}));
|
|
39
|
+
callback();
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/mailer.js
CHANGED
|
@@ -56,12 +56,6 @@ export default function getMailer() {
|
|
|
56
56
|
host: env['EMAIL_MAILGUN_HOST'] || 'api.mailgun.net',
|
|
57
57
|
}));
|
|
58
58
|
}
|
|
59
|
-
else if (transportName === 'sendgrid') {
|
|
60
|
-
const sg = require('nodemailer-sendgrid');
|
|
61
|
-
transporter = nodemailer.createTransport(sg({
|
|
62
|
-
apiKey: env['EMAIL_SENDGRID_API_KEY'],
|
|
63
|
-
}));
|
|
64
|
-
}
|
|
65
59
|
else {
|
|
66
60
|
logger.warn('Illegal transport given for email. Check the EMAIL_TRANSPORT env var.');
|
|
67
61
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
/// <reference types="qs" />
|
|
2
|
-
/// <reference types="cookie-parser" />
|
|
3
1
|
import type { NextFunction, Request, Response } from 'express';
|
|
4
2
|
/**
|
|
5
3
|
* Verify the passed JWT and assign the user ID and role to `req`
|
|
6
4
|
*/
|
|
7
5
|
export declare const handler: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
8
|
-
declare const _default: (req: Request
|
|
6
|
+
declare const _default: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
9
7
|
export default _default;
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
/// <reference types="qs" />
|
|
2
1
|
import type { ErrorRequestHandler } from 'express';
|
|
3
2
|
export declare const errorHandler: (err: any, req: import("express-serve-static-core").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express-serve-static-core").Response<any, Record<string, any>, number>, next: import("express-serve-static-core").NextFunction) => Promise<ReturnType<ErrorRequestHandler>>;
|
|
@@ -20,6 +20,7 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
20
20
|
exceedsMaxSize = valueSize > maxSize;
|
|
21
21
|
}
|
|
22
22
|
if ((req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
|
|
23
|
+
req.originalUrl?.startsWith('/auth') === false &&
|
|
23
24
|
env['CACHE_ENABLED'] === true &&
|
|
24
25
|
cache &&
|
|
25
26
|
!req.sanitizedQuery.export &&
|
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/// <reference types="express" />
|
|
3
|
-
/// <reference types="cookie-parser" />
|
|
4
|
-
export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => (req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express").Response<any, Record<string, any>>, next: import("express").NextFunction) => Promise<void>;
|
|
1
|
+
export declare const validateBatch: (scope: "read" | "update" | "delete") => (req: import("express").Request, res: import("express").Response, next: import("express").NextFunction) => Promise<void>;
|
|
@@ -7,4 +7,14 @@ export interface FetchPermissionsOptions {
|
|
|
7
7
|
accountability?: Pick<Accountability, 'user' | 'role' | 'roles' | 'app'>;
|
|
8
8
|
bypassDynamicVariableProcessing?: boolean;
|
|
9
9
|
}
|
|
10
|
-
export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<
|
|
10
|
+
export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<{
|
|
11
|
+
permissions: import("@directus/types").Filter | null;
|
|
12
|
+
validation: import("@directus/types").Filter | null;
|
|
13
|
+
presets: any;
|
|
14
|
+
id?: number;
|
|
15
|
+
policy: string | null;
|
|
16
|
+
collection: string;
|
|
17
|
+
action: PermissionsAction;
|
|
18
|
+
fields: string[] | null;
|
|
19
|
+
system?: true;
|
|
20
|
+
}[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CollectionKey, FieldMap, QueryPath } from '../types.js';
|
|
2
2
|
export declare function getInfoForPath(fieldMap: FieldMap, group: keyof FieldMap, path: QueryPath, collection: CollectionKey): {
|
|
3
|
-
collection:
|
|
4
|
-
fields: Set<
|
|
3
|
+
collection: CollectionKey;
|
|
4
|
+
fields: Set<import("../types.js").FieldKey>;
|
|
5
5
|
};
|
package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type FetchUserCountOptions } from '../../../utils/fetch-user-count/fetch-user-count.js';
|
|
2
2
|
import type { Context } from '../../types.js';
|
|
3
|
-
export
|
|
4
|
-
}
|
|
3
|
+
export type ValidateRemainingAdminUsersOptions = Pick<FetchUserCountOptions, 'excludeAccessRows' | 'excludePolicies' | 'excludeUsers' | 'excludeRoles'>;
|
|
5
4
|
export declare function validateRemainingAdminUsers(options: ValidateRemainingAdminUsersOptions, context: Context): Promise<void>;
|
|
@@ -32,13 +32,21 @@ export async function fetchDynamicVariableContext(options, context) {
|
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
if (options.policies.length > 0
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
if (options.policies.length > 0) {
|
|
36
|
+
if ((permissionContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
|
|
37
|
+
// Always add the id field
|
|
38
|
+
permissionContext.$CURRENT_POLICIES.add('id');
|
|
39
|
+
contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES', permissionContext, { policies: options.policies }, async (fields) => {
|
|
40
|
+
const policiesService = new PoliciesService(context);
|
|
41
|
+
return await policiesService.readMany(options.policies, {
|
|
42
|
+
fields,
|
|
43
|
+
});
|
|
40
44
|
});
|
|
41
|
-
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Always create entries for the policies with the `id` field present
|
|
48
|
+
contextData['$CURRENT_POLICIES'] = options.policies.map((id) => ({ id }));
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
return contextData;
|
|
44
52
|
}
|
|
@@ -4,4 +4,14 @@ export interface ProcessPermissionsOptions {
|
|
|
4
4
|
accountability: Pick<Accountability, 'user' | 'role' | 'roles'>;
|
|
5
5
|
permissionsContext: Record<string, any>;
|
|
6
6
|
}
|
|
7
|
-
export declare function processPermissions({ permissions, accountability, permissionsContext }: ProcessPermissionsOptions):
|
|
7
|
+
export declare function processPermissions({ permissions, accountability, permissionsContext }: ProcessPermissionsOptions): {
|
|
8
|
+
permissions: import("@directus/types").Filter | null;
|
|
9
|
+
validation: import("@directus/types").Filter | null;
|
|
10
|
+
presets: any;
|
|
11
|
+
id?: number;
|
|
12
|
+
policy: string | null;
|
|
13
|
+
collection: string;
|
|
14
|
+
action: import("@directus/types").PermissionsAction;
|
|
15
|
+
fields: string[] | null;
|
|
16
|
+
system?: true;
|
|
17
|
+
}[];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { parseFilter, parsePreset } from '@directus/utils';
|
|
2
2
|
export function processPermissions({ permissions, accountability, permissionsContext }) {
|
|
3
3
|
return permissions.map((permission) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
return {
|
|
5
|
+
...permission,
|
|
6
|
+
permissions: parseFilter(permission.permissions, accountability, permissionsContext),
|
|
7
|
+
validation: parseFilter(permission.validation, accountability, permissionsContext),
|
|
8
|
+
presets: parsePreset(permission.presets, accountability, permissionsContext),
|
|
9
|
+
};
|
|
8
10
|
});
|
|
9
11
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
+
import { matches } from 'ip-matching';
|
|
3
4
|
import { useLogger } from '../logger/index.js';
|
|
4
5
|
import { ipInNetworks } from '../utils/ip-in-networks.js';
|
|
5
6
|
export function isDeniedIp(ip) {
|
|
@@ -24,8 +25,13 @@ export function isDeniedIp(ip) {
|
|
|
24
25
|
if (!networkInfo)
|
|
25
26
|
continue;
|
|
26
27
|
for (const info of networkInfo) {
|
|
27
|
-
if (info.
|
|
28
|
+
if (info.internal && info.cidr) {
|
|
29
|
+
if (matches(ip, info.cidr))
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
else if (info.address === ip) {
|
|
28
33
|
return true;
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
}
|