@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,21 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
4
|
-
/// <reference types="node/http.js" />
|
|
5
|
-
/// <reference types="pino-http" />
|
|
1
|
+
import type { Accountability } from '@directus/types';
|
|
6
2
|
import type { IncomingMessage, Server as httpServer } from 'http';
|
|
7
3
|
import type { RateLimiterAbstract } from 'rate-limiter-flexible';
|
|
8
4
|
import type internal from 'stream';
|
|
9
5
|
import WebSocket from 'ws';
|
|
10
|
-
import {
|
|
11
|
-
import type { AuthenticationState, UpgradeContext, WebSocketClient } from '../types.js';
|
|
6
|
+
import { WebSocketAuthMessage, WebSocketMessage } from '../messages.js';
|
|
7
|
+
import type { AuthenticationState, UpgradeContext, WebSocketAuthentication, WebSocketClient } from '../types.js';
|
|
12
8
|
export default abstract class SocketController {
|
|
13
9
|
server: WebSocket.Server;
|
|
14
10
|
clients: Set<WebSocketClient>;
|
|
15
|
-
authentication:
|
|
16
|
-
mode: AuthMode;
|
|
17
|
-
timeout: number;
|
|
18
|
-
};
|
|
11
|
+
authentication: WebSocketAuthentication;
|
|
19
12
|
endpoint: string;
|
|
20
13
|
maxConnections: number;
|
|
21
14
|
private rateLimiter;
|
|
@@ -23,10 +16,7 @@ export default abstract class SocketController {
|
|
|
23
16
|
constructor(httpServer: httpServer, configPrefix: string);
|
|
24
17
|
protected getEnvironmentConfig(configPrefix: string): {
|
|
25
18
|
endpoint: string;
|
|
26
|
-
authentication:
|
|
27
|
-
mode: AuthMode;
|
|
28
|
-
timeout: number;
|
|
29
|
-
};
|
|
19
|
+
authentication: WebSocketAuthentication;
|
|
30
20
|
maxConnections: number;
|
|
31
21
|
};
|
|
32
22
|
protected getRateLimiter(): RateLimiterAbstract | null;
|
|
@@ -37,6 +27,7 @@ export default abstract class SocketController {
|
|
|
37
27
|
createClient(ws: WebSocket, { accountability, expires_at }: AuthenticationState): WebSocketClient;
|
|
38
28
|
protected parseMessage(data: string): WebSocketMessage;
|
|
39
29
|
protected handleAuthRequest(client: WebSocketClient, message: WebSocketAuthMessage): Promise<void>;
|
|
30
|
+
protected checkUserRequirements(_accountability: Accountability | null): void;
|
|
40
31
|
setTokenExpireTimer(client: WebSocketClient): void;
|
|
41
32
|
checkClientTokens(): void;
|
|
42
33
|
terminate(): void;
|
|
@@ -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();
|
|
@@ -141,6 +139,15 @@ export default class SocketController {
|
|
|
141
139
|
socket.destroy();
|
|
142
140
|
return;
|
|
143
141
|
}
|
|
142
|
+
try {
|
|
143
|
+
this.checkUserRequirements(accountability);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
logger.debug('WebSocket upgrade denied - ' + JSON.stringify(accountability || 'invalid'));
|
|
147
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
148
|
+
socket.destroy();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
144
151
|
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
145
152
|
this.catchInvalidMessages(ws);
|
|
146
153
|
const state = { accountability, expires_at };
|
|
@@ -155,6 +162,7 @@ export default class SocketController {
|
|
|
155
162
|
if (getMessageType(payload) !== 'auth')
|
|
156
163
|
throw new Error();
|
|
157
164
|
const state = await authenticateConnection(WebSocketAuthMessage.parse(payload));
|
|
165
|
+
this.checkUserRequirements(state.accountability);
|
|
158
166
|
ws.send(authenticationSuccess(payload['uid'], state.refresh_token));
|
|
159
167
|
this.server.emit('connection', ws, state);
|
|
160
168
|
}
|
|
@@ -235,7 +243,7 @@ export default class SocketController {
|
|
|
235
243
|
try {
|
|
236
244
|
message = WebSocketMessage.parse(parseJSON(data));
|
|
237
245
|
}
|
|
238
|
-
catch
|
|
246
|
+
catch {
|
|
239
247
|
throw new WebSocketError('server', 'INVALID_PAYLOAD', 'Unable to parse the incoming message.');
|
|
240
248
|
}
|
|
241
249
|
return message;
|
|
@@ -243,6 +251,7 @@ export default class SocketController {
|
|
|
243
251
|
async handleAuthRequest(client, message) {
|
|
244
252
|
try {
|
|
245
253
|
const { accountability, expires_at, refresh_token } = await authenticateConnection(message);
|
|
254
|
+
this.checkUserRequirements(accountability);
|
|
246
255
|
client.accountability = accountability;
|
|
247
256
|
client.expires_at = expires_at;
|
|
248
257
|
this.setTokenExpireTimer(client);
|
|
@@ -264,6 +273,10 @@ export default class SocketController {
|
|
|
264
273
|
}
|
|
265
274
|
}
|
|
266
275
|
}
|
|
276
|
+
checkUserRequirements(_accountability) {
|
|
277
|
+
// there are no requirements in the abstract class
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
267
280
|
setTokenExpireTimer(client) {
|
|
268
281
|
if (client.auth_timer !== null) {
|
|
269
282
|
// clear up old timeouts if needed
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node/http.js" />
|
|
3
|
-
/// <reference types="pino-http" />
|
|
4
1
|
import type { Server } from 'graphql-ws';
|
|
5
2
|
import type { Server as httpServer } from 'http';
|
|
6
3
|
import type { GraphQLSocket, UpgradeContext, WebSocketClient } from '../types.js';
|
|
@@ -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({
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node/http.js" />
|
|
3
|
-
/// <reference types="pino-http" />
|
|
4
1
|
import type { Server as httpServer } from 'http';
|
|
5
2
|
import { GraphQLSubscriptionController } from './graphql.js';
|
|
3
|
+
import { LogsController } from './logs.js';
|
|
6
4
|
import { WebSocketController } from './rest.js';
|
|
7
5
|
export declare function createWebSocketController(server: httpServer): void;
|
|
8
6
|
export declare function getWebSocketController(): WebSocketController | undefined;
|
|
9
7
|
export declare function createSubscriptionController(server: httpServer): void;
|
|
10
8
|
export declare function getSubscriptionController(): GraphQLSubscriptionController | undefined;
|
|
9
|
+
export declare function createLogsController(server: httpServer): void;
|
|
10
|
+
export declare function getLogsController(): LogsController | undefined;
|
|
11
11
|
export * from './graphql.js';
|
|
12
|
+
export * from './logs.js';
|
|
12
13
|
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,17 @@
|
|
|
1
|
+
import type { Server as httpServer } from 'http';
|
|
2
|
+
import { AuthMode } from '../messages.js';
|
|
3
|
+
import SocketController from './base.js';
|
|
4
|
+
import type { Accountability } from '@directus/types';
|
|
5
|
+
export declare class LogsController extends SocketController {
|
|
6
|
+
constructor(httpServer: httpServer);
|
|
7
|
+
getEnvironmentConfig(configPrefix: string): {
|
|
8
|
+
endpoint: string;
|
|
9
|
+
maxConnections: number;
|
|
10
|
+
authentication: {
|
|
11
|
+
mode: AuthMode;
|
|
12
|
+
timeout: number;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
private bindEvents;
|
|
16
|
+
protected checkUserRequirements(accountability: Accountability | null): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import emitter from '../../emitter.js';
|
|
3
|
+
import { useLogger } from '../../logger/index.js';
|
|
4
|
+
import { handleWebSocketError, WebSocketError } 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
|
+
authentication: {
|
|
25
|
+
mode: 'strict',
|
|
26
|
+
timeout: 0,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
bindEvents(client) {
|
|
31
|
+
client.on('parsed-message', async (message) => {
|
|
32
|
+
try {
|
|
33
|
+
emitter.emitAction('websocket.logs', { message, client });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
handleWebSocketError(client, error, 'server');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
client.on('error', (event) => {
|
|
41
|
+
emitter.emitAction('websocket.error', { client, event });
|
|
42
|
+
});
|
|
43
|
+
client.on('close', (event) => {
|
|
44
|
+
emitter.emitAction('websocket.close', { client, event });
|
|
45
|
+
});
|
|
46
|
+
emitter.emitAction('websocket.connect', { client });
|
|
47
|
+
}
|
|
48
|
+
checkUserRequirements(accountability) {
|
|
49
|
+
// enforce admin only access for the logs streaming websocket
|
|
50
|
+
if (!accountability?.admin) {
|
|
51
|
+
throw new WebSocketError('auth', 'AUTH_FAILED', 'Unauthorized access.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node/http.js" />
|
|
3
|
-
/// <reference types="pino-http" />
|
|
4
1
|
import type { Server as httpServer } from 'http';
|
|
5
2
|
import { WebSocketMessage } from '../messages.js';
|
|
6
3
|
import SocketController from './base.js';
|
|
@@ -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) => {
|
|
@@ -38,7 +40,7 @@ export class WebSocketController extends SocketController {
|
|
|
38
40
|
try {
|
|
39
41
|
message = parseJSON(data);
|
|
40
42
|
}
|
|
41
|
-
catch
|
|
43
|
+
catch {
|
|
42
44
|
throw new WebSocketError('server', 'INVALID_PAYLOAD', 'Unable to parse the incoming message.');
|
|
43
45
|
}
|
|
44
46
|
return 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(),
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
4
|
-
/// <reference types="node/http.js" />
|
|
5
|
-
/// <reference types="pino-http" />
|
|
6
1
|
import type { Accountability, Query } from '@directus/types';
|
|
7
2
|
import type { IncomingMessage } from 'http';
|
|
8
3
|
import type internal from 'stream';
|
|
9
4
|
import type { WebSocket } from 'ws';
|
|
5
|
+
import type { AuthMode } from './messages.js';
|
|
10
6
|
export type AuthenticationState = {
|
|
11
7
|
accountability: Accountability | null;
|
|
12
8
|
expires_at: number | null;
|
|
@@ -17,6 +13,10 @@ export type WebSocketClient = WebSocket & AuthenticationState & {
|
|
|
17
13
|
auth_timer: NodeJS.Timeout | null;
|
|
18
14
|
};
|
|
19
15
|
export type UpgradeRequest = IncomingMessage & AuthenticationState;
|
|
16
|
+
export type WebSocketAuthentication = {
|
|
17
|
+
mode: AuthMode;
|
|
18
|
+
timeout: number;
|
|
19
|
+
};
|
|
20
20
|
export type SubscriptionEvent = 'create' | 'update' | 'delete';
|
|
21
21
|
export type Subscription = {
|
|
22
22
|
uid?: string | number;
|
|
@@ -34,3 +34,4 @@ export type UpgradeContext = {
|
|
|
34
34
|
export type GraphQLSocket = {
|
|
35
35
|
client: WebSocketClient;
|
|
36
36
|
};
|
|
37
|
+
export type LogsSubscription = Record<string, Set<WebSocketClient>>;
|