From ad71650a2b80e2b724738c2b2652cb8775fc23e6 Mon Sep 17 00:00:00 2001 From: virgilchiriac Date: Tue, 12 Sep 2023 19:12:02 +0200 Subject: [PATCH 1/4] BC-4983 - board: POC elements as submission item children --- .../controller/board-submission.controller.ts | 46 ++++++++++++++-- .../controller/dto/card/card.response.ts | 1 + .../submission-item.response.ts | 12 ++++- .../mapper/submission-item-response.mapper.ts | 26 ++++++++- .../board/repo/board-do.builder-impl.ts | 8 ++- .../board/repo/recursive-save.visitor.ts | 40 +++++++------- .../board/service/content-element.service.ts | 14 ++++- .../modules/board/uc/submission-item.uc.ts | 54 ++++++++++++++----- .../domainobject/board/submission-item.do.ts | 15 +++++- .../domainobject/board/types/any-board-do.ts | 2 + .../board/types/any-content-element-do.ts | 8 +++ .../entity/boardnode/types/any-board-node.ts | 12 +++++ .../domain/entity/boardnode/types/index.ts | 1 + 13 files changed, 195 insertions(+), 44 deletions(-) create mode 100644 apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts diff --git a/apps/server/src/modules/board/controller/board-submission.controller.ts b/apps/server/src/modules/board/controller/board-submission.controller.ts index 8f74b05df3f..dbc7c44dfa7 100644 --- a/apps/server/src/modules/board/controller/board-submission.controller.ts +++ b/apps/server/src/modules/board/controller/board-submission.controller.ts @@ -1,5 +1,15 @@ -import { Body, Controller, ForbiddenException, Get, HttpCode, NotFoundException, Param, Patch } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + ForbiddenException, + Get, + HttpCode, + NotFoundException, + Param, + Patch, + Post, +} from '@nestjs/common'; +import { ApiExtraModels, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; import { ICurrentUser } from '@src/modules/authentication'; import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; @@ -7,12 +17,18 @@ import { CardUc } from '../uc'; import { ElementUc } from '../uc/element.uc'; import { SubmissionItemUc } from '../uc/submission-item.uc'; import { + AnyContentElementResponse, + CardUrlParams, + CreateContentElementBodyParams, + FileElementResponse, + RichTextElementResponse, + SubmissionContainerElementResponse, SubmissionContainerUrlParams, SubmissionItemResponse, SubmissionItemUrlParams, UpdateSubmissionItemBodyParams, } from './dto'; -import { SubmissionItemResponseMapper } from './mapper'; +import { ContentElementResponseFactory, SubmissionItemResponseMapper } from './mapper'; @ApiTags('Board Submission') @Authenticate('jwt') @@ -56,4 +72,28 @@ export class BoardSubmissionController { bodyParams.completed ); } + + @ApiOperation({ summary: 'Create a new element in a submission item.' }) + @ApiExtraModels(RichTextElementResponse, FileElementResponse) + @ApiResponse({ + status: 201, + schema: { + oneOf: [{ $ref: getSchemaPath(RichTextElementResponse) }, { $ref: getSchemaPath(FileElementResponse) }], + }, + }) + @ApiResponse({ status: 400, type: ApiValidationError }) + @ApiResponse({ status: 403, type: ForbiddenException }) + @ApiResponse({ status: 404, type: NotFoundException }) + @Post(':submissionItemId/elements') + async createElement( + @Param() urlParams: SubmissionItemUrlParams, + @Body() bodyParams: CreateContentElementBodyParams, + @CurrentUser() currentUser: ICurrentUser + ): Promise { + const { type } = bodyParams; + const element = await this.submissionItemUc.createElement(currentUser.userId, urlParams.submissionItemId, type); + const response = ContentElementResponseFactory.mapToResponse(element); + + return response; + } } diff --git a/apps/server/src/modules/board/controller/dto/card/card.response.ts b/apps/server/src/modules/board/controller/dto/card/card.response.ts index 8ff034ad093..06c82c81b1f 100644 --- a/apps/server/src/modules/board/controller/dto/card/card.response.ts +++ b/apps/server/src/modules/board/controller/dto/card/card.response.ts @@ -31,6 +31,7 @@ export class CardResponse { @ApiProperty({ type: 'array', items: { + // TODO why only RichText ? oneOf: [{ $ref: getSchemaPath(RichTextElementResponse) }], }, }) diff --git a/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts b/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts index 02c3936d843..d5faa56e36d 100644 --- a/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts +++ b/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts @@ -1,13 +1,15 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { FileElementResponse, RichTextElementResponse } from '@src/modules/board/controller/dto'; import { TimestampsResponse } from '../timestamps.response'; import { UserDataResponse } from '../user-data.response'; export class SubmissionItemResponse { - constructor({ id, timestamps, completed, userData }: SubmissionItemResponse) { + constructor({ id, timestamps, completed, userData, elements }: SubmissionItemResponse) { this.id = id; this.timestamps = timestamps; this.completed = completed; this.userData = userData; + this.elements = elements; } @ApiProperty({ pattern: '[a-f0-9]{24}' }) @@ -21,4 +23,10 @@ export class SubmissionItemResponse { @ApiProperty() userData: UserDataResponse; + + @ApiProperty({ + type: 'array', + // TODO add types + }) + elements: (RichTextElementResponse | FileElementResponse)[]; } diff --git a/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts index c2c613da8c6..3c6da1a193f 100644 --- a/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts @@ -1,4 +1,15 @@ -import { SubmissionItem } from '@shared/domain'; +import { + FileElement, + isContent, + isFileElement, + isRichTextElement, + RichTextElement, + SubmissionItem, +} from '@shared/domain'; +import { ContentElementResponseFactory } from '@src/modules/board/controller/mapper/content-element-response.factory'; +import { FileElementResponseMapper } from '@src/modules/board/controller/mapper/file-element-response.mapper'; +import { RichTextElementResponseMapper } from '@src/modules/board/controller/mapper/rich-text-element-response.mapper'; +import { UnprocessableEntityException } from '@nestjs/common'; import { SubmissionItemResponse, TimestampsResponse, UserDataResponse } from '../dto'; export class SubmissionItemResponseMapper { @@ -13,6 +24,8 @@ export class SubmissionItemResponseMapper { } public mapToResponse(submissionItem: SubmissionItem): SubmissionItemResponse { + const children: (FileElement | RichTextElement)[] = submissionItem.children.filter(isContent); + const result = new SubmissionItemResponse({ completed: submissionItem.completed, id: submissionItem.id, @@ -26,6 +39,17 @@ export class SubmissionItemResponseMapper { lastName: 'Mr Doe', userId: submissionItem.userId, }), + elements: children.map((element) => { + if (isFileElement(element)) { + const mapper = FileElementResponseMapper.getInstance(); + return mapper.mapToResponse(element); + } + if (isRichTextElement(element)) { + const mapper = RichTextElementResponseMapper.getInstance(); + return mapper.mapToResponse(element); + } + throw new UnprocessableEntityException(); + }), }); return result; diff --git a/apps/server/src/modules/board/repo/board-do.builder-impl.ts b/apps/server/src/modules/board/repo/board-do.builder-impl.ts index ec13cf8da0d..70dc0da4f5a 100644 --- a/apps/server/src/modules/board/repo/board-do.builder-impl.ts +++ b/apps/server/src/modules/board/repo/board-do.builder-impl.ts @@ -130,7 +130,11 @@ export class BoardDoBuilderImpl implements BoardDoBuilder { } public buildSubmissionItem(boardNode: SubmissionItemNode): SubmissionItem { - this.ensureLeafNode(boardNode); + this.ensureBoardNodeType(this.getChildren(boardNode), [ + BoardNodeType.FILE_ELEMENT, + BoardNodeType.RICH_TEXT_ELEMENT, + ]); + const elements = this.buildChildren(boardNode); const element = new SubmissionItem({ id: boardNode.id, @@ -138,7 +142,7 @@ export class BoardDoBuilderImpl implements BoardDoBuilder { updatedAt: boardNode.updatedAt, completed: boardNode.completed, userId: boardNode.userId, - children: [], + children: elements, }); return element; } diff --git a/apps/server/src/modules/board/repo/recursive-save.visitor.ts b/apps/server/src/modules/board/repo/recursive-save.visitor.ts index ed47ac2d128..42dd01b79f6 100644 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.ts +++ b/apps/server/src/modules/board/repo/recursive-save.visitor.ts @@ -2,6 +2,7 @@ import { Utils } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; import { AnyBoardDo, + AnyBoardNode, BoardCompositeVisitor, BoardNode, Card, @@ -57,8 +58,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { context: columnBoard.context, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(columnBoard, boardNode); + this.saveRecursive(boardNode, columnBoard); } visitColumn(column: Column): void { @@ -71,8 +71,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(column, boardNode); + this.saveRecursive(boardNode, column); } visitCard(card: Card): void { @@ -86,8 +85,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(card, boardNode); + this.saveRecursive(boardNode, card); } visitFileElement(fileElement: FileElement): void { @@ -100,8 +98,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(fileElement, boardNode); + this.saveRecursive(boardNode, fileElement); } visitRichTextElement(richTextElement: RichTextElement): void { @@ -115,8 +112,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(richTextElement, boardNode); + this.saveRecursive(boardNode, richTextElement); } visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void { @@ -129,22 +125,20 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(submissionContainerElement, boardNode); + this.saveRecursive(boardNode, submissionContainerElement); } - visitSubmissionItem(submission: SubmissionItem): void { - const parentData = this.parentsMap.get(submission.id); + visitSubmissionItem(submissionItem: SubmissionItem): void { + const parentData = this.parentsMap.get(submissionItem.id); const boardNode = new SubmissionItemNode({ - id: submission.id, + id: submissionItem.id, parent: parentData?.boardNode, position: parentData?.position, - completed: submission.completed, - userId: submission.userId, + completed: submissionItem.completed, + userId: submissionItem.userId, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(submission, boardNode); + this.saveRecursive(boardNode, submissionItem); } visitChildren(parent: AnyBoardDo, parentNode: BoardNode) { @@ -154,7 +148,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { }); } - registerParentData(parent: AnyBoardDo, child: AnyBoardDo, parentNode: BoardNode) { + private registerParentData(parent: AnyBoardDo, child: AnyBoardDo, parentNode: BoardNode) { const position = parent.children.findIndex((obj) => obj.id === child.id); if (position === -1) { throw new Error(`Cannot get child position. Child doesnt belong to parent`); @@ -162,6 +156,12 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { this.parentsMap.set(child.id, { boardNode: parentNode, position }); } + private saveRecursive(anyBoardNode: AnyBoardNode, anyBoardDo: AnyBoardDo): void { + this.createOrUpdateBoardNode(anyBoardNode); + this.visitChildren(anyBoardDo, anyBoardNode); + } + + // TODO make private createOrUpdateBoardNode(boardNode: BoardNode): void { const existing = this.em.getUnitOfWork().getById(BoardNode.name, boardNode.id); if (existing) { diff --git a/apps/server/src/modules/board/service/content-element.service.ts b/apps/server/src/modules/board/service/content-element.service.ts index 1c92968ec15..74ff7fa5f01 100644 --- a/apps/server/src/modules/board/service/content-element.service.ts +++ b/apps/server/src/modules/board/service/content-element.service.ts @@ -6,6 +6,8 @@ import { ContentElementType, EntityId, isAnyContentElement, + SubmissionContainerElement, + SubmissionItem, } from '@shared/domain'; import { FileContentBody, RichTextContentBody, SubmissionContainerContentBody } from '../controller/dto'; import { BoardDoRepo } from '../repo'; @@ -30,7 +32,17 @@ export class ContentElementService { return element; } - async create(parent: Card, type: ContentElementType): Promise { + async findSubmissionContainerElement(elementId: EntityId): Promise { + const element = await this.boardDoRepo.findById(elementId); + + if (!(element instanceof SubmissionContainerElement)) { + throw new NotFoundException(`There is no '${element.constructor.name}' with this id`); + } + + return element; + } + + async create(parent: Card | SubmissionItem, type: ContentElementType): Promise { const element = this.contentElementFactory.build(type); parent.addChild(element); await this.boardDoRepo.save(parent.children, parent); diff --git a/apps/server/src/modules/board/uc/submission-item.uc.ts b/apps/server/src/modules/board/uc/submission-item.uc.ts index ec15b9322e3..77c7db6113b 100644 --- a/apps/server/src/modules/board/uc/submission-item.uc.ts +++ b/apps/server/src/modules/board/uc/submission-item.uc.ts @@ -1,5 +1,23 @@ -import { ForbiddenException, forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { AnyBoardDo, EntityId, SubmissionContainerElement, SubmissionItem, UserRoleEnum } from '@shared/domain'; +import { + BadRequestException, + ForbiddenException, + forwardRef, + HttpException, + HttpStatus, + Inject, + Injectable, +} from '@nestjs/common'; +import { + AnyBoardDo, + ContentElementType, + EntityId, + FileElement, + isSumbmissionItem, + RichTextElement, + SubmissionContainerElement, + SubmissionItem, + UserRoleEnum, +} from '@shared/domain'; import { Logger } from '@src/core/logger'; import { AuthorizationService } from '@src/modules/authorization'; import { Action } from '@src/modules/authorization/types/action.enum'; @@ -22,14 +40,7 @@ export class SubmissionItemUc { const submissionContainer = await this.getSubmissionContainer(submissionContainerId); await this.checkPermission(userId, submissionContainer, Action.read); - let submissionItems = submissionContainer.children as SubmissionItem[]; - - if (!submissionItems.every((child) => child instanceof SubmissionItem)) { - throw new HttpException( - 'Children of submission-container-element must be of type submission-item', - HttpStatus.UNPROCESSABLE_ENTITY - ); - } + let submissionItems: SubmissionItem[] = submissionContainer.children.filter(isSumbmissionItem); const isAuthorizedStudent = await this.isAuthorizedStudent(userId, submissionContainer); if (isAuthorizedStudent) { @@ -56,6 +67,25 @@ export class SubmissionItemUc { return submissionItem; } + async createElement( + userId: EntityId, + submissionItemId: EntityId, + type: ContentElementType + ): Promise { + if (type !== ContentElementType.RICH_TEXT && type !== ContentElementType.FILE) { + throw new BadRequestException(); + } + + const submissionItem = await this.submissionItemService.findById(submissionItemId); + + // TODO + // await this.checkPermission(userId, submissionItem, Action.write); + + const element = await this.elementService.create(submissionItem, type); + // TODO + return element as FileElement | RichTextElement; + } + private async isAuthorizedStudent(userId: EntityId, boardDo: AnyBoardDo): Promise { const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo); const userRoleEnum = boardDoAuthorizable.users.find((u) => u.userId === userId)?.userRoleEnum; @@ -73,9 +103,7 @@ export class SubmissionItemUc { } private async getSubmissionContainer(submissionContainerId: EntityId): Promise { - const submissionContainer = (await this.elementService.findById( - submissionContainerId - )) as SubmissionContainerElement; + const submissionContainer = await this.elementService.findSubmissionContainerElement(submissionContainerId); if (!(submissionContainer instanceof SubmissionContainerElement)) { throw new HttpException('Id does not belong to a submission container', HttpStatus.UNPROCESSABLE_ENTITY); diff --git a/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts b/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts index c21c2d60af4..4843a8ad52a 100644 --- a/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts @@ -1,8 +1,17 @@ -import { EntityId } from '@shared/domain'; +import { EntityId, FileElement, isFileElement, isRichTextElement, RichTextElement } from '@shared/domain'; import { BoardComposite, BoardCompositeProps } from './board-composite.do'; import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; export class SubmissionItem extends BoardComposite { + /* get children(): (RichTextElement | FileElement)[] { + const { children } = this.props; + if (!children) return []; + + const filteredChildren = children.filter(isFileElement || isRichTextElement); + + return filteredChildren; + } */ + get completed(): boolean { return this.props.completed; } @@ -22,7 +31,9 @@ export class SubmissionItem extends BoardComposite { // eslint-disable-next-line @typescript-eslint/no-unused-vars isAllowedAsChild(child: AnyBoardDo): boolean { // Currently submission-item rejects any children, will open in the future - return false; + const allowed = child instanceof FileElement || child instanceof RichTextElement; + + return allowed; } accept(visitor: BoardCompositeVisitor): void { diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts index c3504070f36..c6634e9aa0f 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts +++ b/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts @@ -5,3 +5,5 @@ import { SubmissionItem } from '../submission-item.do'; import { AnyContentElementDo } from './any-content-element-do'; export type AnyBoardDo = ColumnBoard | Column | Card | AnyContentElementDo | SubmissionItem; + +export const isSumbmissionItem = (element: AnyBoardDo): element is SubmissionItem => element instanceof SubmissionItem; diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts index d0fc5ac15a7..ecb20683181 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts +++ b/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts @@ -13,3 +13,11 @@ export const isAnyContentElement = (element: AnyBoardDo): element is AnyContentE return result; }; + +export const isFileElement = (element: AnyBoardDo): element is FileElement => element instanceof FileElement; + +export const isRichTextElement = (element: AnyBoardDo): element is RichTextElement => + element instanceof RichTextElement; + +export const isContent = (element: AnyBoardDo): element is RichTextElement | FileElement => + element instanceof RichTextElement || element instanceof FileElement; diff --git a/apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts b/apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts new file mode 100644 index 00000000000..1b6a3991475 --- /dev/null +++ b/apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts @@ -0,0 +1,12 @@ +import { + CardNode, + ColumnBoardNode, + ColumnNode, + FileElementNode, + RichTextElementNode, + SubmissionContainerElementNode, + SubmissionItemNode, +} from '@shared/domain'; + +export type AnyElementNode = FileElementNode | RichTextElementNode | SubmissionContainerElementNode; +export type AnyBoardNode = ColumnBoardNode | ColumnNode | CardNode | AnyElementNode | SubmissionItemNode; diff --git a/apps/server/src/shared/domain/entity/boardnode/types/index.ts b/apps/server/src/shared/domain/entity/boardnode/types/index.ts index e2ead1d7ef0..d1f0e3bfb9a 100644 --- a/apps/server/src/shared/domain/entity/boardnode/types/index.ts +++ b/apps/server/src/shared/domain/entity/boardnode/types/index.ts @@ -1,2 +1,3 @@ export * from './board-node-type'; export * from './board-do.builder'; +export * from './any-board-node'; From c488b6f7ad92625ccf55bb623b88b1a1469f6db2 Mon Sep 17 00:00:00 2001 From: virgilchiriac Date: Wed, 27 Sep 2023 14:48:36 +0200 Subject: [PATCH 2/4] cleanup --- .../domainobject/board/types/any-content-element-do.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts index f92cc622a06..5bd7f3860bf 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts +++ b/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts @@ -16,10 +16,5 @@ export const isAnyContentElement = (element: AnyBoardDo): element is AnyContentE return result; }; -export const isFileElement = (element: AnyBoardDo): element is FileElement => element instanceof FileElement; - -export const isRichTextElement = (element: AnyBoardDo): element is RichTextElement => - element instanceof RichTextElement; - export const isContent = (element: AnyBoardDo): element is RichTextElement | FileElement => element instanceof RichTextElement || element instanceof FileElement; From 5e99894bdd65da4c120916d279775f41240a1636 Mon Sep 17 00:00:00 2001 From: virgilchiriac Date: Wed, 4 Oct 2023 10:27:51 +0200 Subject: [PATCH 3/4] BC-4983 - update sub element --- .../update-element-content.body.params.ts | 4 +- .../modules/board/controller/mapper/index.ts | 1 + .../board/service/content-element.service.ts | 9 ++++ .../server/src/modules/board/uc/element.uc.ts | 42 ++++++++++++------- .../modules/board/uc/submission-item.uc.ts | 17 ++++---- .../domainobject/board/submission-item.do.ts | 9 +++- 6 files changed, 59 insertions(+), 23 deletions(-) diff --git a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts index 05856e9ef5f..d782b50b1e6 100644 --- a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts @@ -38,7 +38,9 @@ export class RichTextContentBody { text!: string; @IsEnum(InputFormat) - @ApiProperty() + @ApiProperty({ + enum: InputFormat, + }) inputFormat!: InputFormat; } diff --git a/apps/server/src/modules/board/controller/mapper/index.ts b/apps/server/src/modules/board/controller/mapper/index.ts index 116692df5a4..24b6ccb0943 100644 --- a/apps/server/src/modules/board/controller/mapper/index.ts +++ b/apps/server/src/modules/board/controller/mapper/index.ts @@ -3,6 +3,7 @@ export * from './card-response.mapper'; export * from './column-response.mapper'; export * from './content-element-response.factory'; export * from './rich-text-element-response.mapper'; +export * from './file-element-response.mapper'; export * from './submission-container-element-response.mapper'; export * from './submission-item-response.mapper'; export * from './external-tool-element-response.mapper'; diff --git a/apps/server/src/modules/board/service/content-element.service.ts b/apps/server/src/modules/board/service/content-element.service.ts index 8cc2820bd1a..f9f90f1ca7b 100644 --- a/apps/server/src/modules/board/service/content-element.service.ts +++ b/apps/server/src/modules/board/service/content-element.service.ts @@ -1,5 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { + AnyBoardDo, AnyContentElementDo, Card, ContentElementFactory, @@ -32,6 +33,14 @@ export class ContentElementService { return element; } + async findParentOfId(elementId: EntityId): Promise { + const parent = await this.boardDoRepo.findParentOfId(elementId); + if (!parent) { + throw new NotFoundException('There is no node with this id'); + } + return parent; + } + async findSubmissionContainerElement(elementId: EntityId): Promise { const element = await this.boardDoRepo.findById(elementId); diff --git a/apps/server/src/modules/board/uc/element.uc.ts b/apps/server/src/modules/board/uc/element.uc.ts index 0dafd9eb98f..1674825bc90 100644 --- a/apps/server/src/modules/board/uc/element.uc.ts +++ b/apps/server/src/modules/board/uc/element.uc.ts @@ -1,4 +1,12 @@ -import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { + ForbiddenException, + forwardRef, + HttpException, + HttpStatus, + Inject, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; import { AnyBoardDo, EntityId, @@ -8,8 +16,7 @@ import { UserRoleEnum, } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; -import { Action } from '@src/modules/authorization/types/action.enum'; +import { Action, AuthorizationService } from '@src/modules/authorization'; import { AnyElementContentBody } from '../controller/dto'; import { BoardDoAuthorizableService, ContentElementService } from '../service'; import { SubmissionItemService } from '../service/submission-item.service'; @@ -30,8 +37,13 @@ export class ElementUc { async updateElementContent(userId: EntityId, elementId: EntityId, content: AnyElementContentBody) { const element = await this.elementService.findById(elementId); - await this.checkPermission(userId, element, Action.write); + const parent = await this.elementService.findParentOfId(elementId); + if (isSubmissionItem(parent)) { + await this.checkSubmissionPermission(userId, element, parent); + } else { + await this.checkPermission(userId, element, Action.write); + } await this.elementService.update(element, content); } @@ -43,16 +55,12 @@ export class ElementUc { const submissionContainerElement = await this.elementService.findById(contentElementId); if (!isSubmissionContainerElement(submissionContainerElement)) { - throw new HttpException( - 'Cannot create submission-item for non submission-container-element', - HttpStatus.UNPROCESSABLE_ENTITY - ); + throw new UnprocessableEntityException('Cannot create submission-item for non submission-container-element'); } if (!submissionContainerElement.children.every((child) => isSubmissionItem(child))) { - throw new HttpException( - 'Children of submission-container-element must be of type submission-item', - HttpStatus.UNPROCESSABLE_ENTITY + throw new UnprocessableEntityException( + 'Children of submission-container-element must be of type submission-item' ); } @@ -60,9 +68,8 @@ export class ElementUc { .filter(isSubmissionItem) .find((item) => item.userId === userId); if (userSubmissionExists) { - throw new HttpException( - 'User is not allowed to have multiple submission-items per submission-container-element', - HttpStatus.NOT_ACCEPTABLE + throw new ForbiddenException( + 'User is not allowed to have multiple submission-items per submission-container-element' ); } @@ -86,4 +93,11 @@ export class ElementUc { return this.authorizationService.checkPermission(user, boardDoAuthorizable, context); } + + private async checkSubmissionPermission(userId: EntityId, element: AnyBoardDo, parent: SubmissionItem) { + if (parent.userId !== userId) { + throw new ForbiddenException(); + } + await this.checkPermission(userId, parent, Action.read, UserRoleEnum.STUDENT); + } } diff --git a/apps/server/src/modules/board/uc/submission-item.uc.ts b/apps/server/src/modules/board/uc/submission-item.uc.ts index 91270449cad..d384da71465 100644 --- a/apps/server/src/modules/board/uc/submission-item.uc.ts +++ b/apps/server/src/modules/board/uc/submission-item.uc.ts @@ -6,6 +6,7 @@ import { HttpStatus, Inject, Injectable, + UnprocessableEntityException, } from '@nestjs/common'; import { AnyBoardDo, @@ -44,7 +45,7 @@ export class SubmissionItemUc { const submissionContainerElement = await this.elementService.findById(submissionContainerId); if (!isSubmissionContainerElement(submissionContainerElement)) { - throw new HttpException('Id is not submission container', HttpStatus.UNPROCESSABLE_ENTITY); + throw new UnprocessableEntityException('Id is not belong to a submission container'); } await this.checkPermission(userId, submissionContainerElement, Action.read); @@ -69,12 +70,7 @@ export class SubmissionItemUc { completed: boolean ): Promise { const submissionItem = await this.submissionItemService.findById(submissionItemId); - - await this.checkPermission(userId, submissionItem, Action.read, UserRoleEnum.STUDENT); - if (submissionItem.userId !== userId) { - throw new ForbiddenException(); - } - + await this.checkSubmissionItemPermission(userId, submissionItem); await this.submissionItemService.update(submissionItem, completed); return submissionItem; @@ -115,6 +111,13 @@ export class SubmissionItemUc { return false; } + private async checkSubmissionItemPermission(userId: EntityId, submissionItem: SubmissionItem) { + await this.checkPermission(userId, submissionItem, Action.read, UserRoleEnum.STUDENT); + if (submissionItem.userId !== userId) { + throw new ForbiddenException(); + } + } + private async checkPermission( userId: EntityId, boardDo: AnyBoardDo, diff --git a/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts b/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts index 00c8e9a74be..ce5f9dcbdcb 100644 --- a/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts @@ -1,4 +1,11 @@ -import { EntityId, FileElement, isFileElement, isRichTextElement, RichTextElement } from '@shared/domain'; +import { + EntityId, + FileElement, + isFileElement, + isRichTextElement, + isSumbmissionItem, + RichTextElement, +} from '@shared/domain'; import { BoardComposite, BoardCompositeProps } from './board-composite.do'; import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; From dbf226773ffa985b497b19dc84c70a686543f5a5 Mon Sep 17 00:00:00 2001 From: virgilchiriac Date: Fri, 13 Oct 2023 13:39:56 +0200 Subject: [PATCH 4/4] refactor UC and cleanup --- .../submission-item-create.api.spec.ts | 2 +- .../submission-item.response.ts | 2 +- .../board/controller/element.controller.ts | 2 +- .../modules/board/controller/mapper/index.ts | 1 - .../mapper/submission-item-response.mapper.ts | 8 +- .../board/repo/recursive-save.visitor.ts | 7 +- .../board/service/content-element.service.ts | 11 --- apps/server/src/modules/board/uc/base.uc.ts | 51 +++++++++++++ apps/server/src/modules/board/uc/board.uc.ts | 26 ++----- apps/server/src/modules/board/uc/card.uc.ts | 16 ++-- .../server/src/modules/board/uc/element.uc.ts | 33 ++------ .../modules/board/uc/submission-item.uc.ts | 75 ++++--------------- .../domainobject/board/submission-item.do.ts | 25 ++----- .../domainobject/board/types/any-board-do.ts | 2 - .../board/types/any-content-element-do.ts | 3 - .../entity/boardnode/types/any-board-node.ts | 10 ++- 16 files changed, 108 insertions(+), 166 deletions(-) create mode 100644 apps/server/src/modules/board/uc/base.uc.ts diff --git a/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts index 49cd3af657c..d594ca235fe 100644 --- a/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts @@ -142,7 +142,7 @@ describe('submission create (api)', () => { expect(response.status).toBe(201); const response2 = await loggedInClient.post(`${submissionContainerNode.id}/submissions`, { completed: false }); - expect(response2.status).toBe(406); + expect(response2.status).toBe(403); }); }); diff --git a/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts b/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts index a85681879b6..1962e9bed28 100644 --- a/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts +++ b/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { TimestampsResponse } from '../timestamps.response'; -import { FileElementResponse, RichTextElementResponse } from '..'; +import { FileElementResponse, RichTextElementResponse } from '../element'; export class SubmissionItemResponse { constructor({ id, timestamps, completed, userId, elements }: SubmissionItemResponse) { diff --git a/apps/server/src/modules/board/controller/element.controller.ts b/apps/server/src/modules/board/controller/element.controller.ts index 2dacd2cf539..ef5aab6d3d6 100644 --- a/apps/server/src/modules/board/controller/element.controller.ts +++ b/apps/server/src/modules/board/controller/element.controller.ts @@ -134,7 +134,7 @@ export class ElementController { bodyParams.completed ); const mapper = SubmissionItemResponseMapper.getInstance(); - const response = mapper.mapSubmissionsToResponse(submissionItem); + const response = mapper.mapSubmissionItemToResponse(submissionItem); return response; } diff --git a/apps/server/src/modules/board/controller/mapper/index.ts b/apps/server/src/modules/board/controller/mapper/index.ts index 84fbbfe1da8..a24a905ae3f 100644 --- a/apps/server/src/modules/board/controller/mapper/index.ts +++ b/apps/server/src/modules/board/controller/mapper/index.ts @@ -6,6 +6,5 @@ export * from './external-tool-element-response.mapper'; export * from './file-element-response.mapper'; export * from './link-element-response.mapper'; export * from './rich-text-element-response.mapper'; -export * from './file-element-response.mapper'; export * from './submission-container-element-response.mapper'; export * from './submission-item-response.mapper'; diff --git a/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts index b9ef34b5338..a556d1b32f2 100644 --- a/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts @@ -1,8 +1,8 @@ import { FileElement, - isContent, isFileElement, isRichTextElement, + isSubmissionItemContent, RichTextElement, SubmissionItem, UserBoardRoles, @@ -26,7 +26,7 @@ export class SubmissionItemResponseMapper { public mapToResponse(submissionItems: SubmissionItem[], users: UserBoardRoles[]): SubmissionsResponse { const submissionItemsResponse: SubmissionItemResponse[] = submissionItems.map((item) => - this.mapSubmissionsToResponse(item) + this.mapSubmissionItemToResponse(item) ); const usersResponse: UserDataResponse[] = users.map((user) => this.mapUsersToResponse(user)); @@ -35,8 +35,8 @@ export class SubmissionItemResponseMapper { return response; } - public mapSubmissionsToResponse(submissionItem: SubmissionItem): SubmissionItemResponse { - const children: (FileElement | RichTextElement)[] = submissionItem.children.filter(isContent); + public mapSubmissionItemToResponse(submissionItem: SubmissionItem): SubmissionItemResponse { + const children: (FileElement | RichTextElement)[] = submissionItem.children.filter(isSubmissionItemContent); const result = new SubmissionItemResponse({ completed: submissionItem.completed, id: submissionItem.id, diff --git a/apps/server/src/modules/board/repo/recursive-save.visitor.ts b/apps/server/src/modules/board/repo/recursive-save.visitor.ts index ea7f1aa0ba8..699ea6c3958 100644 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.ts +++ b/apps/server/src/modules/board/repo/recursive-save.visitor.ts @@ -2,7 +2,6 @@ import { Utils } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; import { AnyBoardDo, - AnyBoardNode, BoardCompositeVisitor, BoardNode, Card, @@ -194,9 +193,9 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { this.parentsMap.set(child.id, { boardNode: parentNode, position }); } - private saveRecursive(anyBoardNode: AnyBoardNode, anyBoardDo: AnyBoardDo): void { - this.createOrUpdateBoardNode(anyBoardNode); - this.visitChildren(anyBoardDo, anyBoardNode); + private saveRecursive(boardNode: BoardNode, anyBoardDo: AnyBoardDo): void { + this.createOrUpdateBoardNode(boardNode); + this.visitChildren(anyBoardDo, boardNode); } // TODO make private diff --git a/apps/server/src/modules/board/service/content-element.service.ts b/apps/server/src/modules/board/service/content-element.service.ts index 10679e416a1..4404f51fc3e 100644 --- a/apps/server/src/modules/board/service/content-element.service.ts +++ b/apps/server/src/modules/board/service/content-element.service.ts @@ -7,7 +7,6 @@ import { ContentElementType, EntityId, isAnyContentElement, - SubmissionContainerElement, SubmissionItem, } from '@shared/domain'; import { AnyElementContentBody } from '../controller/dto'; @@ -43,16 +42,6 @@ export class ContentElementService { return parent; } - async findSubmissionContainerElement(elementId: EntityId): Promise { - const element = await this.boardDoRepo.findById(elementId); - - if (!(element instanceof SubmissionContainerElement)) { - throw new NotFoundException(`There is no '${element.constructor.name}' with this id`); - } - - return element; - } - async create(parent: Card | SubmissionItem, type: ContentElementType): Promise { const element = this.contentElementFactory.build(type); parent.addChild(element); diff --git a/apps/server/src/modules/board/uc/base.uc.ts b/apps/server/src/modules/board/uc/base.uc.ts new file mode 100644 index 00000000000..f7b77d9107e --- /dev/null +++ b/apps/server/src/modules/board/uc/base.uc.ts @@ -0,0 +1,51 @@ +import { AnyBoardDo, EntityId, SubmissionItem, UserRoleEnum } from '@shared/domain'; +import { ForbiddenException, forwardRef, Inject } from '@nestjs/common'; +import { Action, AuthorizationService } from '../../authorization'; +import { BoardDoAuthorizableService } from '../service'; + +export abstract class BaseUc { + constructor( + @Inject(forwardRef(() => AuthorizationService)) + protected readonly authorizationService: AuthorizationService, + protected readonly boardDoAuthorizableService: BoardDoAuthorizableService + ) {} + + protected async checkPermission( + userId: EntityId, + boardDo: AnyBoardDo, + action: Action, + requiredUserRole?: UserRoleEnum + ): Promise { + const user = await this.authorizationService.getUserWithPermissions(userId); + const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo); + if (requiredUserRole) { + boardDoAuthorizable.requiredUserRole = requiredUserRole; + } + const context = { action, requiredPermissions: [] }; + + return this.authorizationService.checkPermission(user, boardDoAuthorizable, context); + } + + protected async isAuthorizedStudent(userId: EntityId, boardDo: AnyBoardDo): Promise { + const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo); + const userRoleEnum = boardDoAuthorizable.users.find((u) => u.userId === userId)?.userRoleEnum; + + if (!userRoleEnum) { + throw new ForbiddenException('User not part of this board'); + } + + // TODO do this with permission instead of role and using authorizable rules + if (userRoleEnum === UserRoleEnum.STUDENT) { + return true; + } + + return false; + } + + protected async checkSubmissionItemEditPermission(userId: EntityId, submissionItem: SubmissionItem) { + if (submissionItem.userId !== userId) { + throw new ForbiddenException(); + } + await this.checkPermission(userId, submissionItem, Action.read, UserRoleEnum.STUDENT); + } +} diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 7c3194916ac..bf14283221e 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -1,29 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { - AnyBoardDo, - BoardExternalReference, - Card, - Column, - ColumnBoard, - ContentElementType, - EntityId, -} from '@shared/domain'; +import { BoardExternalReference, Card, Column, ColumnBoard, ContentElementType, EntityId } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationService } from '@src/modules/authorization/authorization.service'; import { Action } from '@src/modules/authorization/types/action.enum'; import { CardService, ColumnBoardService, ColumnService } from '../service'; import { BoardDoAuthorizableService } from '../service/board-do-authorizable.service'; +import { BaseUc } from './base.uc'; @Injectable() -export class BoardUc { +export class BoardUc extends BaseUc { constructor( - private readonly authorizationService: AuthorizationService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, + protected readonly authorizationService: AuthorizationService, + protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, private readonly cardService: CardService, private readonly columnBoardService: ColumnBoardService, private readonly columnService: ColumnService, private readonly logger: LegacyLogger ) { + super(authorizationService, boardDoAuthorizableService); this.logger.setContext(BoardUc.name); } @@ -157,12 +151,4 @@ export class BoardUc { await this.cardService.delete(card); } - - private async checkPermission(userId: EntityId, boardDo: AnyBoardDo, action: Action): Promise { - const user = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo); - const context = { action, requiredPermissions: [] }; - - return this.authorizationService.checkPermission(user, boardDoAuthorizable, context); - } } diff --git a/apps/server/src/modules/board/uc/card.uc.ts b/apps/server/src/modules/board/uc/card.uc.ts index 577f3a8b963..b159840ccd6 100644 --- a/apps/server/src/modules/board/uc/card.uc.ts +++ b/apps/server/src/modules/board/uc/card.uc.ts @@ -4,16 +4,18 @@ import { LegacyLogger } from '@src/core/logger'; import { AuthorizationService } from '@src/modules/authorization/authorization.service'; import { Action } from '@src/modules/authorization/types/action.enum'; import { BoardDoAuthorizableService, CardService, ContentElementService } from '../service'; +import { BaseUc } from './base.uc'; @Injectable() -export class CardUc { +export class CardUc extends BaseUc { constructor( - private readonly authorizationService: AuthorizationService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, + protected readonly authorizationService: AuthorizationService, + protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, private readonly cardService: CardService, private readonly elementService: ContentElementService, private readonly logger: LegacyLogger ) { + super(authorizationService, boardDoAuthorizableService); this.logger.setContext(CardUc.name); } @@ -73,14 +75,6 @@ export class CardUc { await this.elementService.move(element, targetCard, targetPosition); } - private async checkPermission(userId: EntityId, boardDo: AnyBoardDo, action: Action): Promise { - const user = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo); - const context = { action, requiredPermissions: [] }; - - return this.authorizationService.checkPermission(user, boardDoAuthorizable, context); - } - private async filterAllowed(userId: EntityId, boardDos: T[], action: Action): Promise { const user = await this.authorizationService.getUserWithPermissions(userId); diff --git a/apps/server/src/modules/board/uc/element.uc.ts b/apps/server/src/modules/board/uc/element.uc.ts index b7cdce5abcc..372f3d8f1a2 100644 --- a/apps/server/src/modules/board/uc/element.uc.ts +++ b/apps/server/src/modules/board/uc/element.uc.ts @@ -1,6 +1,5 @@ import { ForbiddenException, forwardRef, Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; import { - AnyBoardDo, AnyContentElementDo, EntityId, isSubmissionContainerElement, @@ -13,17 +12,19 @@ import { Action, AuthorizationService } from '@src/modules/authorization'; import { AnyElementContentBody } from '../controller/dto'; import { BoardDoAuthorizableService, ContentElementService } from '../service'; import { SubmissionItemService } from '../service/submission-item.service'; +import { BaseUc } from './base.uc'; @Injectable() -export class ElementUc { +export class ElementUc extends BaseUc { constructor( @Inject(forwardRef(() => AuthorizationService)) - private readonly authorizationService: AuthorizationService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, + protected readonly authorizationService: AuthorizationService, + protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, private readonly elementService: ContentElementService, private readonly submissionItemService: SubmissionItemService, private readonly logger: Logger ) { + super(authorizationService, boardDoAuthorizableService); this.logger.setContext(ElementUc.name); } @@ -36,8 +37,9 @@ export class ElementUc { const parent = await this.elementService.findParentOfId(elementId); + // TODO refactor if (isSubmissionItem(parent)) { - await this.checkSubmissionPermission(userId, element, parent); + await this.checkSubmissionItemEditPermission(userId, parent); } else { await this.checkPermission(userId, element, Action.write); } @@ -78,25 +80,4 @@ export class ElementUc { return submissionItem; } - - private async checkPermission( - userId: EntityId, - boardDo: AnyBoardDo, - action: Action, - requiredUserRole?: UserRoleEnum - ): Promise { - const user = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo); - if (requiredUserRole) boardDoAuthorizable.requiredUserRole = requiredUserRole; - const context = { action, requiredPermissions: [] }; - - return this.authorizationService.checkPermission(user, boardDoAuthorizable, context); - } - - private async checkSubmissionPermission(userId: EntityId, element: AnyBoardDo, parent: SubmissionItem) { - if (parent.userId !== userId) { - throw new ForbiddenException(); - } - await this.checkPermission(userId, parent, Action.read, UserRoleEnum.STUDENT); - } } diff --git a/apps/server/src/modules/board/uc/submission-item.uc.ts b/apps/server/src/modules/board/uc/submission-item.uc.ts index d384da71465..d2bc5954c1a 100644 --- a/apps/server/src/modules/board/uc/submission-item.uc.ts +++ b/apps/server/src/modules/board/uc/submission-item.uc.ts @@ -1,18 +1,10 @@ +import { BadRequestException, forwardRef, Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; import { - BadRequestException, - ForbiddenException, - forwardRef, - HttpException, - HttpStatus, - Inject, - Injectable, - UnprocessableEntityException, -} from '@nestjs/common'; -import { - AnyBoardDo, ContentElementType, EntityId, FileElement, + isFileElement, + isRichTextElement, isSubmissionContainerElement, isSubmissionItem, RichTextElement, @@ -20,22 +12,21 @@ import { UserBoardRoles, UserRoleEnum, } from '@shared/domain'; -import { Logger } from '@src/core/logger'; import { AuthorizationService } from '@src/modules/authorization'; import { Action } from '@src/modules/authorization/types/action.enum'; import { BoardDoAuthorizableService, ContentElementService, SubmissionItemService } from '../service'; +import { BaseUc } from './base.uc'; @Injectable() -export class SubmissionItemUc { +export class SubmissionItemUc extends BaseUc { constructor( @Inject(forwardRef(() => AuthorizationService)) - private readonly authorizationService: AuthorizationService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, - private readonly elementService: ContentElementService, - private readonly submissionItemService: SubmissionItemService, - private readonly logger: Logger + protected readonly authorizationService: AuthorizationService, + protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, + protected readonly elementService: ContentElementService, + protected readonly submissionItemService: SubmissionItemService ) { - this.logger.setContext(SubmissionItemUc.name); + super(authorizationService, boardDoAuthorizableService); } async findSubmissionItems( @@ -70,7 +61,7 @@ export class SubmissionItemUc { completed: boolean ): Promise { const submissionItem = await this.submissionItemService.findById(submissionItemId); - await this.checkSubmissionItemPermission(userId, submissionItem); + await this.checkSubmissionItemEditPermission(userId, submissionItem); await this.submissionItemService.update(submissionItem, completed); return submissionItem; @@ -87,50 +78,14 @@ export class SubmissionItemUc { const submissionItem = await this.submissionItemService.findById(submissionItemId); - // TODO - // await this.checkPermission(userId, submissionItem, Action.write); + await this.checkSubmissionItemEditPermission(userId, submissionItem); const element = await this.elementService.create(submissionItem, type); - // TODO - return element as FileElement | RichTextElement; - } - - private async isAuthorizedStudent(userId: EntityId, boardDo: AnyBoardDo): Promise { - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo); - const userRoleEnum = boardDoAuthorizable.users.find((u) => u.userId === userId)?.userRoleEnum; - - if (!userRoleEnum) { - throw new ForbiddenException('User not part of this board'); - } - - // TODO do this with permission instead of role and using authorizable rules - if (userRoleEnum === UserRoleEnum.STUDENT) { - return true; - } - return false; - } - - private async checkSubmissionItemPermission(userId: EntityId, submissionItem: SubmissionItem) { - await this.checkPermission(userId, submissionItem, Action.read, UserRoleEnum.STUDENT); - if (submissionItem.userId !== userId) { - throw new ForbiddenException(); - } - } - - private async checkPermission( - userId: EntityId, - boardDo: AnyBoardDo, - action: Action, - requiredUserRole?: UserRoleEnum - ): Promise { - const user = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo); - if (requiredUserRole) { - boardDoAuthorizable.requiredUserRole = requiredUserRole; + if (!isFileElement(element) && !isRichTextElement(element)) { + throw new UnprocessableEntityException(); } - const context = { action, requiredPermissions: [] }; - return this.authorizationService.checkPermission(user, boardDoAuthorizable, context); + return element; } } diff --git a/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts b/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts index ce5f9dcbdcb..4a96e562bb4 100644 --- a/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts @@ -1,24 +1,8 @@ -import { - EntityId, - FileElement, - isFileElement, - isRichTextElement, - isSumbmissionItem, - RichTextElement, -} from '@shared/domain'; +import { EntityId, FileElement, isFileElement, isRichTextElement, RichTextElement } from '@shared/domain'; import { BoardComposite, BoardCompositeProps } from './board-composite.do'; import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; export class SubmissionItem extends BoardComposite { - /* get children(): (RichTextElement | FileElement)[] { - const { children } = this.props; - if (!children) return []; - - const filteredChildren = children.filter(isFileElement || isRichTextElement); - - return filteredChildren; - } */ - get completed(): boolean { return this.props.completed; } @@ -35,10 +19,8 @@ export class SubmissionItem extends BoardComposite { this.props.userId = value; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars isAllowedAsChild(child: AnyBoardDo): boolean { - // Currently submission-item rejects any children, will open in the future - const allowed = child instanceof FileElement || child instanceof RichTextElement; + const allowed = isFileElement(child) || isRichTextElement(child); return allowed; } @@ -60,3 +42,6 @@ export interface SubmissionItemProps extends BoardCompositeProps { export function isSubmissionItem(reference: unknown): reference is SubmissionItem { return reference instanceof SubmissionItem; } + +export const isSubmissionItemContent = (element: AnyBoardDo): element is RichTextElement | FileElement => + isRichTextElement(element) || isFileElement(element); diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts index c6634e9aa0f..c3504070f36 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts +++ b/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts @@ -5,5 +5,3 @@ import { SubmissionItem } from '../submission-item.do'; import { AnyContentElementDo } from './any-content-element-do'; export type AnyBoardDo = ColumnBoard | Column | Card | AnyContentElementDo | SubmissionItem; - -export const isSumbmissionItem = (element: AnyBoardDo): element is SubmissionItem => element instanceof SubmissionItem; diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts index f47b46b40f6..614071e658c 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts +++ b/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts @@ -22,6 +22,3 @@ export const isAnyContentElement = (element: AnyBoardDo): element is AnyContentE return result; }; - -export const isContent = (element: AnyBoardDo): element is RichTextElement | FileElement => - element instanceof RichTextElement || element instanceof FileElement; diff --git a/apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts b/apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts index 1b6a3991475..68110bb0fa7 100644 --- a/apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts +++ b/apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts @@ -2,11 +2,19 @@ import { CardNode, ColumnBoardNode, ColumnNode, + ExternalToolElement, FileElementNode, + LinkElementNode, RichTextElementNode, SubmissionContainerElementNode, SubmissionItemNode, } from '@shared/domain'; -export type AnyElementNode = FileElementNode | RichTextElementNode | SubmissionContainerElementNode; +export type AnyElementNode = + | FileElementNode + | RichTextElementNode + | SubmissionContainerElementNode + | ExternalToolElement + | LinkElementNode; + export type AnyBoardNode = ColumnBoardNode | ColumnNode | CardNode | AnyElementNode | SubmissionItemNode;