hedgedoc/backend/src/realtime/realtime-note/realtime-note.service.ts
Tilman Vatteroth a852c79947 refactor: replace permission check methods with ordered permission enum
This commit replaces the "mayWrite", "mayRead" and "checkPermissionOnNote"
functions with one that returns a sortable permission value.
This is done because many places in the code need to do actions based on the fact if
the user has no, read or write access. If done with the may-functions then the permission
data need to be looked through multiple times.

Also, the whole check code is split into more functions that are tested separately and make it easier
to understand the process.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
2023-05-19 19:10:45 +02:00

149 lines
5.3 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Optional } from '@mrdrogdrog/optional';
import { BeforeApplicationShutdown, Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { SchedulerRegistry } from '@nestjs/schedule';
import appConfiguration, { AppConfig } from '../../config/app.config';
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 { RevisionsService } from '../../revisions/revisions.service';
import { RealtimeConnection } from './realtime-connection';
import { RealtimeNote } from './realtime-note';
import { RealtimeNoteStore } from './realtime-note-store';
@Injectable()
export class RealtimeNoteService implements BeforeApplicationShutdown {
constructor(
private revisionsService: RevisionsService,
private readonly logger: ConsoleLoggerService,
private realtimeNoteStore: RealtimeNoteStore,
private schedulerRegistry: SchedulerRegistry,
@Inject(appConfiguration.KEY)
private appConfig: AppConfig,
private permissionService: PermissionsService,
) {}
beforeApplicationShutdown(): void {
this.realtimeNoteStore
.getAllRealtimeNotes()
.forEach((realtimeNote) => realtimeNote.destroy());
}
/**
* Reads the current content from the given {@link RealtimeNote} and creates a new {@link Revision} for the linked {@link Note}.
*
* @param realtimeNote The realtime note for which a revision should be created
*/
public saveRealtimeNote(realtimeNote: RealtimeNote): void {
this.revisionsService
.createRevision(
realtimeNote.getNote(),
realtimeNote.getRealtimeDoc().getCurrentContent(),
realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
)
.catch((reason) => this.logger.error(reason));
}
/**
* 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.
* @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> {
return (
this.realtimeNoteStore.find(note.id) ??
(await this.createNewRealtimeNote(note))
);
}
/**
* Creates a new {@link RealtimeNote} for the given {@link Note}.
*
* @param note 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);
const realtimeNote = this.realtimeNoteStore.create(
note,
lastRevision.content,
lastRevision.yjsStateVector ?? undefined,
);
realtimeNote.on('beforeDestroy', () => {
this.saveRealtimeNote(realtimeNote);
});
this.startPersistTimer(realtimeNote);
return realtimeNote;
}
/**
* Starts a timer that persists the realtime note in a periodic interval depending on the {@link AppConfig}.
* @param realtimeNote The realtime note for which the timer should be started
*/
private startPersistTimer(realtimeNote: RealtimeNote): void {
Optional.of(this.appConfig.persistInterval)
.filter((value) => value > 0)
.ifPresent((persistInterval) => {
const intervalId = setInterval(
this.saveRealtimeNote.bind(this, realtimeNote),
persistInterval * 60 * 1000,
);
this.schedulerRegistry.addInterval(
`periodic-persist-${realtimeNote.getNote().id}`,
intervalId,
);
realtimeNote.on('destroy', () => {
clearInterval(intervalId);
this.schedulerRegistry.deleteInterval(
`periodic-persist-${realtimeNote.getNote().id}`,
);
});
});
}
@OnEvent(NoteEvent.PERMISSION_CHANGE)
public async handleNotePermissionChanged(note: Note): Promise<void> {
const realtimeNote = this.realtimeNoteStore.find(note.id);
if (!realtimeNote) return;
realtimeNote.announcePermissionChange();
const allConnections = realtimeNote.getConnections();
await this.updateOrCloseConnection(allConnections, note);
}
private async updateOrCloseConnection(
connections: RealtimeConnection[],
note: Note,
): Promise<void> {
for (const connection of connections) {
const permission = await this.permissionService.determinePermission(
connection.getUser(),
note,
);
if (permission === NotePermission.DENY) {
connection.getTransporter().disconnect();
} else {
connection.acceptEdits = permission > NotePermission.READ;
}
}
}
@OnEvent(NoteEvent.DELETION)
public handleNoteDeleted(noteId: Note['id']): void {
const realtimeNote = this.realtimeNoteStore.find(noteId);
if (realtimeNote) {
realtimeNote.announceNoteDeletion();
}
}
}