✨(y-provider) add endpoint returning document connection state
We need a new endpoint in the y-provider server allowing the backend to retrieve the number of active connections on a document and if a session key exists.
This commit is contained in:
@@ -0,0 +1,275 @@
|
|||||||
|
import request from 'supertest';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('../src/env', async (importOriginal) => {
|
||||||
|
return {
|
||||||
|
...(await importOriginal()),
|
||||||
|
PORT: 5556,
|
||||||
|
COLLABORATION_SERVER_ORIGIN: 'http://localhost:3000',
|
||||||
|
COLLABORATION_SERVER_SECRET: 'test-secret-api-key',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error = vi.fn();
|
||||||
|
|
||||||
|
import { COLLABORATION_SERVER_ORIGIN as origin } from '@/env';
|
||||||
|
import { hocuspocusServer, initApp } from '@/servers';
|
||||||
|
|
||||||
|
const apiEndpoint = '/collaboration/api/get-connections/';
|
||||||
|
|
||||||
|
describe('Server Tests', () => {
|
||||||
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] with incorrect API key should return 403', async () => {
|
||||||
|
const app = initApp();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`${apiEndpoint}?room=test-room`)
|
||||||
|
.set('Origin', origin)
|
||||||
|
.set('Authorization', 'wrong-api-key');
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(response.body.error).toBe('Unauthorized: Invalid API Key');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] failed if room not indicated', async () => {
|
||||||
|
const app = initApp();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`${apiEndpoint}`)
|
||||||
|
.set('Origin', origin)
|
||||||
|
.set('Authorization', 'test-secret-api-key')
|
||||||
|
.send({ document_id: 'test-document' });
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toBe('Room name not provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] failed if session key not indicated', async () => {
|
||||||
|
const app = initApp();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`${apiEndpoint}?room=test-room`)
|
||||||
|
.set('Origin', origin)
|
||||||
|
.set('Authorization', 'test-secret-api-key')
|
||||||
|
.send({ document_id: 'test-document' });
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toBe('Session key not provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] return a 404 if room not found', async () => {
|
||||||
|
const app = initApp();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`${apiEndpoint}?room=test-room&sessionKey=test-session-key`)
|
||||||
|
.set('Origin', origin)
|
||||||
|
.set('Authorization', 'test-secret-api-key');
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(response.body.error).toBe('Room not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key existing', async () => {
|
||||||
|
const document = await hocuspocusServer.createDocument(
|
||||||
|
'test-room',
|
||||||
|
{},
|
||||||
|
uuid(),
|
||||||
|
{ isAuthenticated: true, readOnly: false, requiresAuthentication: true },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 1,
|
||||||
|
context: { sessionKey: 'test-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 2,
|
||||||
|
context: { sessionKey: 'other-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 3,
|
||||||
|
context: { sessionKey: 'last-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 4,
|
||||||
|
context: { sessionKey: 'session-read-only' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: true,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const app = initApp();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`${apiEndpoint}?room=test-room&sessionKey=test-session-key`)
|
||||||
|
.set('Origin', origin)
|
||||||
|
.set('Authorization', 'test-secret-api-key');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 3,
|
||||||
|
exists: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key not existing', async () => {
|
||||||
|
const document = await hocuspocusServer.createDocument(
|
||||||
|
'test-room',
|
||||||
|
{},
|
||||||
|
uuid(),
|
||||||
|
{ isAuthenticated: true, readOnly: false, requiresAuthentication: true },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 1,
|
||||||
|
context: { sessionKey: 'test-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 2,
|
||||||
|
context: { sessionKey: 'other-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 3,
|
||||||
|
context: { sessionKey: 'last-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 4,
|
||||||
|
context: { sessionKey: 'session-read-only' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: true,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const app = initApp();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`${apiEndpoint}?room=test-room&sessionKey=non-existing-session-key`)
|
||||||
|
.set('Origin', origin)
|
||||||
|
.set('Authorization', 'test-secret-api-key');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 3,
|
||||||
|
exists: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key not existing, read only connection', async () => {
|
||||||
|
const document = await hocuspocusServer.createDocument(
|
||||||
|
'test-room',
|
||||||
|
{},
|
||||||
|
uuid(),
|
||||||
|
{ isAuthenticated: true, readOnly: false, requiresAuthentication: true },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 1,
|
||||||
|
context: { sessionKey: 'test-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 2,
|
||||||
|
context: { sessionKey: 'other-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 3,
|
||||||
|
context: { sessionKey: 'last-session-key' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: false,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
document.addConnection({
|
||||||
|
webSocket: 4,
|
||||||
|
context: { sessionKey: 'session-read-only' },
|
||||||
|
document: document,
|
||||||
|
pongReceived: false,
|
||||||
|
readOnly: true,
|
||||||
|
request: null,
|
||||||
|
timeout: 0,
|
||||||
|
socketId: uuid(),
|
||||||
|
lock: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const app = initApp();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`${apiEndpoint}?room=test-room&sessionKey=session-read-only`)
|
||||||
|
.set('Origin', origin)
|
||||||
|
.set('Authorization', 'test-secret-api-key');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 3,
|
||||||
|
exists: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { hocuspocusServer } from '@/servers';
|
||||||
|
import { logger } from '@/utils';
|
||||||
|
|
||||||
|
type getDocumentConnectionInfoRequestQuery = {
|
||||||
|
room?: string;
|
||||||
|
sessionKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDocumentConnectionInfoHandler = (
|
||||||
|
req: Request<object, object, object, getDocumentConnectionInfoRequestQuery>,
|
||||||
|
res: Response,
|
||||||
|
) => {
|
||||||
|
const room = req.query.room;
|
||||||
|
const sessionKey = req.query.sessionKey;
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
res.status(400).json({ error: 'Room name not provided' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.query.sessionKey) {
|
||||||
|
res.status(400).json({ error: 'Session key not provided' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('Getting document connection info for room:', room);
|
||||||
|
|
||||||
|
const roomInfo = hocuspocusServer.documents.get(room);
|
||||||
|
|
||||||
|
if (!roomInfo) {
|
||||||
|
logger('Room not found:', room);
|
||||||
|
res.status(404).json({ error: 'Room not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const connections = roomInfo
|
||||||
|
.getConnections()
|
||||||
|
.filter((connection) => connection.readOnly === false);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
count: connections.length,
|
||||||
|
exists: connections.some(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
(connection) => connection.context.sessionKey === sessionKey,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './collaborationResetConnectionsHandler';
|
export * from './collaborationResetConnectionsHandler';
|
||||||
export * from './collaborationWSHandler';
|
export * from './collaborationWSHandler';
|
||||||
export * from './convertHandler';
|
export * from './convertHandler';
|
||||||
|
export * from './getDocumentConnectionInfoHandler';
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export const routes = {
|
|||||||
COLLABORATION_WS: '/collaboration/ws/',
|
COLLABORATION_WS: '/collaboration/ws/',
|
||||||
COLLABORATION_RESET_CONNECTIONS: '/collaboration/api/reset-connections/',
|
COLLABORATION_RESET_CONNECTIONS: '/collaboration/api/reset-connections/',
|
||||||
CONVERT: '/api/convert/',
|
CONVERT: '/api/convert/',
|
||||||
|
COLLABORATION_GET_CONNECTIONS: '/collaboration/api/get-connections/',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
collaborationResetConnectionsHandler,
|
collaborationResetConnectionsHandler,
|
||||||
collaborationWSHandler,
|
collaborationWSHandler,
|
||||||
convertHandler,
|
convertHandler,
|
||||||
|
getDocumentConnectionInfoHandler,
|
||||||
} from '@/handlers';
|
} from '@/handlers';
|
||||||
import { corsMiddleware, httpSecurity, wsSecurity } from '@/middlewares';
|
import { corsMiddleware, httpSecurity, wsSecurity } from '@/middlewares';
|
||||||
import { routes } from '@/routes';
|
import { routes } from '@/routes';
|
||||||
@@ -41,6 +42,12 @@ export const initApp = () => {
|
|||||||
collaborationResetConnectionsHandler,
|
collaborationResetConnectionsHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
routes.COLLABORATION_GET_CONNECTIONS,
|
||||||
|
httpSecurity,
|
||||||
|
getDocumentConnectionInfoHandler,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to convert Markdown or BlockNote blocks
|
* Route to convert Markdown or BlockNote blocks
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ export const hocuspocusServer = Server.configure({
|
|||||||
|
|
||||||
connection.readOnly = !canEdit;
|
connection.readOnly = !canEdit;
|
||||||
|
|
||||||
|
const session = requestHeaders['cookie']
|
||||||
|
?.split('; ')
|
||||||
|
.find((cookie) => cookie.startsWith('docs_sessionid='));
|
||||||
|
if (session) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
context.sessionKey = session.split('=')[1];
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Unauthenticated users can be allowed to connect
|
* Unauthenticated users can be allowed to connect
|
||||||
* so we flag only authenticated users
|
* so we flag only authenticated users
|
||||||
|
|||||||
Reference in New Issue
Block a user