✨(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".
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"build": "tsc -p ./src",
|
||||
"dev": "nodemon --config nodemon.json",
|
||||
"start": "node ./dist/server.js",
|
||||
"lint": "eslint . --ext .ts"
|
||||
@@ -16,9 +16,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hocuspocus/server": "2.14.0",
|
||||
"express": "4.21.1",
|
||||
"express-ws": "5.0.2",
|
||||
"y-protocols": "1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "5.0.0",
|
||||
"@types/express-ws": "3.0.5",
|
||||
"@types/node": "*",
|
||||
"eslint-config-impress": "*",
|
||||
"nodemon": "3.1.7",
|
||||
|
||||
7
src/frontend/servers/y-provider/src/env.ts
Normal file
7
src/frontend/servers/y-provider/src/env.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const COLLABORATION_LOGGING =
|
||||
process.env.COLLABORATION_LOGGING || 'false';
|
||||
export const COLLABORATION_SERVER_ORIGIN =
|
||||
process.env.COLLABORATION_SERVER_ORIGIN || 'http://localhost:3000';
|
||||
export const COLLABORATION_SERVER_SECRET =
|
||||
process.env.COLLABORATION_SERVER_SECRET || 'secret-api-key';
|
||||
export const PORT = Number(process.env.PORT || 4444);
|
||||
59
src/frontend/servers/y-provider/src/middlewares.ts
Normal file
59
src/frontend/servers/y-provider/src/middlewares.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import * as ws from 'ws';
|
||||
|
||||
import {
|
||||
COLLABORATION_SERVER_ORIGIN,
|
||||
COLLABORATION_SERVER_SECRET,
|
||||
} from '@/env';
|
||||
|
||||
import { logger } from './utils';
|
||||
|
||||
export const httpSecurity = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): void => {
|
||||
// Origin check
|
||||
const origin = req.headers['origin'];
|
||||
if (origin && COLLABORATION_SERVER_ORIGIN !== origin) {
|
||||
logger('CORS policy violation: Invalid Origin', origin);
|
||||
|
||||
res
|
||||
.status(403)
|
||||
.json({ error: 'CORS policy violation: Invalid Origin', origin });
|
||||
return;
|
||||
}
|
||||
|
||||
// Secret API Key check
|
||||
const apiKey = req.headers['authorization'];
|
||||
if (apiKey !== COLLABORATION_SERVER_SECRET) {
|
||||
res.status(403).json({ error: 'Forbidden: Invalid API Key' });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export const wsSecurity = (
|
||||
ws: ws.WebSocket,
|
||||
req: Request,
|
||||
next: NextFunction,
|
||||
): void => {
|
||||
// Origin check
|
||||
const origin = req.headers['origin'];
|
||||
if (COLLABORATION_SERVER_ORIGIN !== origin) {
|
||||
console.error('CORS policy violation: Invalid Origin', origin);
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Secret API Key check
|
||||
const apiKey = req.headers['authorization'];
|
||||
if (apiKey !== COLLABORATION_SERVER_SECRET) {
|
||||
console.error('Forbidden: Invalid API Key');
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
4
src/frontend/servers/y-provider/src/routes.ts
Normal file
4
src/frontend/servers/y-provider/src/routes.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const routes = {
|
||||
COLLABORATION_WS: '/collaboration/ws/',
|
||||
COLLABORATION_RESET_CONNECTIONS: '/collaboration/api/reset-connections/',
|
||||
};
|
||||
@@ -1,18 +1,147 @@
|
||||
import { Server } from '@hocuspocus/server';
|
||||
import express, { Request, Response } from 'express';
|
||||
import expressWebsockets from 'express-ws';
|
||||
|
||||
const port = Number(process.env.PORT || 4444);
|
||||
import { PORT } from './env';
|
||||
import { httpSecurity, wsSecurity } from './middlewares';
|
||||
import { routes } from './routes';
|
||||
import { logger } from './utils';
|
||||
|
||||
const server = Server.configure({
|
||||
name: 'docs-y-provider',
|
||||
port: port,
|
||||
export const hocuspocusServer = Server.configure({
|
||||
name: 'docs-y-server',
|
||||
timeout: 30000,
|
||||
debounce: 2000,
|
||||
maxDebounce: 30000,
|
||||
quiet: true,
|
||||
onConnect({ requestHeaders, connection, documentName, requestParameters }) {
|
||||
const roomParam = requestParameters.get('room');
|
||||
const canEdit = requestHeaders['x-can-edit'] === 'True';
|
||||
|
||||
if (!canEdit) {
|
||||
connection.readOnly = true;
|
||||
}
|
||||
|
||||
logger(
|
||||
'Connection established:',
|
||||
documentName,
|
||||
'userId:',
|
||||
requestHeaders['x-user-id'],
|
||||
'canEdit:',
|
||||
canEdit,
|
||||
'room:',
|
||||
requestParameters.get('room'),
|
||||
);
|
||||
|
||||
if (documentName !== roomParam) {
|
||||
console.error(
|
||||
'Invalid room name - Probable hacking attempt:',
|
||||
documentName,
|
||||
requestParameters.get('room'),
|
||||
requestHeaders['x-user-id'],
|
||||
);
|
||||
|
||||
return Promise.reject(new Error('Unauthorized'));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
server.listen().catch((error) => {
|
||||
console.error('Failed to start the server:', error);
|
||||
});
|
||||
/**
|
||||
* init the collaboration server.
|
||||
*
|
||||
* @param port - The port on which the server listens.
|
||||
* @param serverSecret - The secret key for API authentication.
|
||||
* @returns An object containing the Express app, Hocuspocus server, and HTTP server instance.
|
||||
*/
|
||||
export const initServer = () => {
|
||||
const { app } = expressWebsockets(express());
|
||||
app.use(express.json());
|
||||
|
||||
console.log('Websocket server running on port :', port);
|
||||
/**
|
||||
* Route to handle WebSocket connections
|
||||
*/
|
||||
app.ws(routes.COLLABORATION_WS, wsSecurity, (ws, req) => {
|
||||
logger('Incoming Origin:', req.headers['origin']);
|
||||
|
||||
try {
|
||||
hocuspocusServer.handleConnection(ws, req);
|
||||
} catch (error) {
|
||||
console.error('Failed to handle WebSocket connection:', error);
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
type ResetConnectionsRequestQuery = {
|
||||
room?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Route to reset connections in a room:
|
||||
* - If no user ID is provided, close all connections in the room
|
||||
* - If a user ID is provided, close connections for the user in the room
|
||||
*/
|
||||
app.post(
|
||||
routes.COLLABORATION_RESET_CONNECTIONS,
|
||||
httpSecurity,
|
||||
(
|
||||
req: Request<object, object, object, ResetConnectionsRequestQuery>,
|
||||
res: Response,
|
||||
) => {
|
||||
const room = req.query.room;
|
||||
const userId = req.headers['x-user-id'];
|
||||
|
||||
logger(
|
||||
'Resetting connections in room:',
|
||||
room,
|
||||
'for user:',
|
||||
userId,
|
||||
'room:',
|
||||
room,
|
||||
);
|
||||
|
||||
if (!room) {
|
||||
res.status(400).json({ error: 'Room name not provided' });
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If no user ID is provided, close all connections in the room
|
||||
*/
|
||||
if (!userId) {
|
||||
hocuspocusServer.closeConnections(room);
|
||||
} else {
|
||||
/**
|
||||
* Close connections for the user in the room
|
||||
*/
|
||||
hocuspocusServer.documents.forEach((doc) => {
|
||||
if (doc.name !== room) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.getConnections().forEach((connection) => {
|
||||
const connectionUserId = connection.request.headers['x-user-id'];
|
||||
if (connectionUserId === userId) {
|
||||
connection.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Connections reset' });
|
||||
},
|
||||
);
|
||||
|
||||
app.get('/ping', (req, res) => {
|
||||
res.status(200).json({ message: 'pong' });
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
logger('Invalid route:', req.url);
|
||||
res.status(403).json({ error: 'Forbidden' });
|
||||
});
|
||||
|
||||
const server = app.listen(PORT, () =>
|
||||
console.log('Listening on port :', PORT),
|
||||
);
|
||||
|
||||
return { app, server };
|
||||
};
|
||||
|
||||
3
src/frontend/servers/y-provider/src/start-server.ts
Normal file
3
src/frontend/servers/y-provider/src/start-server.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { initServer } from './server';
|
||||
|
||||
initServer();
|
||||
9
src/frontend/servers/y-provider/src/utils.ts
Normal file
9
src/frontend/servers/y-provider/src/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { COLLABORATION_LOGGING } from './env';
|
||||
|
||||
export function logger(...args: any[]) {
|
||||
if (COLLABORATION_LOGGING === 'true') {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user