@directus/api 22.1.1 → 22.2.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/cache.d.ts +2 -2
- package/dist/cache.js +2 -2
- 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/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/manager.js +2 -2
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +79 -28
- package/dist/logger/logs-stream.d.ts +11 -0
- package/dist/logger/logs-stream.js +41 -0
- package/dist/middleware/respond.js +1 -0
- package/dist/request/is-denied-ip.js +7 -1
- package/dist/server.js +4 -2
- package/dist/services/fields.js +58 -20
- 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/users.js +4 -1
- package/dist/utils/get-address.d.ts +1 -1
- 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-schema.js +19 -24
- package/dist/utils/parse-filter-key.js +1 -5
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/controllers/base.d.ts +10 -10
- package/dist/websocket/controllers/base.js +22 -3
- package/dist/websocket/controllers/graphql.js +3 -1
- package/dist/websocket/controllers/index.d.ts +4 -0
- package/dist/websocket/controllers/index.js +12 -0
- package/dist/websocket/controllers/logs.d.ts +18 -0
- package/dist/websocket/controllers/logs.js +50 -0
- package/dist/websocket/controllers/rest.js +3 -1
- 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 +7 -0
- package/package.json +25 -25
|
@@ -16,7 +16,7 @@ export declare function sanitizeCollection(collection: Collection | undefined):
|
|
|
16
16
|
* @returns sanitized field
|
|
17
17
|
*/
|
|
18
18
|
export declare function sanitizeField(field: Field | undefined, sanitizeAllSchema?: boolean): Partial<Field> | undefined;
|
|
19
|
-
export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "name" | "numeric_precision" | "
|
|
19
|
+
export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "name" | "numeric_precision" | "data_type" | "default_value" | "max_length" | "numeric_scale" | "is_nullable" | "is_unique" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment" | "foreign_key_table" | "foreign_key_column">;
|
|
20
20
|
/**
|
|
21
21
|
* Pick certain database vendor specific relation properties that should be compared when performing diff
|
|
22
22
|
*
|
|
@@ -3,19 +3,17 @@
|
|
|
3
3
|
/// <reference types="node" resolution-mode="require"/>
|
|
4
4
|
/// <reference types="node/http.js" />
|
|
5
5
|
/// <reference types="pino-http" />
|
|
6
|
+
import type { Accountability } from '@directus/types';
|
|
6
7
|
import type { IncomingMessage, Server as httpServer } from 'http';
|
|
7
8
|
import type { RateLimiterAbstract } from 'rate-limiter-flexible';
|
|
8
9
|
import type internal from 'stream';
|
|
9
10
|
import WebSocket from 'ws';
|
|
10
|
-
import {
|
|
11
|
-
import type { AuthenticationState, UpgradeContext, WebSocketClient } from '../types.js';
|
|
11
|
+
import { WebSocketAuthMessage, WebSocketMessage } from '../messages.js';
|
|
12
|
+
import type { AuthenticationState, UpgradeContext, WebSocketAuthentication, WebSocketClient } from '../types.js';
|
|
12
13
|
export default abstract class SocketController {
|
|
13
14
|
server: WebSocket.Server;
|
|
14
15
|
clients: Set<WebSocketClient>;
|
|
15
|
-
authentication:
|
|
16
|
-
mode: AuthMode;
|
|
17
|
-
timeout: number;
|
|
18
|
-
};
|
|
16
|
+
authentication: WebSocketAuthentication;
|
|
19
17
|
endpoint: string;
|
|
20
18
|
maxConnections: number;
|
|
21
19
|
private rateLimiter;
|
|
@@ -23,10 +21,7 @@ export default abstract class SocketController {
|
|
|
23
21
|
constructor(httpServer: httpServer, configPrefix: string);
|
|
24
22
|
protected getEnvironmentConfig(configPrefix: string): {
|
|
25
23
|
endpoint: string;
|
|
26
|
-
authentication:
|
|
27
|
-
mode: AuthMode;
|
|
28
|
-
timeout: number;
|
|
29
|
-
};
|
|
24
|
+
authentication: WebSocketAuthentication;
|
|
30
25
|
maxConnections: number;
|
|
31
26
|
};
|
|
32
27
|
protected getRateLimiter(): RateLimiterAbstract | null;
|
|
@@ -39,5 +34,10 @@ export default abstract class SocketController {
|
|
|
39
34
|
protected handleAuthRequest(client: WebSocketClient, message: WebSocketAuthMessage): Promise<void>;
|
|
40
35
|
setTokenExpireTimer(client: WebSocketClient): void;
|
|
41
36
|
checkClientTokens(): void;
|
|
37
|
+
meetsAdminRequirement({ socket, client, accountability, }: {
|
|
38
|
+
socket?: UpgradeContext['socket'];
|
|
39
|
+
client?: WebSocketClient | WebSocket;
|
|
40
|
+
accountability: Accountability | null;
|
|
41
|
+
}): boolean;
|
|
42
42
|
terminate(): void;
|
|
43
43
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { InvalidProviderConfigError, TokenExpiredError } from '@directus/errors';
|
|
3
3
|
import { parseJSON, toBoolean } from '@directus/utils';
|
|
4
|
+
import cookie from 'cookie';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { parse } from 'url';
|
|
6
7
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
@@ -15,8 +16,6 @@ import { AuthMode, WebSocketAuthMessage, WebSocketMessage } from '../messages.js
|
|
|
15
16
|
import { getExpiresAtForToken } from '../utils/get-expires-at-for-token.js';
|
|
16
17
|
import { getMessageType } from '../utils/message.js';
|
|
17
18
|
import { waitForAnyMessage, waitForMessageType } from '../utils/wait-for-message.js';
|
|
18
|
-
import { registerWebSocketEvents } from './hooks.js';
|
|
19
|
-
import cookie from 'cookie';
|
|
20
19
|
const TOKEN_CHECK_INTERVAL = 15 * 60 * 1000; // 15 minutes
|
|
21
20
|
const logger = useLogger();
|
|
22
21
|
export default class SocketController {
|
|
@@ -42,7 +41,6 @@ export default class SocketController {
|
|
|
42
41
|
this.rateLimiter = this.getRateLimiter();
|
|
43
42
|
httpServer.on('upgrade', this.handleUpgrade.bind(this));
|
|
44
43
|
this.checkClientTokens();
|
|
45
|
-
registerWebSocketEvents();
|
|
46
44
|
}
|
|
47
45
|
getEnvironmentConfig(configPrefix) {
|
|
48
46
|
const env = useEnv();
|
|
@@ -62,6 +60,7 @@ export default class SocketController {
|
|
|
62
60
|
authentication: {
|
|
63
61
|
mode: authMode.data,
|
|
64
62
|
timeout: authTimeout,
|
|
63
|
+
requireAdmin: false,
|
|
65
64
|
},
|
|
66
65
|
};
|
|
67
66
|
}
|
|
@@ -141,6 +140,8 @@ export default class SocketController {
|
|
|
141
140
|
socket.destroy();
|
|
142
141
|
return;
|
|
143
142
|
}
|
|
143
|
+
if (!this.meetsAdminRequirement({ socket, accountability }))
|
|
144
|
+
return;
|
|
144
145
|
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
145
146
|
this.catchInvalidMessages(ws);
|
|
146
147
|
const state = { accountability, expires_at };
|
|
@@ -155,6 +156,8 @@ export default class SocketController {
|
|
|
155
156
|
if (getMessageType(payload) !== 'auth')
|
|
156
157
|
throw new Error();
|
|
157
158
|
const state = await authenticateConnection(WebSocketAuthMessage.parse(payload));
|
|
159
|
+
if (this.meetsAdminRequirement({ client: ws, accountability: state.accountability }))
|
|
160
|
+
return;
|
|
158
161
|
ws.send(authenticationSuccess(payload['uid'], state.refresh_token));
|
|
159
162
|
this.server.emit('connection', ws, state);
|
|
160
163
|
}
|
|
@@ -243,6 +246,8 @@ export default class SocketController {
|
|
|
243
246
|
async handleAuthRequest(client, message) {
|
|
244
247
|
try {
|
|
245
248
|
const { accountability, expires_at, refresh_token } = await authenticateConnection(message);
|
|
249
|
+
if (!this.meetsAdminRequirement({ client, accountability }))
|
|
250
|
+
return;
|
|
246
251
|
client.accountability = accountability;
|
|
247
252
|
client.expires_at = expires_at;
|
|
248
253
|
this.setTokenExpireTimer(client);
|
|
@@ -300,6 +305,20 @@ export default class SocketController {
|
|
|
300
305
|
}
|
|
301
306
|
}, TOKEN_CHECK_INTERVAL);
|
|
302
307
|
}
|
|
308
|
+
meetsAdminRequirement({ socket, client, accountability, }) {
|
|
309
|
+
if (!this.authentication.requireAdmin || accountability?.admin)
|
|
310
|
+
return true;
|
|
311
|
+
logger.debug('WebSocket connection denied - ' + JSON.stringify(accountability || 'invalid'));
|
|
312
|
+
if (socket) {
|
|
313
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
314
|
+
socket.destroy();
|
|
315
|
+
}
|
|
316
|
+
else if (client) {
|
|
317
|
+
handleWebSocketError(client, new WebSocketError('auth', 'UNAUTHORIZED', 'Unauthorized.'), 'auth');
|
|
318
|
+
client.close();
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
303
322
|
terminate() {
|
|
304
323
|
if (this.authInterval)
|
|
305
324
|
clearInterval(this.authInterval);
|
|
@@ -9,11 +9,13 @@ import { handleWebSocketError } from '../errors.js';
|
|
|
9
9
|
import { ConnectionParams, WebSocketMessage } from '../messages.js';
|
|
10
10
|
import { getMessageType } from '../utils/message.js';
|
|
11
11
|
import SocketController from './base.js';
|
|
12
|
+
import { registerWebSocketEvents } from './hooks.js';
|
|
12
13
|
const logger = useLogger();
|
|
13
14
|
export class GraphQLSubscriptionController extends SocketController {
|
|
14
15
|
gql;
|
|
15
16
|
constructor(httpServer) {
|
|
16
17
|
super(httpServer, 'WEBSOCKETS_GRAPHQL');
|
|
18
|
+
registerWebSocketEvents();
|
|
17
19
|
this.server.on('connection', (ws, auth) => {
|
|
18
20
|
this.bindEvents(this.createClient(ws, auth));
|
|
19
21
|
});
|
|
@@ -30,7 +32,7 @@ export class GraphQLSubscriptionController extends SocketController {
|
|
|
30
32
|
},
|
|
31
33
|
});
|
|
32
34
|
bindPubSub();
|
|
33
|
-
logger.info(`GraphQL Subscriptions started at
|
|
35
|
+
logger.info(`GraphQL Subscriptions started at ${getAddress(httpServer)}${this.endpoint}`);
|
|
34
36
|
}
|
|
35
37
|
bindEvents(client) {
|
|
36
38
|
const closedHandler = this.gql.opened({
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
/// <reference types="pino-http" />
|
|
4
4
|
import type { Server as httpServer } from 'http';
|
|
5
5
|
import { GraphQLSubscriptionController } from './graphql.js';
|
|
6
|
+
import { LogsController } from './logs.js';
|
|
6
7
|
import { WebSocketController } from './rest.js';
|
|
7
8
|
export declare function createWebSocketController(server: httpServer): void;
|
|
8
9
|
export declare function getWebSocketController(): WebSocketController | undefined;
|
|
9
10
|
export declare function createSubscriptionController(server: httpServer): void;
|
|
10
11
|
export declare function getSubscriptionController(): GraphQLSubscriptionController | undefined;
|
|
12
|
+
export declare function createLogsController(server: httpServer): void;
|
|
13
|
+
export declare function getLogsController(): LogsController | undefined;
|
|
11
14
|
export * from './graphql.js';
|
|
15
|
+
export * from './logs.js';
|
|
12
16
|
export * from './rest.js';
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { toBoolean } from '@directus/utils';
|
|
3
3
|
import { GraphQLSubscriptionController } from './graphql.js';
|
|
4
|
+
import { LogsController } from './logs.js';
|
|
4
5
|
import { WebSocketController } from './rest.js';
|
|
5
6
|
let websocketController;
|
|
6
7
|
let subscriptionController;
|
|
8
|
+
let logsController;
|
|
7
9
|
export function createWebSocketController(server) {
|
|
8
10
|
const env = useEnv();
|
|
9
11
|
if (toBoolean(env['WEBSOCKETS_REST_ENABLED'])) {
|
|
@@ -22,5 +24,15 @@ export function createSubscriptionController(server) {
|
|
|
22
24
|
export function getSubscriptionController() {
|
|
23
25
|
return subscriptionController;
|
|
24
26
|
}
|
|
27
|
+
export function createLogsController(server) {
|
|
28
|
+
const env = useEnv();
|
|
29
|
+
if (toBoolean(env['WEBSOCKETS_LOGS_ENABLED'])) {
|
|
30
|
+
logsController = new LogsController(server);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function getLogsController() {
|
|
34
|
+
return logsController;
|
|
35
|
+
}
|
|
25
36
|
export * from './graphql.js';
|
|
37
|
+
export * from './logs.js';
|
|
26
38
|
export * from './rest.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="node/http.js" />
|
|
3
|
+
/// <reference types="pino-http" />
|
|
4
|
+
import type { Server as httpServer } from 'http';
|
|
5
|
+
import SocketController from './base.js';
|
|
6
|
+
export declare class LogsController extends SocketController {
|
|
7
|
+
constructor(httpServer: httpServer);
|
|
8
|
+
getEnvironmentConfig(configPrefix: string): {
|
|
9
|
+
endpoint: string;
|
|
10
|
+
maxConnections: number;
|
|
11
|
+
authentication: {
|
|
12
|
+
mode: "strict" | "public" | "handshake";
|
|
13
|
+
timeout: number;
|
|
14
|
+
requireAdmin: boolean;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
private bindEvents;
|
|
18
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import emitter from '../../emitter.js';
|
|
3
|
+
import { useLogger } from '../../logger/index.js';
|
|
4
|
+
import { handleWebSocketError } from '../errors.js';
|
|
5
|
+
import { AuthMode, WebSocketMessage } from '../messages.js';
|
|
6
|
+
import SocketController from './base.js';
|
|
7
|
+
const logger = useLogger();
|
|
8
|
+
export class LogsController extends SocketController {
|
|
9
|
+
constructor(httpServer) {
|
|
10
|
+
super(httpServer, 'WEBSOCKETS_LOGS');
|
|
11
|
+
const env = useEnv();
|
|
12
|
+
this.server.on('connection', (ws, auth) => {
|
|
13
|
+
this.bindEvents(this.createClient(ws, auth));
|
|
14
|
+
});
|
|
15
|
+
logger.info(`Logs WebSocket Server started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
|
|
16
|
+
}
|
|
17
|
+
getEnvironmentConfig(configPrefix) {
|
|
18
|
+
const env = useEnv();
|
|
19
|
+
const endpoint = String(env[`${configPrefix}_PATH`]);
|
|
20
|
+
const maxConnections = `${configPrefix}_CONN_LIMIT` in env ? Number(env[`${configPrefix}_CONN_LIMIT`]) : Number.POSITIVE_INFINITY;
|
|
21
|
+
return {
|
|
22
|
+
endpoint,
|
|
23
|
+
maxConnections,
|
|
24
|
+
// require strict auth
|
|
25
|
+
authentication: {
|
|
26
|
+
mode: 'strict',
|
|
27
|
+
timeout: 0,
|
|
28
|
+
requireAdmin: true,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
bindEvents(client) {
|
|
33
|
+
client.on('parsed-message', async (message) => {
|
|
34
|
+
try {
|
|
35
|
+
emitter.emitAction('websocket.logs', { message, client });
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
handleWebSocketError(client, error, 'server');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
client.on('error', (event) => {
|
|
43
|
+
emitter.emitAction('websocket.error', { client, event });
|
|
44
|
+
});
|
|
45
|
+
client.on('close', (event) => {
|
|
46
|
+
emitter.emitAction('websocket.close', { client, event });
|
|
47
|
+
});
|
|
48
|
+
emitter.emitAction('websocket.connect', { client });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -5,14 +5,16 @@ import { getAddress } from '../../utils/get-address.js';
|
|
|
5
5
|
import { WebSocketError, handleWebSocketError } from '../errors.js';
|
|
6
6
|
import { WebSocketMessage } from '../messages.js';
|
|
7
7
|
import SocketController from './base.js';
|
|
8
|
+
import { registerWebSocketEvents } from './hooks.js';
|
|
8
9
|
const logger = useLogger();
|
|
9
10
|
export class WebSocketController extends SocketController {
|
|
10
11
|
constructor(httpServer) {
|
|
11
12
|
super(httpServer, 'WEBSOCKETS_REST');
|
|
13
|
+
registerWebSocketEvents();
|
|
12
14
|
this.server.on('connection', (ws, auth) => {
|
|
13
15
|
this.bindEvents(this.createClient(ws, auth));
|
|
14
16
|
});
|
|
15
|
-
logger.info(`WebSocket Server started at
|
|
17
|
+
logger.info(`WebSocket Server started at ${getAddress(httpServer)}${this.endpoint}`);
|
|
16
18
|
}
|
|
17
19
|
bindEvents(client) {
|
|
18
20
|
client.on('parsed-message', async (message) => {
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toBoolean } from '@directus/utils';
|
|
1
3
|
import { HeartbeatHandler } from './heartbeat.js';
|
|
2
4
|
import { ItemsHandler } from './items.js';
|
|
5
|
+
import { LogsHandler } from './logs.js';
|
|
3
6
|
import { SubscribeHandler } from './subscribe.js';
|
|
4
7
|
export function startWebSocketHandlers() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
const env = useEnv();
|
|
9
|
+
const heartbeatEnabled = toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED']);
|
|
10
|
+
const restEnabled = toBoolean(env['WEBSOCKETS_REST_ENABLED']);
|
|
11
|
+
const graphqlEnabled = toBoolean(env['WEBSOCKETS_GRAPHQL_ENABLED']);
|
|
12
|
+
const logsEnabled = toBoolean(env['WEBSOCKETS_LOGS_ENABLED']);
|
|
13
|
+
if (heartbeatEnabled) {
|
|
14
|
+
new HeartbeatHandler();
|
|
15
|
+
}
|
|
16
|
+
if (restEnabled || graphqlEnabled) {
|
|
17
|
+
new ItemsHandler();
|
|
18
|
+
}
|
|
19
|
+
if (restEnabled) {
|
|
20
|
+
new SubscribeHandler();
|
|
21
|
+
}
|
|
22
|
+
if (logsEnabled) {
|
|
23
|
+
new LogsHandler();
|
|
24
|
+
}
|
|
8
25
|
}
|
|
9
26
|
export * from './heartbeat.js';
|
|
10
27
|
export * from './items.js';
|
|
28
|
+
export * from './logs.js';
|
|
11
29
|
export * from './subscribe.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Bus } from '@directus/memory';
|
|
2
|
+
import { LogsController } from '../controllers/index.js';
|
|
3
|
+
import { WebSocketLogsMessage } from '../messages.js';
|
|
4
|
+
import type { LogsSubscription, WebSocketClient } from '../types.js';
|
|
5
|
+
export declare class LogsHandler {
|
|
6
|
+
controller: LogsController;
|
|
7
|
+
messenger: Bus;
|
|
8
|
+
availableLogLevels: string[];
|
|
9
|
+
logLevelValueMap: Record<string, string>;
|
|
10
|
+
subscriptions: LogsSubscription;
|
|
11
|
+
constructor(controller?: LogsController);
|
|
12
|
+
/**
|
|
13
|
+
* Hook into websocket client lifecycle events
|
|
14
|
+
*/
|
|
15
|
+
bindWebSocket(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Register a logs subscription
|
|
18
|
+
* @param logLevel
|
|
19
|
+
* @param client
|
|
20
|
+
*/
|
|
21
|
+
subscribe(logLevel: string, client: WebSocketClient): void;
|
|
22
|
+
/**
|
|
23
|
+
* Remove a logs subscription
|
|
24
|
+
* @param client WebSocketClient
|
|
25
|
+
*/
|
|
26
|
+
unsubscribe(client: WebSocketClient): void;
|
|
27
|
+
/**
|
|
28
|
+
* Handle incoming (un)subscribe requests
|
|
29
|
+
*/
|
|
30
|
+
onMessage(client: WebSocketClient, message: WebSocketLogsMessage): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ErrorCode, ServiceUnavailableError } from '@directus/errors';
|
|
2
|
+
import { useBus } from '../../bus/index.js';
|
|
3
|
+
import emitter from '../../emitter.js';
|
|
4
|
+
import { useLogger } from '../../logger/index.js';
|
|
5
|
+
import { getAllowedLogLevels } from '../../utils/get-allowed-log-levels.js';
|
|
6
|
+
import { getLogsController, LogsController } from '../controllers/index.js';
|
|
7
|
+
import { handleWebSocketError, WebSocketError } from '../errors.js';
|
|
8
|
+
import { WebSocketLogsMessage } from '../messages.js';
|
|
9
|
+
import { fmtMessage, getMessageType } from '../utils/message.js';
|
|
10
|
+
const logger = useLogger();
|
|
11
|
+
export class LogsHandler {
|
|
12
|
+
controller;
|
|
13
|
+
messenger;
|
|
14
|
+
availableLogLevels;
|
|
15
|
+
logLevelValueMap;
|
|
16
|
+
subscriptions;
|
|
17
|
+
constructor(controller) {
|
|
18
|
+
controller = controller ?? getLogsController();
|
|
19
|
+
if (!controller) {
|
|
20
|
+
throw new ServiceUnavailableError({ service: 'ws', reason: 'WebSocket server is not initialized' });
|
|
21
|
+
}
|
|
22
|
+
this.controller = controller;
|
|
23
|
+
this.messenger = useBus();
|
|
24
|
+
this.availableLogLevels = Object.keys(logger.levels.values);
|
|
25
|
+
this.logLevelValueMap = Object.fromEntries(Object.entries(logger.levels.values).map(([key, value]) => [value, key]));
|
|
26
|
+
this.subscriptions = this.availableLogLevels.reduce((acc, logLevel) => {
|
|
27
|
+
acc[logLevel] = new Set();
|
|
28
|
+
return acc;
|
|
29
|
+
}, {});
|
|
30
|
+
this.bindWebSocket();
|
|
31
|
+
this.messenger.subscribe('logs', (message) => {
|
|
32
|
+
const { log, nodeId } = JSON.parse(message);
|
|
33
|
+
const logLevel = this.logLevelValueMap[log['level']];
|
|
34
|
+
if (logLevel) {
|
|
35
|
+
this.subscriptions[logLevel]?.forEach((subscription) => subscription.send(fmtMessage('logs', { data: log }, nodeId)));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Hook into websocket client lifecycle events
|
|
41
|
+
*/
|
|
42
|
+
bindWebSocket() {
|
|
43
|
+
// listen to incoming messages on the connected websockets
|
|
44
|
+
emitter.onAction('websocket.logs', ({ client, message }) => {
|
|
45
|
+
if (!['subscribe', 'unsubscribe'].includes(getMessageType(message)))
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
const parsedMessage = WebSocketLogsMessage.parse(message);
|
|
49
|
+
this.onMessage(client, parsedMessage).catch((error) => {
|
|
50
|
+
// this catch is required because the async onMessage function is not awaited
|
|
51
|
+
handleWebSocketError(client, error, 'logs');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
handleWebSocketError(client, error, 'logs');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// unsubscribe when a connection drops
|
|
59
|
+
emitter.onAction('websocket.error', ({ client }) => this.unsubscribe(client));
|
|
60
|
+
emitter.onAction('websocket.close', ({ client }) => this.unsubscribe(client));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Register a logs subscription
|
|
64
|
+
* @param logLevel
|
|
65
|
+
* @param client
|
|
66
|
+
*/
|
|
67
|
+
subscribe(logLevel, client) {
|
|
68
|
+
let allowedLogLevelNames = [];
|
|
69
|
+
try {
|
|
70
|
+
allowedLogLevelNames = Object.keys(getAllowedLogLevels(logLevel));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new WebSocketError('logs', ErrorCode.InvalidPayload, error.message);
|
|
74
|
+
}
|
|
75
|
+
for (const availableLogLevel of this.availableLogLevels) {
|
|
76
|
+
if (allowedLogLevelNames.includes(availableLogLevel)) {
|
|
77
|
+
this.subscriptions[availableLogLevel]?.add(client);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.subscriptions[availableLogLevel]?.delete(client);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Remove a logs subscription
|
|
86
|
+
* @param client WebSocketClient
|
|
87
|
+
*/
|
|
88
|
+
unsubscribe(client) {
|
|
89
|
+
for (const availableLogLevel of this.availableLogLevels) {
|
|
90
|
+
this.subscriptions[availableLogLevel]?.delete(client);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handle incoming (un)subscribe requests
|
|
95
|
+
*/
|
|
96
|
+
async onMessage(client, message) {
|
|
97
|
+
const accountability = client.accountability;
|
|
98
|
+
if (!accountability?.admin) {
|
|
99
|
+
throw new WebSocketError('logs', ErrorCode.Forbidden, `You don't have permission to access this.`);
|
|
100
|
+
}
|
|
101
|
+
if (message.type === 'subscribe') {
|
|
102
|
+
try {
|
|
103
|
+
const logLevel = message.log_level;
|
|
104
|
+
this.subscribe(logLevel, client);
|
|
105
|
+
client.send(fmtMessage('logs', { event: 'subscribe', log_level: logLevel }));
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
handleWebSocketError(client, err, 'subscribe');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (message.type === 'unsubscribe') {
|
|
112
|
+
try {
|
|
113
|
+
this.unsubscribe(client);
|
|
114
|
+
client.send(fmtMessage('logs', { event: 'unsubscribe' }));
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
handleWebSocketError(client, err, 'unsubscribe');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -186,6 +186,32 @@ export declare const WebSocketSubscribeMessage: z.ZodDiscriminatedUnion<"type",
|
|
|
186
186
|
type: z.ZodLiteral<"unsubscribe">;
|
|
187
187
|
}>, z.ZodTypeAny, "passthrough">>]>;
|
|
188
188
|
export type WebSocketSubscribeMessage = z.infer<typeof WebSocketSubscribeMessage>;
|
|
189
|
+
export declare const WebSocketLogsMessage: z.ZodUnion<[z.ZodObject<{
|
|
190
|
+
type: z.ZodLiteral<"subscribe">;
|
|
191
|
+
log_level: z.ZodString;
|
|
192
|
+
}, "strip", z.ZodTypeAny, {
|
|
193
|
+
type: "subscribe";
|
|
194
|
+
log_level: string;
|
|
195
|
+
}, {
|
|
196
|
+
type: "subscribe";
|
|
197
|
+
log_level: string;
|
|
198
|
+
}>, z.ZodObject<z.objectUtil.extendShape<{
|
|
199
|
+
type: z.ZodString;
|
|
200
|
+
uid: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
201
|
+
}, {
|
|
202
|
+
type: z.ZodLiteral<"unsubscribe">;
|
|
203
|
+
}>, "passthrough", z.ZodTypeAny, z.objectOutputType<z.objectUtil.extendShape<{
|
|
204
|
+
type: z.ZodString;
|
|
205
|
+
uid: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
206
|
+
}, {
|
|
207
|
+
type: z.ZodLiteral<"unsubscribe">;
|
|
208
|
+
}>, z.ZodTypeAny, "passthrough">, z.objectInputType<z.objectUtil.extendShape<{
|
|
209
|
+
type: z.ZodString;
|
|
210
|
+
uid: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
211
|
+
}, {
|
|
212
|
+
type: z.ZodLiteral<"unsubscribe">;
|
|
213
|
+
}>, z.ZodTypeAny, "passthrough">>]>;
|
|
214
|
+
export type WebSocketLogsMessage = z.infer<typeof WebSocketLogsMessage>;
|
|
189
215
|
export declare const WebSocketItemsMessage: z.ZodUnion<[z.ZodObject<z.objectUtil.extendShape<{
|
|
190
216
|
uid: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
191
217
|
type: z.ZodLiteral<"items">;
|
|
@@ -41,6 +41,15 @@ export const WebSocketSubscribeMessage = z.discriminatedUnion('type', [
|
|
|
41
41
|
type: z.literal('unsubscribe'),
|
|
42
42
|
}),
|
|
43
43
|
]);
|
|
44
|
+
export const WebSocketLogsMessage = z.union([
|
|
45
|
+
z.object({
|
|
46
|
+
type: z.literal('subscribe'),
|
|
47
|
+
log_level: z.string(),
|
|
48
|
+
}),
|
|
49
|
+
WebSocketMessage.extend({
|
|
50
|
+
type: z.literal('unsubscribe'),
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
44
53
|
const ZodItem = z.custom();
|
|
45
54
|
const PartialItemsMessage = z.object({
|
|
46
55
|
uid: zodStringOrNumber.optional(),
|
|
@@ -7,6 +7,7 @@ import type { Accountability, Query } from '@directus/types';
|
|
|
7
7
|
import type { IncomingMessage } from 'http';
|
|
8
8
|
import type internal from 'stream';
|
|
9
9
|
import type { WebSocket } from 'ws';
|
|
10
|
+
import type { AuthMode } from './messages.js';
|
|
10
11
|
export type AuthenticationState = {
|
|
11
12
|
accountability: Accountability | null;
|
|
12
13
|
expires_at: number | null;
|
|
@@ -17,6 +18,11 @@ export type WebSocketClient = WebSocket & AuthenticationState & {
|
|
|
17
18
|
auth_timer: NodeJS.Timeout | null;
|
|
18
19
|
};
|
|
19
20
|
export type UpgradeRequest = IncomingMessage & AuthenticationState;
|
|
21
|
+
export type WebSocketAuthentication = {
|
|
22
|
+
mode: AuthMode;
|
|
23
|
+
timeout: number;
|
|
24
|
+
requireAdmin: boolean;
|
|
25
|
+
};
|
|
20
26
|
export type SubscriptionEvent = 'create' | 'update' | 'delete';
|
|
21
27
|
export type Subscription = {
|
|
22
28
|
uid?: string | number;
|
|
@@ -34,3 +40,4 @@ export type UpgradeContext = {
|
|
|
34
40
|
export type GraphQLSocket = {
|
|
35
41
|
client: WebSocketClient;
|
|
36
42
|
};
|
|
43
|
+
export type LogsSubscription = Record<string, Set<WebSocketClient>>;
|