collaboration module cleanup x2

This commit is contained in:
Philipinho
2024-05-04 17:21:44 +01:00
parent 8cfc42b79a
commit a2768e7d30
6 changed files with 47 additions and 21 deletions

View File

@ -7,8 +7,8 @@ export class CollabWsAdapter {
this.wss = new WebSocketServer({ noServer: true }); this.wss = new WebSocketServer({ noServer: true });
} }
handleUpgrade(path: string, httpServer) { handleUpgrade(path: string, httpServer: any) {
httpServer.on('upgrade', (request, socket, head) => { httpServer.on('upgrade', (request: any, socket: any, head: any) => {
try { try {
const baseUrl = 'ws://' + request.headers.host + '/'; const baseUrl = 'ws://' + request.headers.host + '/';
const pathname = new URL(request.url, baseUrl).pathname; const pathname = new URL(request.url, baseUrl).pathname;

View File

@ -20,7 +20,7 @@ import { HistoryExtension } from './extensions/history.extension';
}) })
export class CollaborationModule implements OnModuleInit, OnModuleDestroy { export class CollaborationModule implements OnModuleInit, OnModuleDestroy {
private collabWsAdapter: CollabWsAdapter; private collabWsAdapter: CollabWsAdapter;
private path = '/collaboration'; private path = '/collab';
constructor( constructor(
private readonly collaborationGateway: CollaborationGateway, private readonly collaborationGateway: CollaborationGateway,

View File

@ -42,3 +42,7 @@ export function htmlToJson(html: string) {
export function jsonToText(tiptapJson: JSONContent) { export function jsonToText(tiptapJson: JSONContent) {
return generateText(tiptapJson, tiptapExtensions); return generateText(tiptapJson, tiptapExtensions);
} }
export function getPageId(documentName: string) {
return documentName.split('.')[1];
}

View File

@ -1,14 +1,22 @@
import { Extension, onAuthenticatePayload } from '@hocuspocus/server'; import { Extension, onAuthenticatePayload } from '@hocuspocus/server';
import { Injectable, UnauthorizedException } from '@nestjs/common'; import {
Injectable,
Logger,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { TokenService } from '../../core/auth/services/token.service'; import { TokenService } from '../../core/auth/services/token.service';
import { UserRepo } from '@docmost/db/repos/user/user.repo'; import { UserRepo } from '@docmost/db/repos/user/user.repo';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils'; import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
import { SpaceRole } from '../../helpers/types/permission'; import { SpaceRole } from '../../helpers/types/permission';
import { getPageId } from '../collaboration.util';
@Injectable() @Injectable()
export class AuthenticationExtension implements Extension { export class AuthenticationExtension implements Extension {
private readonly logger = new Logger(AuthenticationExtension.name);
constructor( constructor(
private tokenService: TokenService, private tokenService: TokenService,
private userRepo: UserRepo, private userRepo: UserRepo,
@ -18,6 +26,7 @@ export class AuthenticationExtension implements Extension {
async onAuthenticate(data: onAuthenticatePayload) { async onAuthenticate(data: onAuthenticatePayload) {
const { documentName, token } = data; const { documentName, token } = data;
const pageId = getPageId(documentName);
let jwtPayload = null; let jwtPayload = null;
@ -36,9 +45,10 @@ export class AuthenticationExtension implements Extension {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
const page = await this.pageRepo.findById(documentName); const page = await this.pageRepo.findById(pageId);
if (!page) { if (!page) {
throw new UnauthorizedException('Page not found'); this.logger.warn(`Page not found: ${pageId}}`);
throw new NotFoundException('Page not found');
} }
const userSpaceRoles = await this.spaceMemberRepo.getUserSpaceRoles( const userSpaceRoles = await this.spaceMemberRepo.getUserSpaceRoles(
@ -49,13 +59,17 @@ export class AuthenticationExtension implements Extension {
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles); const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
if (!userSpaceRole) { if (!userSpaceRole) {
this.logger.warn(`User authorized to access page: ${pageId}}`);
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
if (userSpaceRole === SpaceRole.READER) { if (userSpaceRole === SpaceRole.READER) {
data.connection.readOnly = true; data.connection.readOnly = true;
this.logger.warn(`User granted readonly access to page: ${pageId}}`);
} }
this.logger.debug(`Authenticated user ${user.id} on page ${pageId}`);
return { return {
user, user,
}; };

View File

@ -3,12 +3,15 @@ import {
onChangePayload, onChangePayload,
onDisconnectPayload, onDisconnectPayload,
} from '@hocuspocus/server'; } from '@hocuspocus/server';
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo'; import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo';
import { getPageId } from '../collaboration.util';
@Injectable() @Injectable()
export class HistoryExtension implements Extension { export class HistoryExtension implements Extension {
private readonly logger = new Logger(HistoryExtension.name);
ACTIVE_EDITING_INTERVAL = 10 * 60 * 1000; // 10 minutes ACTIVE_EDITING_INTERVAL = 10 * 60 * 1000; // 10 minutes
historyIntervalMap = new Map<string, NodeJS.Timeout>(); historyIntervalMap = new Map<string, NodeJS.Timeout>();
lastEditTimeMap = new Map<string, number>(); lastEditTimeMap = new Map<string, number>();
@ -19,7 +22,8 @@ export class HistoryExtension implements Extension {
) {} ) {}
async onChange(data: onChangePayload): Promise<void> { async onChange(data: onChangePayload): Promise<void> {
const pageId = data.documentName; const pageId = getPageId(data.documentName);
this.lastEditTimeMap.set(pageId, Date.now()); this.lastEditTimeMap.set(pageId, Date.now());
if (!this.historyIntervalMap.has(pageId)) { if (!this.historyIntervalMap.has(pageId)) {
@ -33,7 +37,7 @@ export class HistoryExtension implements Extension {
} }
async onDisconnect(data: onDisconnectPayload): Promise<void> { async onDisconnect(data: onDisconnectPayload): Promise<void> {
const pageId = data.documentName; const pageId = getPageId(data.documentName);
if (data.clientsCount === 0) { if (data.clientsCount === 0) {
if (this.historyIntervalMap.has(pageId)) { if (this.historyIntervalMap.has(pageId)) {
clearInterval(this.historyIntervalMap.get(pageId)); clearInterval(this.historyIntervalMap.get(pageId));
@ -58,9 +62,12 @@ export class HistoryExtension implements Extension {
}); });
// Todo: compare if data is the same as the previous version // Todo: compare if data is the same as the previous version
await this.pageHistoryRepo.saveHistory(page); await this.pageHistoryRepo.saveHistory(page);
console.log(`New history created for: ${pageId}`); this.logger.debug(`New history created for: ${pageId}`);
} catch (err) { } catch (err) {
console.error('An error occurred saving page history', err); this.logger.error(
`An error occurred saving page history for: ${pageId}`,
err,
);
} }
} }
} }

View File

@ -4,18 +4,20 @@ import {
onStoreDocumentPayload, onStoreDocumentPayload,
} from '@hocuspocus/server'; } from '@hocuspocus/server';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { TiptapTransformer } from '@hocuspocus/transformer'; import { TiptapTransformer } from '@hocuspocus/transformer';
import { jsonToText, tiptapExtensions } from '../collaboration.util'; import { getPageId, jsonToText, tiptapExtensions } from '../collaboration.util';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
@Injectable() @Injectable()
export class PersistenceExtension implements Extension { export class PersistenceExtension implements Extension {
private readonly logger = new Logger(PersistenceExtension.name);
constructor(private readonly pageRepo: PageRepo) {} constructor(private readonly pageRepo: PageRepo) {}
async onLoadDocument(data: onLoadDocumentPayload) { async onLoadDocument(data: onLoadDocumentPayload) {
const { documentName, document } = data; const { documentName, document } = data;
const pageId = documentName; const pageId = getPageId(documentName);
if (!document.isEmpty('default')) { if (!document.isEmpty('default')) {
return; return;
@ -27,13 +29,12 @@ export class PersistenceExtension implements Extension {
}); });
if (!page) { if (!page) {
console.log('page does not exist.'); this.logger.warn('page not found');
//TODO: terminate connection if the page does not exist?
return; return;
} }
if (page.ydoc) { if (page.ydoc) {
console.log('ydoc loaded from db'); this.logger.debug(`ydoc loaded from db: ${pageId}`);
const doc = new Y.Doc(); const doc = new Y.Doc();
const dbState = new Uint8Array(page.ydoc); const dbState = new Uint8Array(page.ydoc);
@ -44,7 +45,7 @@ export class PersistenceExtension implements Extension {
// if no ydoc state in db convert json in page.content to Ydoc. // if no ydoc state in db convert json in page.content to Ydoc.
if (page.content) { if (page.content) {
console.log('converting json to ydoc'); this.logger.debug(`converting json to ydoc: ${pageId}`);
const ydoc = TiptapTransformer.toYdoc( const ydoc = TiptapTransformer.toYdoc(
page.content, page.content,
@ -56,14 +57,14 @@ export class PersistenceExtension implements Extension {
return ydoc; return ydoc;
} }
console.log('creating fresh ydoc'); this.logger.debug(`creating fresh ydoc': ${pageId}`);
return new Y.Doc(); return new Y.Doc();
} }
async onStoreDocument(data: onStoreDocumentPayload) { async onStoreDocument(data: onStoreDocumentPayload) {
const { documentName, document, context } = data; const { documentName, document, context } = data;
const pageId = documentName; const pageId = getPageId(documentName);
const tiptapJson = TiptapTransformer.fromYdoc(document, 'default'); const tiptapJson = TiptapTransformer.fromYdoc(document, 'default');
const ydocState = Buffer.from(Y.encodeStateAsUpdate(document)); const ydocState = Buffer.from(Y.encodeStateAsUpdate(document));
@ -81,7 +82,7 @@ export class PersistenceExtension implements Extension {
pageId, pageId,
); );
} catch (err) { } catch (err) {
console.error(`Failed to update page ${documentName}`); this.logger.error(`Failed to update page ${pageId}`, err);
} }
} }
} }