diff --git a/apps/server/src/config/database.config.ts b/apps/server/src/config/database.config.ts index 17c45dd1887..ad97e4c3d66 100644 --- a/apps/server/src/config/database.config.ts +++ b/apps/server/src/config/database.config.ts @@ -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; diff --git a/apps/server/src/modules/board/repo/recursive-delete.vistor.ts b/apps/server/src/modules/board/repo/recursive-delete.vistor.ts index 3993d9da87c..8414e78f304 100644 --- a/apps/server/src/modules/board/repo/recursive-delete.vistor.ts +++ b/apps/server/src/modules/board/repo/recursive-delete.vistor.ts @@ -65,6 +65,7 @@ export class RecursiveDeleteVisitor implements BoardCompositeVisitorAsync { async visitDrawingElementAsync(drawingElement: DrawingElement): Promise { await this.drawingElementAdapterService.deleteDrawingBinData(drawingElement.id); + await this.filesStorageClientAdapterService.deleteFilesOfParent(drawingElement.id); this.deleteNode(drawingElement); await this.visitChildrenAsync(drawingElement); diff --git a/apps/server/src/modules/board/service/board-do-authorizable.service.ts b/apps/server/src/modules/board/service/board-do-authorizable.service.ts index bcdc08adfee..16a38edb656 100644 --- a/apps/server/src/modules/board/service/board-do-authorizable.service.ts +++ b/apps/server/src/modules/board/service/board-do-authorizable.service.ts @@ -6,6 +6,7 @@ import { BoardExternalReferenceType, BoardRoles, ColumnBoard, + isDrawingElement, UserBoardRoles, UserRoleEnum, } from '@shared/domain/domainobject'; @@ -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 { @@ -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 { @@ -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, }; }), diff --git a/apps/server/src/modules/board/uc/element.uc.spec.ts b/apps/server/src/modules/board/uc/element.uc.spec.ts index f520fc6444d..f628d37c03d 100644 --- a/apps/server/src/modules/board/uc/element.uc.spec.ts +++ b/apps/server/src/modules/board/uc/element.uc.spec.ts @@ -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(); diff --git a/apps/server/src/modules/board/uc/element.uc.ts b/apps/server/src/modules/board/uc/element.uc.ts index a7f978a1fb3..7f1a835ad75 100644 --- a/apps/server/src/modules/board/uc/element.uc.ts +++ b/apps/server/src/modules/board/uc/element.uc.ts @@ -3,6 +3,7 @@ import { ForbiddenException, forwardRef, Inject, Injectable, UnprocessableEntity import { AnyBoardDo, AnyContentElementDo, + isDrawingElement, isSubmissionContainerElement, isSubmissionItem, SubmissionItem, @@ -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); } diff --git a/apps/server/src/modules/tldraw/config.ts b/apps/server/src/modules/tldraw/config.ts index 9025adb9a25..9a141b5d621 100644 --- a/apps/server/src/modules/tldraw/config.ts +++ b/apps/server/src/modules/tldraw/config.ts @@ -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; @@ -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, @@ -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; diff --git a/apps/server/src/modules/tldraw/controller/tldraw.ws.ts b/apps/server/src/modules/tldraw/controller/tldraw.ws.ts index c211f6eff31..c0d36b7fc69 100644 --- a/apps/server/src/modules/tldraw/controller/tldraw.ws.ts +++ b/apps/server/src/modules/tldraw/controller/tldraw.ws.ts @@ -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; diff --git a/apps/server/src/modules/tldraw/tldraw-ws.module.ts b/apps/server/src/modules/tldraw/tldraw-ws.module.ts index 9a3552cccb4..465c1ce9f95 100644 --- a/apps/server/src/modules/tldraw/tldraw-ws.module.ts +++ b/apps/server/src/modules/tldraw/tldraw-ws.module.ts @@ -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'; @@ -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 = { diff --git a/apps/server/src/modules/tldraw/tldraw.module.ts b/apps/server/src/modules/tldraw/tldraw.module.ts index 5c43cfb5780..588afe8eb60 100644 --- a/apps/server/src/modules/tldraw/tldraw.module.ts +++ b/apps/server/src/modules/tldraw/tldraw.module.ts @@ -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'; @@ -8,7 +8,7 @@ import { AuthenticationModule } from '@src/modules/authentication/authentication 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'; diff --git a/apps/server/src/modules/user/mapper/resolved-user.mapper.ts b/apps/server/src/modules/user/mapper/resolved-user.mapper.ts index a6518b23a72..9c26b49ba32 100644 --- a/apps/server/src/modules/user/mapper/resolved-user.mapper.ts +++ b/apps/server/src/modules/user/mapper/resolved-user.mapper.ts @@ -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 }; }); diff --git a/config/default.schema.json b/config/default.schema.json index b9e85e4287b..d8c3185e2d1 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -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", @@ -1468,14 +1473,30 @@ "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": { @@ -1483,11 +1504,6 @@ "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", diff --git a/config/globals.js b/config/globals.js index 633878d1b3f..c9275419bcf 100644 --- a/config/globals.js +++ b/config/globals.js @@ -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 = { @@ -106,9 +104,6 @@ const globals = { // calendar CALENDAR_URI: process.env.CALENDAR_URI, - - // tldraw - TLDRAW_DB_URL: process.env.TLDRAW_DB_URL || defaultTldrawDbUrl, }; // validation ///////////////////////////////////////////////// diff --git a/config/test.json b/config/test.json index addbab932a3..69d8c87aaf2 100644 --- a/config/test.json +++ b/config/test.json @@ -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/", diff --git a/package-lock.json b/package-lock.json index e6d5e755d4c..01ef37d963c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -142,7 +142,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", diff --git a/package.json b/package.json index 6c21c5afcdf..18ff5988492 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/services/config/publicAppConfigService.js b/src/services/config/publicAppConfigService.js index 23133e879e1..48eafd598c8 100644 --- a/src/services/config/publicAppConfigService.js +++ b/src/services/config/publicAppConfigService.js @@ -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', ]; /**