mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-07 01:51:36 -04:00
refactor: replace TypeORM with knex.js
Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
6e151c8a1b
commit
c0ce00b3f9
242 changed files with 4601 additions and 6871 deletions
|
@ -11,8 +11,7 @@ import {
|
|||
import * as HedgeDocCommonsModule from '@hedgedoc/commons';
|
||||
import { Mock } from 'ts-mockery';
|
||||
|
||||
import { User } from '../../database/user.entity';
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { FieldNameUser, User } from '../../database/types';
|
||||
import * as NameRandomizerModule from './random-word-lists/name-randomizer';
|
||||
import { RealtimeConnection } from './realtime-connection';
|
||||
import { RealtimeNote } from './realtime-note';
|
||||
|
@ -41,12 +40,15 @@ describe('websocket connection', () => {
|
|||
|
||||
const mockedUserName: string = 'mocked-user-name';
|
||||
const mockedDisplayName = 'mockedDisplayName';
|
||||
const mockedAuthorStyle = 42;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedRealtimeNote = new RealtimeNote(Mock.of<Note>({}), '');
|
||||
mockedRealtimeNote = new RealtimeNote(1, '');
|
||||
mockedUser = Mock.of<User>({
|
||||
username: mockedUserName,
|
||||
displayName: mockedDisplayName,
|
||||
[FieldNameUser.id]: 0,
|
||||
[FieldNameUser.username]: mockedUserName,
|
||||
[FieldNameUser.displayName]: mockedDisplayName,
|
||||
[FieldNameUser.authorStyle]: mockedAuthorStyle,
|
||||
});
|
||||
|
||||
mockedMessageTransporter = new MessageTransporter();
|
||||
|
@ -61,7 +63,10 @@ describe('websocket connection', () => {
|
|||
it('returns the correct transporter', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
@ -71,7 +76,10 @@ describe('websocket connection', () => {
|
|||
it('returns the correct realtime note', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
@ -107,12 +115,14 @@ describe('websocket connection', () => {
|
|||
(
|
||||
username,
|
||||
displayName,
|
||||
authorStyle,
|
||||
otherAdapterCollector: OtherAdapterCollector,
|
||||
messageTransporter,
|
||||
acceptCursorUpdateProvider,
|
||||
) => {
|
||||
expect(username).toBe(mockedUserName);
|
||||
expect(displayName).toBe(mockedDisplayName);
|
||||
expect(authorStyle).toBe(mockedAuthorStyle);
|
||||
expect(otherAdapterCollector()).toStrictEqual([
|
||||
realtimeUserStatus1,
|
||||
realtimeUserStatus2,
|
||||
|
@ -126,7 +136,10 @@ describe('websocket connection', () => {
|
|||
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
acceptEdits,
|
||||
);
|
||||
|
@ -157,7 +170,10 @@ describe('websocket connection', () => {
|
|||
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
acceptEdits,
|
||||
);
|
||||
|
@ -169,7 +185,10 @@ describe('websocket connection', () => {
|
|||
it('removes the client from the note on transporter disconnect', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
@ -181,46 +200,59 @@ describe('websocket connection', () => {
|
|||
expect(removeClientSpy).toHaveBeenCalledWith(sut);
|
||||
});
|
||||
|
||||
it('saves the correct user', () => {
|
||||
it('correctly return user id', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sut.getUser()).toBe(mockedUser);
|
||||
expect(sut.getUserId()).toBe(mockedUser[FieldNameUser.id]);
|
||||
});
|
||||
|
||||
it('returns the correct username', () => {
|
||||
const mockedUserWithUsername = Mock.of<User>({ displayName: 'MockUser' });
|
||||
|
||||
it('correctly return username', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUserWithUsername,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sut.getDisplayName()).toBe('MockUser');
|
||||
expect(sut.getUsername()).toBe(mockedUser[FieldNameUser.username]);
|
||||
});
|
||||
|
||||
it('returns a random fallback display name if the provided user has no display name', () => {
|
||||
const randomName = 'I am a random name';
|
||||
|
||||
jest
|
||||
.spyOn(NameRandomizerModule, 'generateRandomName')
|
||||
.mockReturnValue(randomName);
|
||||
|
||||
mockedUser = Mock.of<User>({});
|
||||
|
||||
it('correctly return displayName', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sut.getDisplayName()).toBe(randomName);
|
||||
expect(sut.getDisplayName()).toBe(mockedUser[FieldNameUser.displayName]);
|
||||
});
|
||||
|
||||
it('correctly return authorStyle', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sut.getAuthorStyle()).toBe(mockedUser[FieldNameUser.authorStyle]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
import { MessageTransporter, YDocSyncServerAdapter } from '@hedgedoc/commons';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { User } from '../../database/user.entity';
|
||||
import { generateRandomName } from './random-word-lists/name-randomizer';
|
||||
import { RealtimeNote } from './realtime-note';
|
||||
import { RealtimeUserStatusAdapter } from './realtime-user-status-adapter';
|
||||
|
||||
|
@ -19,24 +17,28 @@ export class RealtimeConnection {
|
|||
private readonly transporter: MessageTransporter;
|
||||
private readonly yDocSyncAdapter: YDocSyncServerAdapter;
|
||||
private readonly realtimeUserStateAdapter: RealtimeUserStatusAdapter;
|
||||
private readonly displayName: string;
|
||||
|
||||
/**
|
||||
* Instantiates the connection wrapper.
|
||||
*
|
||||
* @param messageTransporter The message transporter that handles the communication with the client.
|
||||
* @param user The user of the client
|
||||
* @param userId The id of the user of the client
|
||||
* @param username The username of the user of the client
|
||||
* @param displayName The displayName of the user of the client
|
||||
* @param authorStyle The authorStyle of the user of the client
|
||||
* @param realtimeNote The {@link RealtimeNote} that the client connected to.
|
||||
* @param acceptEdits If edits by this connection should be accepted.
|
||||
* @throws Error if the socket is not open
|
||||
*/
|
||||
constructor(
|
||||
messageTransporter: MessageTransporter,
|
||||
private user: User | null,
|
||||
private userId: number,
|
||||
private username: string | null,
|
||||
private displayName: string,
|
||||
private authorStyle: number,
|
||||
private realtimeNote: RealtimeNote,
|
||||
public acceptEdits: boolean,
|
||||
) {
|
||||
this.displayName = user?.displayName ?? generateRandomName();
|
||||
this.transporter = messageTransporter;
|
||||
|
||||
this.transporter.on('disconnected', () => {
|
||||
|
@ -48,8 +50,9 @@ export class RealtimeConnection {
|
|||
() => acceptEdits,
|
||||
);
|
||||
this.realtimeUserStateAdapter = new RealtimeUserStatusAdapter(
|
||||
this.user?.username ?? null,
|
||||
this.getDisplayName(),
|
||||
this.username ?? null,
|
||||
this.displayName,
|
||||
this.authorStyle,
|
||||
() =>
|
||||
this.realtimeNote
|
||||
.getConnections()
|
||||
|
@ -67,18 +70,26 @@ export class RealtimeConnection {
|
|||
return this.transporter;
|
||||
}
|
||||
|
||||
public getUser(): User | null {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
public getSyncAdapter(): YDocSyncServerAdapter {
|
||||
return this.yDocSyncAdapter;
|
||||
}
|
||||
|
||||
public getUserId(): number {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public getDisplayName(): string {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
public getUsername(): string | null {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public getAuthorStyle(): number {
|
||||
return this.authorStyle;
|
||||
}
|
||||
|
||||
public getRealtimeNote(): RealtimeNote {
|
||||
return this.realtimeNote;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { RealtimeNote } from './realtime-note';
|
||||
|
||||
@Injectable()
|
||||
|
@ -15,29 +14,29 @@ export class RealtimeNoteStore {
|
|||
/**
|
||||
* Creates a new {@link RealtimeNote} for the given {@link Note} and memorizes it.
|
||||
*
|
||||
* @param note The note for which the realtime note should be created
|
||||
* @param noteId The note for which the realtime note should be created
|
||||
* @param initialTextContent the initial text content of realtime doc
|
||||
* @param initialYjsState the initial yjs state. If provided this will be used instead of the text content
|
||||
* @throws Error if there is already an realtime note for the given note.
|
||||
* @return The created realtime note
|
||||
*/
|
||||
public create(
|
||||
note: Note,
|
||||
noteId: number,
|
||||
initialTextContent: string,
|
||||
initialYjsState?: number[],
|
||||
initialYjsState?: ArrayBuffer,
|
||||
): RealtimeNote {
|
||||
if (this.noteIdToRealtimeNote.has(note.id)) {
|
||||
throw new Error(`Realtime note for note ${note.id} already exists.`);
|
||||
if (this.noteIdToRealtimeNote.has(noteId)) {
|
||||
throw new Error(`Realtime note for note ${noteId} already exists.`);
|
||||
}
|
||||
const realtimeNote = new RealtimeNote(
|
||||
note,
|
||||
noteId,
|
||||
initialTextContent,
|
||||
initialYjsState,
|
||||
);
|
||||
realtimeNote.on('destroy', () => {
|
||||
this.noteIdToRealtimeNote.delete(note.id);
|
||||
this.noteIdToRealtimeNote.delete(noteId);
|
||||
});
|
||||
this.noteIdToRealtimeNote.set(note.id, realtimeNote);
|
||||
this.noteIdToRealtimeNote.set(noteId, realtimeNote);
|
||||
return realtimeNote;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Module } from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
import { LoggerModule } from '../../logger/logger.module';
|
||||
|
@ -18,7 +18,7 @@ import { RealtimeNoteService } from './realtime-note.service';
|
|||
imports: [
|
||||
LoggerModule,
|
||||
UsersModule,
|
||||
PermissionsModule,
|
||||
forwardRef(() => PermissionsModule),
|
||||
SessionModule,
|
||||
RevisionsModule,
|
||||
ScheduleModule.forRoot(),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -10,8 +10,8 @@ import { AppConfig } from '../../config/app.config';
|
|||
import { User } from '../../database/user.entity';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { NotePermission } from '../../permissions/note-permission.enum';
|
||||
import { PermissionsService } from '../../permissions/permissions.service';
|
||||
import { NotePermissionLevel } from '../../permissions/note-permission.enum';
|
||||
import { PermissionService } from '../../permissions/permission.service';
|
||||
import { Revision } from '../../revisions/revision.entity';
|
||||
import { RevisionsService } from '../../revisions/revisions.service';
|
||||
import { RealtimeConnection } from './realtime-connection';
|
||||
|
@ -29,7 +29,7 @@ describe('RealtimeNoteService', () => {
|
|||
let realtimeNoteService: RealtimeNoteService;
|
||||
let revisionsService: RevisionsService;
|
||||
let realtimeNoteStore: RealtimeNoteStore;
|
||||
let mockedPermissionService: PermissionsService;
|
||||
let mockedPermissionService: PermissionService;
|
||||
let consoleLoggerService: ConsoleLoggerService;
|
||||
let mockedAppConfig: AppConfig;
|
||||
let addIntervalSpy: jest.SpyInstance;
|
||||
|
@ -91,14 +91,14 @@ describe('RealtimeNoteService', () => {
|
|||
});
|
||||
|
||||
mockedAppConfig = Mock.of<AppConfig>({ persistInterval: 0 });
|
||||
mockedPermissionService = Mock.of<PermissionsService>({
|
||||
mockedPermissionService = Mock.of<PermissionService>({
|
||||
determinePermission: async (user: User | null) => {
|
||||
if (user?.username === readWriteUsername) {
|
||||
return NotePermission.WRITE;
|
||||
return NotePermissionLevel.WRITE;
|
||||
} else if (user?.username === onlyReadUsername) {
|
||||
return NotePermission.READ;
|
||||
return NotePermissionLevel.READ;
|
||||
} else {
|
||||
return NotePermission.DENY;
|
||||
return NotePermissionLevel.DENY;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -9,11 +9,11 @@ import { OnEvent } from '@nestjs/event-emitter';
|
|||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
|
||||
import appConfiguration, { AppConfig } from '../../config/app.config';
|
||||
import { FieldNameRevision } from '../../database/types';
|
||||
import { NoteEvent } from '../../events';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { NotePermission } from '../../permissions/note-permission.enum';
|
||||
import { PermissionsService } from '../../permissions/permissions.service';
|
||||
import { NotePermissionLevel } from '../../permissions/note-permission.enum';
|
||||
import { PermissionService } from '../../permissions/permission.service';
|
||||
import { RevisionsService } from '../../revisions/revisions.service';
|
||||
import { RealtimeConnection } from './realtime-connection';
|
||||
import { RealtimeNote } from './realtime-note';
|
||||
|
@ -28,7 +28,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
|||
private schedulerRegistry: SchedulerRegistry,
|
||||
@Inject(appConfiguration.KEY)
|
||||
private appConfig: AppConfig,
|
||||
private permissionService: PermissionsService,
|
||||
private permissionService: PermissionService,
|
||||
) {}
|
||||
|
||||
beforeApplicationShutdown(): void {
|
||||
|
@ -44,9 +44,10 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
|||
*/
|
||||
public saveRealtimeNote(realtimeNote: RealtimeNote): void {
|
||||
this.revisionsService
|
||||
.createAndSaveRevision(
|
||||
realtimeNote.getNote(),
|
||||
.createRevision(
|
||||
realtimeNote.getNoteId(),
|
||||
realtimeNote.getRealtimeDoc().getCurrentContent(),
|
||||
undefined,
|
||||
realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
|
||||
)
|
||||
.then(() => {
|
||||
|
@ -57,30 +58,30 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
|||
|
||||
/**
|
||||
* Creates or reuses a {@link RealtimeNote} that is handling the real time editing of the {@link Note} which is identified by the given note id.
|
||||
* @param note The {@link Note} for which a {@link RealtimeNote realtime note} should be retrieved.
|
||||
* @param noteId The {@link Note} for which a {@link RealtimeNote realtime note} should be retrieved.
|
||||
* @throws NotInDBError if note doesn't exist or has no revisions.
|
||||
* @return A {@link RealtimeNote} that is linked to the given note.
|
||||
*/
|
||||
public async getOrCreateRealtimeNote(note: Note): Promise<RealtimeNote> {
|
||||
public async getOrCreateRealtimeNote(noteId: number): Promise<RealtimeNote> {
|
||||
return (
|
||||
this.realtimeNoteStore.find(note.id) ??
|
||||
(await this.createNewRealtimeNote(note))
|
||||
this.realtimeNoteStore.find(noteId) ??
|
||||
(await this.createNewRealtimeNote(noteId))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link RealtimeNote} for the given {@link Note}.
|
||||
*
|
||||
* @param note The note for which the realtime note should be created
|
||||
* @param noteId The note for which the realtime note should be created
|
||||
* @throws NotInDBError if note doesn't exist or has no revisions.
|
||||
* @return The created realtime note
|
||||
*/
|
||||
private async createNewRealtimeNote(note: Note): Promise<RealtimeNote> {
|
||||
const lastRevision = await this.revisionsService.getLatestRevision(note);
|
||||
private async createNewRealtimeNote(noteId: number): Promise<RealtimeNote> {
|
||||
const lastRevision = await this.revisionsService.getLatestRevision(noteId);
|
||||
const realtimeNote = this.realtimeNoteStore.create(
|
||||
note,
|
||||
noteId,
|
||||
lastRevision.content,
|
||||
lastRevision.yjsStateVector ?? undefined,
|
||||
lastRevision[FieldNameRevision.yjsStateVector] ?? undefined,
|
||||
);
|
||||
realtimeNote.on('beforeDestroy', () => {
|
||||
this.saveRealtimeNote(realtimeNote);
|
||||
|
@ -103,47 +104,47 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
|||
persistInterval * 60 * 1000,
|
||||
);
|
||||
this.schedulerRegistry.addInterval(
|
||||
`periodic-persist-${realtimeNote.getNote().id}`,
|
||||
`periodic-persist-${realtimeNote.getNoteId()}`,
|
||||
intervalId,
|
||||
);
|
||||
realtimeNote.on('destroy', () => {
|
||||
clearInterval(intervalId);
|
||||
this.schedulerRegistry.deleteInterval(
|
||||
`periodic-persist-${realtimeNote.getNote().id}`,
|
||||
`periodic-persist-${realtimeNote.getNoteId()}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(NoteEvent.PERMISSION_CHANGE)
|
||||
public async handleNotePermissionChanged(note: Note): Promise<void> {
|
||||
const realtimeNote = this.realtimeNoteStore.find(note.id);
|
||||
public async handleNotePermissionChanged(noteId: number): Promise<void> {
|
||||
const realtimeNote = this.realtimeNoteStore.find(noteId);
|
||||
if (!realtimeNote) return;
|
||||
|
||||
realtimeNote.announceMetadataUpdate();
|
||||
const allConnections = realtimeNote.getConnections();
|
||||
await this.updateOrCloseConnection(allConnections, note);
|
||||
await this.updateOrCloseConnection(allConnections, noteId);
|
||||
}
|
||||
|
||||
private async updateOrCloseConnection(
|
||||
connections: RealtimeConnection[],
|
||||
note: Note,
|
||||
noteId: number,
|
||||
): Promise<void> {
|
||||
for (const connection of connections) {
|
||||
const permission = await this.permissionService.determinePermission(
|
||||
connection.getUser(),
|
||||
note,
|
||||
connection.getUserId(),
|
||||
noteId,
|
||||
);
|
||||
if (permission === NotePermission.DENY) {
|
||||
if (permission === NotePermissionLevel.DENY) {
|
||||
connection.getTransporter().disconnect();
|
||||
} else {
|
||||
connection.acceptEdits = permission > NotePermission.READ;
|
||||
connection.acceptEdits = permission > NotePermissionLevel.READ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent(NoteEvent.DELETION)
|
||||
public handleNoteDeleted(noteId: Note['id']): void {
|
||||
public handleNoteDeleted(noteId: number): void {
|
||||
const realtimeNote = this.realtimeNoteStore.find(noteId);
|
||||
if (realtimeNote) {
|
||||
realtimeNote.announceNoteDeletion();
|
||||
|
|
|
@ -29,7 +29,7 @@ describe('realtime note', () => {
|
|||
|
||||
it('can return the given note', () => {
|
||||
const sut = new RealtimeNote(mockedNote, 'nothing');
|
||||
expect(sut.getNote()).toBe(mockedNote);
|
||||
expect(sut.getNoteId()).toBe(mockedNote);
|
||||
});
|
||||
|
||||
it('can connect and disconnect clients', () => {
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Message, MessageType, RealtimeDoc } from '@hedgedoc/commons';
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { EventEmitter2, EventMap, Listener } from 'eventemitter2';
|
||||
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { RealtimeConnection } from './realtime-connection';
|
||||
|
||||
export interface RealtimeNoteEventMap extends EventMap {
|
||||
|
@ -33,16 +32,16 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
|
|||
private destroyEventTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly note: Note,
|
||||
private readonly noteId: number,
|
||||
initialTextContent: string,
|
||||
initialYjsState?: number[],
|
||||
initialYjsState?: ArrayBuffer,
|
||||
) {
|
||||
super();
|
||||
this.logger = new Logger(`${RealtimeNote.name} ${note.id}`);
|
||||
this.logger = new Logger(`${RealtimeNote.name} ${noteId}`);
|
||||
this.doc = new RealtimeDoc(initialTextContent, initialYjsState);
|
||||
const length = this.doc.getCurrentContent().length;
|
||||
this.logger.debug(
|
||||
`New realtime session for note ${note.id} created. Length of initial content: ${length} characters`,
|
||||
`New realtime session for note ${noteId} created. Length of initial content: ${length} characters`,
|
||||
);
|
||||
this.clientAddedListener = this.on(
|
||||
'clientAdded',
|
||||
|
@ -74,7 +73,7 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
|
|||
/**
|
||||
* Disconnects the given websocket client while cleaning-up if it was the last user in the realtime note.
|
||||
*
|
||||
* @param {WebSocket} client The websocket client that disconnects.
|
||||
* @param client The websocket client that disconnects.
|
||||
*/
|
||||
public removeClient(client: RealtimeConnection): void {
|
||||
this.clients.delete(client);
|
||||
|
@ -144,8 +143,8 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
|
|||
*
|
||||
* @return the {@link Note note}
|
||||
*/
|
||||
public getNote(): Note {
|
||||
return this.note;
|
||||
public getNoteId(): number {
|
||||
return this.noteId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@ export class RealtimeUserStatusAdapter {
|
|||
constructor(
|
||||
private readonly username: string | null,
|
||||
private readonly displayName: string,
|
||||
private readonly authorStyle: number,
|
||||
private collectOtherAdapters: OtherAdapterCollector,
|
||||
private messageTransporter: MessageTransporter,
|
||||
private acceptCursorUpdateProvider: () => boolean,
|
||||
|
@ -35,9 +36,7 @@ export class RealtimeUserStatusAdapter {
|
|||
username: this.username,
|
||||
displayName: this.displayName,
|
||||
active: true,
|
||||
styleIndex: this.findLeastUsedStyleIndex(
|
||||
this.createStyleIndexToCountMap(),
|
||||
),
|
||||
styleIndex: this.authorStyle,
|
||||
cursor: !this.acceptCursorUpdateProvider()
|
||||
? null
|
||||
: {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '@hedgedoc/commons';
|
||||
import { Mock } from 'ts-mockery';
|
||||
|
||||
import { User } from '../../../database/user.entity';
|
||||
import { FieldNameUser, User } from '../../../database/types';
|
||||
import { RealtimeConnection } from '../realtime-connection';
|
||||
import { RealtimeNote } from '../realtime-note';
|
||||
import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter';
|
||||
|
@ -21,14 +21,14 @@ enum RealtimeUserState {
|
|||
WITH_READONLY,
|
||||
}
|
||||
|
||||
const MOCK_FALLBACK_USERNAME: string = 'mock';
|
||||
|
||||
/**
|
||||
* Creates a mocked {@link RealtimeConnection realtime connection}.
|
||||
*/
|
||||
export class MockConnectionBuilder {
|
||||
private userId: number;
|
||||
private username: string | null;
|
||||
private displayName: string | undefined;
|
||||
private displayName: string;
|
||||
private authorStyle: number;
|
||||
private includeRealtimeUserStatus: RealtimeUserState =
|
||||
RealtimeUserState.WITHOUT;
|
||||
|
||||
|
@ -42,6 +42,8 @@ export class MockConnectionBuilder {
|
|||
public withGuestUser(displayName: string): this {
|
||||
this.username = null;
|
||||
this.displayName = displayName;
|
||||
this.authorStyle = 8;
|
||||
this.userId = 1000;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -50,10 +52,11 @@ export class MockConnectionBuilder {
|
|||
*
|
||||
* @param username the username of the mocked user. If this value is omitted then the builder will user a {@link MOCK_FALLBACK_USERNAME fallback}.
|
||||
*/
|
||||
public withLoggedInUser(username?: string): this {
|
||||
const newUsername = username ?? MOCK_FALLBACK_USERNAME;
|
||||
this.username = newUsername;
|
||||
this.displayName = newUsername;
|
||||
public withLoggedInUser(username: string): this {
|
||||
this.username = username;
|
||||
this.displayName = username;
|
||||
this.userId = 1001;
|
||||
this.authorStyle = 1;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -80,16 +83,15 @@ export class MockConnectionBuilder {
|
|||
* @throws Error if neither withGuestUser nor withLoggedInUser has been called.
|
||||
*/
|
||||
public build(): RealtimeConnection {
|
||||
const displayName = this.deriveDisplayName();
|
||||
|
||||
const transporter = new MockMessageTransporter();
|
||||
transporter.setAdapter(new MockedBackendTransportAdapter(''));
|
||||
const realtimeUserStateAdapter: RealtimeUserStatusAdapter =
|
||||
this.includeRealtimeUserStatus === RealtimeUserState.WITHOUT
|
||||
? Mock.of<RealtimeUserStatusAdapter>({})
|
||||
: new RealtimeUserStatusAdapter(
|
||||
this.username ?? null,
|
||||
displayName,
|
||||
this.username,
|
||||
this.displayName,
|
||||
this.authorStyle,
|
||||
() =>
|
||||
this.realtimeNote
|
||||
.getConnections()
|
||||
|
@ -100,18 +102,19 @@ export class MockConnectionBuilder {
|
|||
RealtimeUserState.WITH_READWRITE,
|
||||
);
|
||||
|
||||
const mockUser =
|
||||
this.username === null
|
||||
? null
|
||||
: Mock.of<User>({
|
||||
username: this.username,
|
||||
displayName: this.displayName,
|
||||
});
|
||||
const mockUser = Mock.of<User>({
|
||||
[FieldNameUser.username]: this.username,
|
||||
[FieldNameUser.displayName]: this.displayName,
|
||||
[FieldNameUser.id]: this.userId,
|
||||
[FieldNameUser.authorStyle]: this.authorStyle,
|
||||
});
|
||||
const yDocSyncServerAdapter = Mock.of<YDocSyncServerAdapter>({});
|
||||
|
||||
const connection = Mock.of<RealtimeConnection>({
|
||||
getUser: jest.fn(() => mockUser),
|
||||
getDisplayName: jest.fn(() => displayName),
|
||||
getUserId: jest.fn(() => mockUser[FieldNameUser.id]),
|
||||
getUsername: jest.fn(() => mockUser[FieldNameUser.username]),
|
||||
getAuthorStyle: jest.fn(() => mockUser[FieldNameUser.authorStyle]),
|
||||
getDisplayName: jest.fn(() => mockUser[FieldNameUser.displayName]),
|
||||
getSyncAdapter: jest.fn(() => yDocSyncServerAdapter),
|
||||
getTransporter: jest.fn(() => transporter),
|
||||
getRealtimeUserStateAdapter: () => realtimeUserStateAdapter,
|
||||
|
@ -129,14 +132,4 @@ export class MockConnectionBuilder {
|
|||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private deriveDisplayName(): string {
|
||||
if (this.displayName === undefined) {
|
||||
throw new Error(
|
||||
'Neither withGuestUser nor withLoggedInUser has been called.',
|
||||
);
|
||||
}
|
||||
|
||||
return this.displayName;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,19 +6,19 @@
|
|||
import { IncomingMessage } from 'http';
|
||||
import { Mock } from 'ts-mockery';
|
||||
|
||||
import { extractNoteIdFromRequestUrl } from './extract-note-id-from-request-url';
|
||||
import { extractNoteAliasFromRequestUrl } from './extract-note-id-from-request-url';
|
||||
|
||||
describe('extract note id from path', () => {
|
||||
it('fails if no URL is present', () => {
|
||||
const mockedRequest = Mock.of<IncomingMessage>();
|
||||
expect(() => extractNoteIdFromRequestUrl(mockedRequest)).toThrow();
|
||||
expect(() => extractNoteAliasFromRequestUrl(mockedRequest)).toThrow();
|
||||
});
|
||||
|
||||
it('can find a note id', () => {
|
||||
const mockedRequest = Mock.of<IncomingMessage>({
|
||||
url: '/realtime?noteId=somethingsomething',
|
||||
});
|
||||
expect(extractNoteIdFromRequestUrl(mockedRequest)).toBe(
|
||||
expect(extractNoteAliasFromRequestUrl(mockedRequest)).toBe(
|
||||
'somethingsomething',
|
||||
);
|
||||
});
|
||||
|
@ -27,20 +27,20 @@ describe('extract note id from path', () => {
|
|||
const mockedRequest = Mock.of<IncomingMessage>({
|
||||
url: '/realtime?nöteId=somethingsomething',
|
||||
});
|
||||
expect(() => extractNoteIdFromRequestUrl(mockedRequest)).toThrow();
|
||||
expect(() => extractNoteAliasFromRequestUrl(mockedRequest)).toThrow();
|
||||
});
|
||||
|
||||
it('fails if note id is empty', () => {
|
||||
const mockedRequest = Mock.of<IncomingMessage>({
|
||||
url: '/realtime?noteId=',
|
||||
});
|
||||
expect(() => extractNoteIdFromRequestUrl(mockedRequest)).toThrow();
|
||||
expect(() => extractNoteAliasFromRequestUrl(mockedRequest)).toThrow();
|
||||
});
|
||||
|
||||
it('fails if path is empty', () => {
|
||||
const mockedRequest = Mock.of<IncomingMessage>({
|
||||
url: '',
|
||||
});
|
||||
expect(() => extractNoteIdFromRequestUrl(mockedRequest)).toThrow();
|
||||
expect(() => extractNoteAliasFromRequestUrl(mockedRequest)).toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,17 +12,19 @@ import { IncomingMessage } from 'http';
|
|||
* @return The extracted note id
|
||||
* @throws Error if the given string isn't a valid realtime URL path
|
||||
*/
|
||||
export function extractNoteIdFromRequestUrl(request: IncomingMessage): string {
|
||||
export function extractNoteAliasFromRequestUrl(
|
||||
request: IncomingMessage,
|
||||
): string {
|
||||
if (request.url === undefined) {
|
||||
throw new Error('No URL found in request');
|
||||
}
|
||||
// A valid domain name is needed for the URL constructor, although not being used here.
|
||||
// The example.org domain should be safe to use according to RFC 6761 §6.5.
|
||||
// The example.org domain should be safe to use, according to RFC 6761 §6.5.
|
||||
const url = new URL(request.url, 'https://example.org');
|
||||
const noteId = url.searchParams.get('noteId');
|
||||
if (noteId === null || noteId === '') {
|
||||
throw new Error("Path doesn't contain parameter noteId");
|
||||
const noteAlias = url.searchParams.get('noteAlias');
|
||||
if (noteAlias === null || noteAlias === '') {
|
||||
throw new Error("Path doesn't contain parameter noteAlias");
|
||||
} else {
|
||||
return noteId;
|
||||
return noteAlias;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Mock } from 'ts-mockery';
|
|||
import { Repository } from 'typeorm';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
import { AliasModule } from '../../alias/alias.module';
|
||||
import { ApiToken } from '../../api-token/api-token.entity';
|
||||
import { Identity } from '../../auth/identity.entity';
|
||||
import { Author } from '../../authors/author.entity';
|
||||
|
@ -24,16 +25,15 @@ import { User } from '../../database/user.entity';
|
|||
import { eventModuleConfig } from '../../events';
|
||||
import { Group } from '../../groups/group.entity';
|
||||
import { LoggerModule } from '../../logger/logger.module';
|
||||
import { Alias } from '../../notes/alias.entity';
|
||||
import { Alias } from '../../notes/aliases.entity';
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { NotesModule } from '../../notes/notes.module';
|
||||
import { NotesService } from '../../notes/notes.service';
|
||||
import { NoteService } from '../../notes/note.service';
|
||||
import { Tag } from '../../notes/tag.entity';
|
||||
import { NoteGroupPermission } from '../../permissions/note-group-permission.entity';
|
||||
import { NotePermission } from '../../permissions/note-permission.enum';
|
||||
import { NotePermissionLevel } from '../../permissions/note-permission.enum';
|
||||
import { NoteUserPermission } from '../../permissions/note-user-permission.entity';
|
||||
import { PermissionService } from '../../permissions/permission.service';
|
||||
import { PermissionsModule } from '../../permissions/permissions.module';
|
||||
import { PermissionsService } from '../../permissions/permissions.service';
|
||||
import { Edit } from '../../revisions/edit.entity';
|
||||
import { Revision } from '../../revisions/revision.entity';
|
||||
import { Session } from '../../sessions/session.entity';
|
||||
|
@ -55,9 +55,9 @@ describe('Websocket gateway', () => {
|
|||
let gateway: WebsocketGateway;
|
||||
let sessionService: SessionService;
|
||||
let usersService: UsersService;
|
||||
let notesService: NotesService;
|
||||
let notesService: NoteService;
|
||||
let realtimeNoteService: RealtimeNoteService;
|
||||
let permissionsService: PermissionsService;
|
||||
let permissionsService: PermissionService;
|
||||
let mockedWebsocketConnection: RealtimeConnection;
|
||||
let mockedWebsocket: WebSocket;
|
||||
let mockedWebsocketCloseSpy: jest.SpyInstance;
|
||||
|
@ -102,7 +102,7 @@ describe('Websocket gateway', () => {
|
|||
],
|
||||
imports: [
|
||||
LoggerModule,
|
||||
NotesModule,
|
||||
AliasModule,
|
||||
PermissionsModule,
|
||||
RealtimeNoteModule,
|
||||
UsersModule,
|
||||
|
@ -150,9 +150,9 @@ describe('Websocket gateway', () => {
|
|||
gateway = module.get<WebsocketGateway>(WebsocketGateway);
|
||||
sessionService = module.get<SessionService>(SessionService);
|
||||
usersService = module.get<UsersService>(UsersService);
|
||||
notesService = module.get<NotesService>(NotesService);
|
||||
notesService = module.get<NoteService>(NoteService);
|
||||
realtimeNoteService = module.get<RealtimeNoteService>(RealtimeNoteService);
|
||||
permissionsService = module.get<PermissionsService>(PermissionsService);
|
||||
permissionsService = module.get<PermissionService>(PermissionService);
|
||||
|
||||
jest
|
||||
.spyOn(sessionService, 'extractSessionIdFromRequest')
|
||||
|
@ -209,7 +209,7 @@ describe('Websocket gateway', () => {
|
|||
groupPermissions: Promise.resolve([]),
|
||||
});
|
||||
jest
|
||||
.spyOn(notesService, 'getNoteByIdOrAlias')
|
||||
.spyOn(notesService, 'getNoteIdByAlias')
|
||||
.mockImplementation((noteId: string) => {
|
||||
if (noteExistsForNoteId && noteId === mockedValidNoteId) {
|
||||
return Promise.resolve(mockedNote);
|
||||
|
@ -224,13 +224,13 @@ describe('Websocket gateway', () => {
|
|||
jest
|
||||
.spyOn(permissionsService, 'determinePermission')
|
||||
.mockImplementation(
|
||||
async (user: User | null, note: Note): Promise<NotePermission> =>
|
||||
async (user: User | null, note: Note): Promise<NotePermissionLevel> =>
|
||||
(user === mockUser &&
|
||||
note === mockedNote &&
|
||||
userHasReadPermissions) ||
|
||||
(user === null && note === mockedGuestNote)
|
||||
? NotePermission.READ
|
||||
: NotePermission.DENY,
|
||||
? NotePermissionLevel.READ
|
||||
: NotePermissionLevel.DENY,
|
||||
);
|
||||
|
||||
const mockedRealtimeNote = Mock.of<RealtimeNote>({
|
||||
|
|
|
@ -1,28 +1,24 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import {
|
||||
DisconnectReason,
|
||||
MessageTransporter,
|
||||
userCanEdit,
|
||||
} from '@hedgedoc/commons';
|
||||
import { DisconnectReason, MessageTransporter } from '@hedgedoc/commons';
|
||||
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
|
||||
import { IncomingMessage } from 'http';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
import { User } from '../../database/user.entity';
|
||||
import { FieldNameUser } from '../../database/types';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { NotesService } from '../../notes/notes.service';
|
||||
import { NotePermission } from '../../permissions/note-permission.enum';
|
||||
import { PermissionsService } from '../../permissions/permissions.service';
|
||||
import { NoteService } from '../../notes/note.service';
|
||||
import { NotePermissionLevel } from '../../permissions/note-permission.enum';
|
||||
import { PermissionService } from '../../permissions/permission.service';
|
||||
import { SessionService } from '../../sessions/session.service';
|
||||
import { UsersService } from '../../users/users.service';
|
||||
import { RealtimeConnection } from '../realtime-note/realtime-connection';
|
||||
import { RealtimeNoteService } from '../realtime-note/realtime-note.service';
|
||||
import { BackendWebsocketAdapter } from './backend-websocket-adapter';
|
||||
import { extractNoteIdFromRequestUrl } from './utils/extract-note-id-from-request-url';
|
||||
import { extractNoteAliasFromRequestUrl } from './utils/extract-note-id-from-request-url';
|
||||
|
||||
/**
|
||||
* Gateway implementing the realtime logic required for realtime note editing.
|
||||
|
@ -31,10 +27,10 @@ import { extractNoteIdFromRequestUrl } from './utils/extract-note-id-from-reques
|
|||
export class WebsocketGateway implements OnGatewayConnection {
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
private noteService: NotesService,
|
||||
private noteService: NoteService,
|
||||
private realtimeNoteService: RealtimeNoteService,
|
||||
private userService: UsersService,
|
||||
private permissionsService: PermissionsService,
|
||||
private permissionsService: PermissionService,
|
||||
private sessionService: SessionService,
|
||||
) {
|
||||
this.logger.setContext(WebsocketGateway.name);
|
||||
|
@ -54,48 +50,53 @@ export class WebsocketGateway implements OnGatewayConnection {
|
|||
request: IncomingMessage,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const user = await this.findUserByRequestSession(request);
|
||||
const note = await this.noteService.getNoteByIdOrAlias(
|
||||
extractNoteIdFromRequestUrl(request),
|
||||
const userId = await this.findUserIdByRequestSession(request);
|
||||
if (userId === undefined) {
|
||||
clientSocket.close(DisconnectReason.SESSION_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
const noteId = await this.noteService.getNoteIdByAlias(
|
||||
extractNoteAliasFromRequestUrl(request),
|
||||
);
|
||||
|
||||
const username = user?.username ?? 'guest';
|
||||
const user = await this.userService.getUserById(userId);
|
||||
const username = user[FieldNameUser.username];
|
||||
const displayName = user[FieldNameUser.displayName];
|
||||
const authorStyle = user[FieldNameUser.authorStyle];
|
||||
|
||||
const notePermission = await this.permissionsService.determinePermission(
|
||||
user,
|
||||
note,
|
||||
userId,
|
||||
noteId,
|
||||
);
|
||||
if (notePermission < NotePermission.READ) {
|
||||
if (notePermission < NotePermissionLevel.READ) {
|
||||
this.logger.log(
|
||||
`Access denied to note '${note.id}' for user '${username}'`,
|
||||
`Access denied to note '${noteId}' for user '${userId}'`,
|
||||
'handleConnection',
|
||||
);
|
||||
clientSocket.close(DisconnectReason.USER_NOT_PERMITTED);
|
||||
return;
|
||||
}
|
||||
const acceptEdits: boolean = notePermission >= NotePermissionLevel.WRITE;
|
||||
|
||||
this.logger.debug(
|
||||
`New realtime connection to note '${note.id}' (${
|
||||
note.publicId
|
||||
}) by user '${username}' from ${
|
||||
`New realtime connection to note '${noteId}' by user '${userId}' from ${
|
||||
request.socket.remoteAddress ?? 'unknown'
|
||||
}`,
|
||||
);
|
||||
|
||||
const realtimeNote =
|
||||
await this.realtimeNoteService.getOrCreateRealtimeNote(note);
|
||||
await this.realtimeNoteService.getOrCreateRealtimeNote(noteId);
|
||||
|
||||
const websocketTransporter = new MessageTransporter();
|
||||
websocketTransporter.setAdapter(
|
||||
new BackendWebsocketAdapter(clientSocket),
|
||||
);
|
||||
|
||||
const permissions = await this.noteService.toNotePermissionsDto(note);
|
||||
const acceptEdits: boolean = userCanEdit(permissions, user?.username);
|
||||
|
||||
const connection = new RealtimeConnection(
|
||||
websocketTransporter,
|
||||
user,
|
||||
userId,
|
||||
username,
|
||||
displayName,
|
||||
authorStyle,
|
||||
realtimeNote,
|
||||
acceptEdits,
|
||||
);
|
||||
|
@ -114,30 +115,18 @@ export class WebsocketGateway implements OnGatewayConnection {
|
|||
}
|
||||
|
||||
/**
|
||||
* Finds the {@link User} whose session cookie is saved in the given {@link IncomingMessage}.
|
||||
* Finds the user id whose session cookie is saved in the given {@link IncomingMessage}.
|
||||
*
|
||||
* @param request The request that contains the session cookie
|
||||
* @return The found user
|
||||
* @return The found user id
|
||||
*/
|
||||
private async findUserByRequestSession(
|
||||
private async findUserIdByRequestSession(
|
||||
request: IncomingMessage,
|
||||
): Promise<User | null> {
|
||||
): Promise<number | undefined> {
|
||||
const sessionId = this.sessionService.extractSessionIdFromRequest(request);
|
||||
|
||||
this.logger.debug(
|
||||
'Checking if sessionId is empty',
|
||||
'findUserByRequestSession',
|
||||
);
|
||||
if (sessionId.isEmpty()) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
this.logger.debug('sessionId is not empty', 'findUserByRequestSession');
|
||||
const username = await this.sessionService.fetchUsernameForSessionId(
|
||||
sessionId.get(),
|
||||
);
|
||||
if (username === undefined) {
|
||||
return null;
|
||||
}
|
||||
return await this.userService.getUserByUsername(username);
|
||||
return await this.sessionService.getUserIdForSessionId(sessionId.get());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AliasModule } from '../../alias/alias.module';
|
||||
import { LoggerModule } from '../../logger/logger.module';
|
||||
import { NotesModule } from '../../notes/notes.module';
|
||||
import { NoteModule } from '../../notes/note.module';
|
||||
import { PermissionsModule } from '../../permissions/permissions.module';
|
||||
import { SessionModule } from '../../sessions/session.module';
|
||||
import { UsersModule } from '../../users/users.module';
|
||||
|
@ -16,8 +17,9 @@ import { WebsocketGateway } from './websocket.gateway';
|
|||
@Module({
|
||||
imports: [
|
||||
LoggerModule,
|
||||
NotesModule,
|
||||
AliasModule,
|
||||
RealtimeNoteModule,
|
||||
NoteModule,
|
||||
UsersModule,
|
||||
PermissionsModule,
|
||||
SessionModule,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue