Skip to content

Commit

Permalink
BC-5424 - persistent storage for tldraw (#4685)
Browse files Browse the repository at this point in the history
* add env vars related to tldraw asset upload

* add temporary hack to give students permission to upload files to DrawingElement

* add recursive deletion of files for DrawingElement

---------

Co-authored-by: blazejpass <blazej.szczepanowski@gca.pass-consulting.com>
  • Loading branch information
davwas and blazejpass authored Jan 30, 2024
1 parent 01f4185 commit 46c0648
Show file tree
Hide file tree
Showing 16 changed files with 95 additions and 31 deletions.
3 changes: 1 addition & 2 deletions apps/server/src/config/database.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ interface GlobalConstants {
DB_URL: string;
DB_PASSWORD?: string;
DB_USERNAME?: string;
TLDRAW_DB_URL: string;
}

const usedGlobals: GlobalConstants = globals;

/** Database URL */
export const { DB_URL, DB_PASSWORD, DB_USERNAME, TLDRAW_DB_URL } = usedGlobals;
export const { DB_URL, DB_PASSWORD, DB_USERNAME } = usedGlobals;
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class RecursiveDeleteVisitor implements BoardCompositeVisitorAsync {

async visitDrawingElementAsync(drawingElement: DrawingElement): Promise<void> {
await this.drawingElementAdapterService.deleteDrawingBinData(drawingElement.id);
await this.filesStorageClientAdapterService.deleteFilesOfParent(drawingElement.id);

this.deleteNode(drawingElement);
await this.visitChildrenAsync(drawingElement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
BoardExternalReferenceType,
BoardRoles,
ColumnBoard,
isDrawingElement,
UserBoardRoles,
UserRoleEnum,
} from '@shared/domain/domainobject';
Expand Down Expand Up @@ -34,10 +35,12 @@ export class BoardDoAuthorizableService implements AuthorizationLoaderService {
const ids = [...ancestorIds, boardDo.id];
const rootId = ids[0];
const rootBoardDo = await this.boardDoRepo.findById(rootId, 1);
const isDrawing = isDrawingElement(boardDo);

if (rootBoardDo instanceof ColumnBoard) {
if (rootBoardDo.context?.type === BoardExternalReferenceType.Course) {
const course = await this.courseRepo.findById(rootBoardDo.context.id);
const users = this.mapCourseUsersToUsergroup(course);
const users = this.mapCourseUsersToUsergroup(course, isDrawing);
return new BoardDoAuthorizable({ users, id: boardDo.id });
}
} else {
Expand All @@ -47,7 +50,7 @@ export class BoardDoAuthorizableService implements AuthorizationLoaderService {
return new BoardDoAuthorizable({ users: [], id: boardDo.id });
}

private mapCourseUsersToUsergroup(course: Course): UserBoardRoles[] {
private mapCourseUsersToUsergroup(course: Course, isDrawing: boolean): UserBoardRoles[] {
const users = [
...course.getTeachersList().map((user) => {
return {
Expand All @@ -72,7 +75,10 @@ export class BoardDoAuthorizableService implements AuthorizationLoaderService {
userId: user.id,
firstName: user.firstName,
lastName: user.lastName,
roles: [BoardRoles.READER],
// TODO: fix this temporary hack allowing students to upload files to the DrawingElement
// linked with getElementWithWritePermission method in element.uc.ts
// this is needed to allow students to upload/delete files to/from the tldraw whiteboard (DrawingElement)
roles: isDrawing ? [BoardRoles.EDITOR] : [BoardRoles.READER],
userRoleEnum: UserRoleEnum.STUDENT,
};
}),
Expand Down
24 changes: 24 additions & 0 deletions apps/server/src/modules/board/uc/element.uc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,30 @@ describe(ElementUc.name, () => {
});
});

describe('when deleting an element which is of DrawingElement type', () => {
const setup = () => {
const user = userFactory.build();
const element = drawingElementFactory.build();

boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue(
new BoardDoAuthorizable({ users: [], id: new ObjectId().toHexString() })
);

elementService.findById.mockResolvedValueOnce(element);
return { element, user };
};

it('should authorize the user to delete the element', async () => {
const { element, user } = setup();

const boardDoAuthorizable = await boardDoAuthorizableService.getBoardAuthorizable(element);
const context = { action: Action.write, requiredPermissions: [] };
await uc.deleteElement(user.id, element.id);

expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, boardDoAuthorizable, context);
});
});

describe('when deleting a content element', () => {
const setup = () => {
const user = userFactory.build();
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/modules/board/uc/element.uc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ForbiddenException, forwardRef, Inject, Injectable, UnprocessableEntity
import {
AnyBoardDo,
AnyContentElementDo,
isDrawingElement,
isSubmissionContainerElement,
isSubmissionItem,
SubmissionItem,
Expand Down Expand Up @@ -53,6 +54,13 @@ export class ElementUc extends BaseUc {

if (isSubmissionItem(parent)) {
await this.checkSubmissionItemWritePermission(userId, parent);
} else if (isDrawingElement(element)) {
// TODO: fix this temporary hack preventing students from deleting the DrawingElement
// linked with getBoardAuthorizable method in board-do-authorizable.service.ts
// the roles are hacked for the DrawingElement to allow students for file upload
// so because students have BoardRoles.EDITOR role, they can delete the DrawingElement by calling delete endpoint directly
// to prevent this, we add UserRoleEnum.TEACHER to the requiredUserRole
await this.checkPermission(userId, element, Action.write, UserRoleEnum.TEACHER);
} else {
await this.checkPermission(userId, element, Action.write);
}
Expand Down
12 changes: 11 additions & 1 deletion apps/server/src/modules/tldraw/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Configuration } from '@hpi-schul-cloud/commons';

export interface TldrawConfig {
TLDRAW_DB_URL: string;
NEST_LOG_LEVEL: string;
INCOMING_REQUEST_TIMEOUT: number;
TLDRAW_DB_FLUSH_SIZE: string;
Expand All @@ -9,11 +10,18 @@ export interface TldrawConfig {
TLDRAW_PING_TIMEOUT: number;
TLDRAW_GC_ENABLED: number;
REDIS_URI: string;
TLDRAW_ASSETS_ENABLED: boolean;
TLDRAW_ASSETS_MAX_SIZE: number;
TLDRAW_ASSETS_ALLOWED_EXTENSIONS_LIST: string;
API_HOST: number;
TLDRAW_MAX_DOCUMENT_SIZE: number;
}

export const TLDRAW_DB_URL: string = Configuration.get('TLDRAW_DB_URL') as string;
export const TLDRAW_SOCKET_PORT = Configuration.get('TLDRAW__SOCKET_PORT') as number;

const tldrawConfig = {
TLDRAW_DB_URL,
NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string,
INCOMING_REQUEST_TIMEOUT: Configuration.get('INCOMING_REQUEST_TIMEOUT_API') as number,
TLDRAW_DB_FLUSH_SIZE: Configuration.get('TLDRAW__DB_FLUSH_SIZE') as number,
Expand All @@ -22,9 +30,11 @@ const tldrawConfig = {
TLDRAW_PING_TIMEOUT: Configuration.get('TLDRAW__PING_TIMEOUT') as number,
TLDRAW_GC_ENABLED: Configuration.get('TLDRAW__GC_ENABLED') as boolean,
REDIS_URI: Configuration.has('REDIS_URI') ? (Configuration.get('REDIS_URI') as string) : null,
TLDRAW_ASSETS_ENABLED: Configuration.get('TLDRAW__ASSETS_ENABLED') as boolean,
TLDRAW_ASSETS_MAX_SIZE: Configuration.get('TLDRAW__ASSETS_MAX_SIZE') as number,
TLDRAW_ASSETS_ALLOWED_EXTENSIONS_LIST: Configuration.get('TLDRAW__ASSETS_ALLOWED_EXTENSIONS_LIST') as string,
API_HOST: Configuration.get('API_HOST') as string,
TLDRAW_MAX_DOCUMENT_SIZE: Configuration.get('TLDRAW__MAX_DOCUMENT_SIZE') as number,
};

export const SOCKET_PORT = Configuration.get('TLDRAW__SOCKET_PORT') as number;
export const config = () => tldrawConfig;
4 changes: 2 additions & 2 deletions apps/server/src/modules/tldraw/controller/tldraw.ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { AxiosError } from 'axios';
import { firstValueFrom } from 'rxjs';
import { HttpService } from '@nestjs/axios';
import { WebsocketCloseErrorLoggable } from '../loggable';
import { TldrawConfig, SOCKET_PORT } from '../config';
import { TldrawConfig, TLDRAW_SOCKET_PORT } from '../config';
import { WsCloseCodeEnum, WsCloseMessageEnum } from '../types';
import { TldrawWsService } from '../service';

@WebSocketGateway(SOCKET_PORT)
@WebSocketGateway(TLDRAW_SOCKET_PORT)
export class TldrawWs implements OnGatewayInit, OnGatewayConnection {
@WebSocketServer()
server!: Server;
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/modules/tldraw/tldraw-ws.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Module, NotFoundException } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { createConfigModuleOptions, DB_PASSWORD, DB_USERNAME, TLDRAW_DB_URL } from '@src/config';
import { createConfigModuleOptions, DB_PASSWORD, DB_USERNAME } from '@src/config';
import { CoreModule } from '@src/core';
import { LoggerModule } from '@src/core/logger';
import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs';
Expand All @@ -11,7 +11,7 @@ import { MetricsService } from './metrics';
import { TldrawBoardRepo, TldrawRepo, YMongodb } from './repo';
import { TldrawWsService } from './service';
import { TldrawWs } from './controller';
import { config } from './config';
import { config, TLDRAW_DB_URL } from './config';
import { TldrawRedisFactory } from './redis';

const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = {
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/modules/tldraw/tldraw.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Module, NotFoundException } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { createConfigModuleOptions, DB_PASSWORD, DB_USERNAME, TLDRAW_DB_URL } from '@src/config';
import { createConfigModuleOptions, DB_PASSWORD, DB_USERNAME } from '@src/config';
import { CoreModule } from '@src/core';
import { LoggerModule } from '@src/core/logger';
import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs';
import { AuthenticationModule } from '@src/modules/authentication/authentication.module';
import { RabbitMQWrapperModule } from '@infra/rabbitmq';
import { Dictionary, IPrimaryKey } from '@mikro-orm/core';
import { AuthorizationModule } from '@modules/authorization';
import { config } from './config';
import { config, TLDRAW_DB_URL } from './config';
import { TldrawDrawing } from './entities';
import { TldrawController } from './controller';
import { TldrawService } from './service';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class ResolvedUserMapper {
dto.lastName = user.lastName;
dto.createdAt = user.createdAt;
dto.updatedAt = user.updatedAt;
dto.schoolId = user.school.toString();
dto.schoolId = user.school.id;
dto.roles = roles.map((role) => {
return { name: role.name, id: role.id };
});
Expand Down
34 changes: 25 additions & 9 deletions config/default.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1444,10 +1444,15 @@
"API_KEY": ""
}
},
"FEATURE_TLDRAW_ENABLED": {
"type": "boolean",
"default": true,
"description": "Enables tldraw feature"
},
"TLDRAW": {
"type": "object",
"description": "Tldraw managing variables.",
"required": ["PING_TIMEOUT", "SOCKET_PORT","GC_ENABLED", "DB_FLUSH_SIZE", "MAX_DOCUMENT_SIZE"],
"description": "Configuration of tldraw related settings",
"required": ["PING_TIMEOUT", "SOCKET_PORT","GC_ENABLED", "DB_FLUSH_SIZE", "MAX_DOCUMENT_SIZE", "ASSETS_ENABLED", "ASSETS_MAX_SIZE", "ASSETS_ALLOWED_EXTENSIONS_LIST"],
"properties": {
"SOCKET_PORT": {
"type": "number",
Expand All @@ -1468,26 +1473,37 @@
"MAX_DOCUMENT_SIZE": {
"type": "number",
"description": "Maximum size of a single tldraw document in mongo"
},
"ASSETS_ENABLED": {
"type": "boolean",
"description": "Enables uploading assets to tldraw board"
},
"ASSETS_MAX_SIZE": {
"type": "integer",
"description": "Maximum asset size in bytes"
},
"ASSETS_ALLOWED_EXTENSIONS_LIST": {
"type": "string",
"description": "List with allowed assets extensions, comma separated, empty if all extensions should be allowed",
"examples": ["png,jpg,jpeg,svg,webp"]
}
},
"default": {
"SOCKET_PORT": 3345,
"PING_TIMEOUT": 10000,
"PING_TIMEOUT": 30000,
"GC_ENABLED": true,
"DB_FLUSH_SIZE": 400,
"MAX_DOCUMENT_SIZE": 15000000
"MAX_DOCUMENT_SIZE": 15000000,
"ASSETS_ENABLED": true,
"ASSETS_MAX_SIZE": 25000000,
"ASSETS_ALLOWED_EXTENSIONS_LIST": ""
}
},
"TLDRAW_DB_URL": {
"type": "string",
"default": "mongodb://127.0.0.1:27017/tldraw",
"description": "DB connection url"
},
"FEATURE_TLDRAW_ENABLED": {
"type": "boolean",
"default": true,
"description": "Tldraw feature enabled"
},
"TLDRAW_URI": {
"type": "string",
"default": "http://localhost:3349",
Expand Down
5 changes: 0 additions & 5 deletions config/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,12 @@ switch (NODE_ENV) {
}

let defaultDbUrl = null;
let defaultTldrawDbUrl = null;
switch (NODE_ENV) {
case ENVIRONMENTS.TEST:
defaultDbUrl = 'mongodb://127.0.0.1:27017/schulcloud-test';
break;
default:
defaultDbUrl = 'mongodb://127.0.0.1:27017/schulcloud';
defaultTldrawDbUrl = 'mongodb://127.0.0.1:27017/tldraw';
}

const globals = {
Expand Down Expand Up @@ -106,9 +104,6 @@ const globals = {

// calendar
CALENDAR_URI: process.env.CALENDAR_URI,

// tldraw
TLDRAW_DB_URL: process.env.TLDRAW_DB_URL || defaultTldrawDbUrl,
};

// validation /////////////////////////////////////////////////
Expand Down
4 changes: 3 additions & 1 deletion config/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@
"GC_ENABLED": true,
"DB_FLUSH_SIZE": 400,
"MAX_DOCUMENT_SIZE": 15000000,
"DB_MULTIPLE_COLLECTIONS": false
"ASSETS_ENABLED": true,
"ASSETS_MAX_SIZE": 25000000,
"ASSETS_ALLOWED_EXTENSIONS_LIST": ""
},
"SCHULCONNEX_CLIENT": {
"API_URL": "http://localhost:8888/v1/",
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@
"winston": "^3.8.2",
"ws": "^8.16.0",
"y-protocols": "^1.0.6",
"yjs": "^13.6.10"
"yjs": "^13.6.11"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.352.0",
Expand Down
5 changes: 4 additions & 1 deletion src/services/config/publicAppConfigService.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,13 @@ const exposedVars = [
'FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION',
'FEATURE_CTL_CONTEXT_CONFIGURATION_ENABLED',
'FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED',
'FEATURE_TLDRAW_ENABLED',
'FEATURE_CTL_TOOLS_COPY_ENABLED',
'FEATURE_SHOW_MIGRATION_WIZARD',
'MIGRATION_WIZARD_DOCUMENTATION_LINK',
'FEATURE_TLDRAW_ENABLED',
'TLDRAW__ASSETS_ENABLED',
'TLDRAW__ASSETS_MAX_SIZE',
'TLDRAW__ASSETS_ALLOWED_EXTENSIONS_LIST',
];

/**
Expand Down

0 comments on commit 46c0648

Please sign in to comment.