✨(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 './collaborationWSHandler';
|
||||
export * from './convertHandler';
|
||||
export * from './getDocumentConnectionInfoHandler';
|
||||
|
||||
@@ -2,4 +2,5 @@ export const routes = {
|
||||
COLLABORATION_WS: '/collaboration/ws/',
|
||||
COLLABORATION_RESET_CONNECTIONS: '/collaboration/api/reset-connections/',
|
||||
CONVERT: '/api/convert/',
|
||||
COLLABORATION_GET_CONNECTIONS: '/collaboration/api/get-connections/',
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
collaborationResetConnectionsHandler,
|
||||
collaborationWSHandler,
|
||||
convertHandler,
|
||||
getDocumentConnectionInfoHandler,
|
||||
} from '@/handlers';
|
||||
import { corsMiddleware, httpSecurity, wsSecurity } from '@/middlewares';
|
||||
import { routes } from '@/routes';
|
||||
@@ -41,6 +42,12 @@ export const initApp = () => {
|
||||
collaborationResetConnectionsHandler,
|
||||
);
|
||||
|
||||
app.get(
|
||||
routes.COLLABORATION_GET_CONNECTIONS,
|
||||
httpSecurity,
|
||||
getDocumentConnectionInfoHandler,
|
||||
);
|
||||
|
||||
/**
|
||||
* Route to convert Markdown or BlockNote blocks
|
||||
*/
|
||||
|
||||
@@ -60,6 +60,14 @@ export const hocuspocusServer = Server.configure({
|
||||
|
||||
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
|
||||
* so we flag only authenticated users
|
||||
|
||||
Reference in New Issue
Block a user