Skip to content

Commit ba1cfc3

Browse files
committed
✨(y-provider) endpoint POST /collaboration/api/reset-connections
We want to be able to reset the connections of a document. To do this, we need to be able to send a request to the collaboration server. To do so, we added the endpoint POST "/collaboration/api/reset-connections" to the collaboration server thanks to "express".
1 parent 2cba228 commit ba1cfc3

File tree

11 files changed

+661
-23
lines changed

11 files changed

+661
-23
lines changed

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ services:
162162
dockerfile: ./src/frontend/Dockerfile
163163
target: y-provider
164164
restart: unless-stopped
165+
env_file:
166+
- env.d/development/common
165167
ports:
166168
- "4444:4444"
167169
volumes:

env.d/development/common.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ AI_API_KEY=password
5353
AI_MODEL=llama
5454

5555
# Collaboration
56+
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
5657
COLLABORATION_SERVER_SECRET=my-secret
5758
COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws
5859

src/frontend/servers/y-provider/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"license": "MIT",
77
"type": "module",
88
"scripts": {
9-
"build": "tsc -p .",
9+
"build": "tsc -p ./src",
1010
"dev": "nodemon --config nodemon.json",
1111
"start": "node ./dist/server.js",
1212
"lint": "eslint . --ext .ts"
@@ -16,9 +16,13 @@
1616
},
1717
"dependencies": {
1818
"@hocuspocus/server": "2.14.0",
19+
"express": "4.21.1",
20+
"express-ws": "5.0.2",
1921
"y-protocols": "1.0.6"
2022
},
2123
"devDependencies": {
24+
"@types/express": "5.0.0",
25+
"@types/express-ws": "3.0.5",
2226
"@types/node": "*",
2327
"eslint-config-impress": "*",
2428
"nodemon": "3.1.7",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const COLLABORATION_LOGGING =
2+
process.env.COLLABORATION_LOGGING || 'false';
3+
export const COLLABORATION_SERVER_ORIGIN =
4+
process.env.COLLABORATION_SERVER_ORIGIN || 'http://localhost:3000';
5+
export const COLLABORATION_SERVER_SECRET =
6+
process.env.COLLABORATION_SERVER_SECRET || 'secret-api-key';
7+
export const PORT = Number(process.env.PORT || 4444);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { NextFunction, Request, Response } from 'express';
2+
import * as ws from 'ws';
3+
4+
import {
5+
COLLABORATION_SERVER_ORIGIN,
6+
COLLABORATION_SERVER_SECRET,
7+
} from '@/env';
8+
9+
import { logger } from './utils';
10+
11+
export const httpSecurity = (
12+
req: Request,
13+
res: Response,
14+
next: NextFunction,
15+
): void => {
16+
// Origin check
17+
const origin = req.headers['origin'];
18+
if (origin && COLLABORATION_SERVER_ORIGIN !== origin) {
19+
logger('CORS policy violation: Invalid Origin', origin);
20+
21+
res
22+
.status(403)
23+
.json({ error: 'CORS policy violation: Invalid Origin', origin });
24+
return;
25+
}
26+
27+
// Secret API Key check
28+
const apiKey = req.headers['authorization'];
29+
if (apiKey !== COLLABORATION_SERVER_SECRET) {
30+
res.status(403).json({ error: 'Forbidden: Invalid API Key' });
31+
return;
32+
}
33+
34+
next();
35+
};
36+
37+
export const wsSecurity = (
38+
ws: ws.WebSocket,
39+
req: Request,
40+
next: NextFunction,
41+
): void => {
42+
// Origin check
43+
const origin = req.headers['origin'];
44+
if (COLLABORATION_SERVER_ORIGIN !== origin) {
45+
console.error('CORS policy violation: Invalid Origin', origin);
46+
ws.close();
47+
return;
48+
}
49+
50+
// Secret API Key check
51+
const apiKey = req.headers['authorization'];
52+
if (apiKey !== COLLABORATION_SERVER_SECRET) {
53+
console.error('Forbidden: Invalid API Key');
54+
ws.close();
55+
return;
56+
}
57+
58+
next();
59+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const routes = {
2+
COLLABORATION_WS: '/collaboration/ws/',
3+
COLLABORATION_RESET_CONNECTIONS: '/collaboration/api/reset-connections/',
4+
};
Lines changed: 139 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,147 @@
11
import { Server } from '@hocuspocus/server';
2+
import express, { Request, Response } from 'express';
3+
import expressWebsockets from 'express-ws';
24

3-
const port = Number(process.env.PORT || 4444);
5+
import { PORT } from './env';
6+
import { httpSecurity, wsSecurity } from './middlewares';
7+
import { routes } from './routes';
8+
import { logger } from './utils';
49

5-
const server = Server.configure({
6-
name: 'docs-y-provider',
7-
port: port,
10+
export const hocuspocusServer = Server.configure({
11+
name: 'docs-y-server',
812
timeout: 30000,
9-
debounce: 2000,
10-
maxDebounce: 30000,
1113
quiet: true,
12-
});
14+
onConnect({ requestHeaders, connection, documentName, requestParameters }) {
15+
const roomParam = requestParameters.get('room');
16+
const canEdit = requestHeaders['x-can-edit'] === 'True';
17+
18+
if (!canEdit) {
19+
connection.readOnly = true;
20+
}
21+
22+
logger(
23+
'Connection established:',
24+
documentName,
25+
'userId:',
26+
requestHeaders['x-user-id'],
27+
'canEdit:',
28+
canEdit,
29+
'room:',
30+
requestParameters.get('room'),
31+
);
32+
33+
if (documentName !== roomParam) {
34+
console.error(
35+
'Invalid room name - Probable hacking attempt:',
36+
documentName,
37+
requestParameters.get('room'),
38+
requestHeaders['x-user-id'],
39+
);
40+
41+
return Promise.reject(new Error('Unauthorized'));
42+
}
1343

14-
server.listen().catch((error) => {
15-
console.error('Failed to start the server:', error);
44+
return Promise.resolve();
45+
},
1646
});
1747

18-
console.log('Websocket server running on port :', port);
48+
/**
49+
* init the collaboration server.
50+
*
51+
* @param port - The port on which the server listens.
52+
* @param serverSecret - The secret key for API authentication.
53+
* @returns An object containing the Express app, Hocuspocus server, and HTTP server instance.
54+
*/
55+
export const initServer = () => {
56+
const { app } = expressWebsockets(express());
57+
app.use(express.json());
58+
59+
/**
60+
* Route to handle WebSocket connections
61+
*/
62+
app.ws(routes.COLLABORATION_WS, wsSecurity, (ws, req) => {
63+
logger('Incoming Origin:', req.headers['origin']);
64+
65+
try {
66+
hocuspocusServer.handleConnection(ws, req);
67+
} catch (error) {
68+
console.error('Failed to handle WebSocket connection:', error);
69+
ws.close();
70+
}
71+
});
72+
73+
type ResetConnectionsRequestQuery = {
74+
room?: string;
75+
};
76+
77+
/**
78+
* Route to reset connections in a room:
79+
* - If no user ID is provided, close all connections in the room
80+
* - If a user ID is provided, close connections for the user in the room
81+
*/
82+
app.post(
83+
routes.COLLABORATION_RESET_CONNECTIONS,
84+
httpSecurity,
85+
(
86+
req: Request<object, object, object, ResetConnectionsRequestQuery>,
87+
res: Response,
88+
) => {
89+
const room = req.query.room;
90+
const userId = req.headers['x-user-id'];
91+
92+
logger(
93+
'Resetting connections in room:',
94+
room,
95+
'for user:',
96+
userId,
97+
'room:',
98+
room,
99+
);
100+
101+
if (!room) {
102+
res.status(400).json({ error: 'Room name not provided' });
103+
return;
104+
}
105+
106+
/**
107+
* If no user ID is provided, close all connections in the room
108+
*/
109+
if (!userId) {
110+
hocuspocusServer.closeConnections(room);
111+
} else {
112+
/**
113+
* Close connections for the user in the room
114+
*/
115+
hocuspocusServer.documents.forEach((doc) => {
116+
if (doc.name !== room) {
117+
return;
118+
}
119+
120+
doc.getConnections().forEach((connection) => {
121+
const connectionUserId = connection.request.headers['x-user-id'];
122+
if (connectionUserId === userId) {
123+
connection.close();
124+
}
125+
});
126+
});
127+
}
128+
129+
res.status(200).json({ message: 'Connections reset' });
130+
},
131+
);
132+
133+
app.get('/ping', (req, res) => {
134+
res.status(200).json({ message: 'pong' });
135+
});
136+
137+
app.use((req, res) => {
138+
logger('Invalid route:', req.url);
139+
res.status(403).json({ error: 'Forbidden' });
140+
});
141+
142+
const server = app.listen(PORT, () =>
143+
console.log('Listening on port :', PORT),
144+
);
145+
146+
return { app, server };
147+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { initServer } from './server';
2+
3+
initServer();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
import { COLLABORATION_LOGGING } from './env';
4+
5+
export function logger(...args: any[]) {
6+
if (COLLABORATION_LOGGING === 'true') {
7+
console.log(...args);
8+
}
9+
}

0 commit comments

Comments
 (0)