mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 18:52:36 +10:00
collaboration module cleanup x2
This commit is contained in:
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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];
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user