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/board-submission.controller.ts b/apps/server/src/modules/board/controller/board-submission.controller.ts index 56c6ae76ce2..c385deabfcf 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,8 +17,16 @@ import { SubmissionsResponse } from '@src/modules/board/controller/dto/submissio import { CardUc } from '../uc'; import { ElementUc } from '../uc/element.uc'; import { SubmissionItemUc } from '../uc/submission-item.uc'; -import { SubmissionContainerUrlParams, SubmissionItemUrlParams, UpdateSubmissionItemBodyParams } from './dto'; -import { SubmissionItemResponseMapper } from './mapper'; +import { + AnyContentElementResponse, + CreateContentElementBodyParams, + FileElementResponse, + RichTextElementResponse, + SubmissionContainerUrlParams, + SubmissionItemUrlParams, + UpdateSubmissionItemBodyParams, +} from './dto'; +import { ContentElementResponseFactory, SubmissionItemResponseMapper } from './mapper'; @ApiTags('Board Submission') @Authenticate('jwt') @@ -57,4 +75,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/element/update-element-content.body.params.ts b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts index 5eb0f239c1f..edafb0fd0eb 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 @@ -52,7 +52,9 @@ export class RichTextContentBody { text!: string; @IsEnum(InputFormat) - @ApiProperty() + @ApiProperty({ + enum: InputFormat, + }) inputFormat!: InputFormat; } 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 5b2fd522476..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,12 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { TimestampsResponse } from '../timestamps.response'; +import { FileElementResponse, RichTextElementResponse } from '../element'; export class SubmissionItemResponse { - constructor({ id, timestamps, completed, userId }: SubmissionItemResponse) { + constructor({ id, timestamps, completed, userId, elements }: SubmissionItemResponse) { this.id = id; this.timestamps = timestamps; this.completed = completed; this.userId = userId; + this.elements = elements; } @ApiProperty({ pattern: '[a-f0-9]{24}' }) @@ -20,4 +22,10 @@ export class SubmissionItemResponse { @ApiProperty({ pattern: '[a-f0-9]{24}' }) userId: string; + + @ApiProperty({ + type: 'array', + // TODO add types + }) + elements: (RichTextElementResponse | FileElementResponse)[]; } 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/submission-item-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts index 82d2292ba11..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,4 +1,15 @@ -import { SubmissionItem, UserBoardRoles } from '@shared/domain'; +import { + FileElement, + isFileElement, + isRichTextElement, + isSubmissionItemContent, + RichTextElement, + SubmissionItem, + UserBoardRoles, +} from '@shared/domain'; +import { UnprocessableEntityException } from '@nestjs/common'; +import { FileElementResponseMapper } from './file-element-response.mapper'; +import { RichTextElementResponseMapper } from './rich-text-element-response.mapper'; import { SubmissionsResponse } from '../dto/submission-item/submissions.response'; import { SubmissionItemResponse, TimestampsResponse, UserDataResponse } from '../dto'; @@ -15,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)); @@ -24,7 +35,8 @@ export class SubmissionItemResponseMapper { return response; } - public mapSubmissionsToResponse(submissionItem: SubmissionItem): SubmissionItemResponse { + public mapSubmissionItemToResponse(submissionItem: SubmissionItem): SubmissionItemResponse { + const children: (FileElement | RichTextElement)[] = submissionItem.children.filter(isSubmissionItemContent); const result = new SubmissionItemResponse({ completed: submissionItem.completed, id: submissionItem.id, @@ -33,6 +45,17 @@ export class SubmissionItemResponseMapper { createdAt: submissionItem.createdAt, }), 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 18b0583daa1..6e2b375991e 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 @@ -155,7 +155,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, @@ -163,7 +167,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 7b2c7901605..699ea6c3958 100644 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.ts +++ b/apps/server/src/modules/board/repo/recursive-save.visitor.ts @@ -62,8 +62,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { context: columnBoard.context, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(columnBoard, boardNode); + this.saveRecursive(boardNode, columnBoard); } visitColumn(column: Column): void { @@ -76,8 +75,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(column, boardNode); + this.saveRecursive(boardNode, column); } visitCard(card: Card): void { @@ -91,8 +89,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(card, boardNode); + this.saveRecursive(boardNode, card); } visitFileElement(fileElement: FileElement): void { @@ -106,8 +103,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(fileElement, boardNode); + this.saveRecursive(boardNode, fileElement); } visitLinkElement(linkElement: LinkElement): void { @@ -137,8 +133,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, }); - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(richTextElement, boardNode); + this.saveRecursive(boardNode, richTextElement); } visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void { @@ -151,22 +146,20 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { dueDate: submissionContainerElement.dueDate, }); - 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); } visitExternalToolElement(externalToolElement: ExternalToolElement): void { @@ -192,7 +185,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`); @@ -200,6 +193,12 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { this.parentsMap.set(child.id, { boardNode: parentNode, position }); } + private saveRecursive(boardNode: BoardNode, anyBoardDo: AnyBoardDo): void { + this.createOrUpdateBoardNode(boardNode); + this.visitChildren(anyBoardDo, boardNode); + } + + // 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 a7c957173f3..4404f51fc3e 100644 --- a/apps/server/src/modules/board/service/content-element.service.ts +++ b/apps/server/src/modules/board/service/content-element.service.ts @@ -1,11 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { + AnyBoardDo, AnyContentElementDo, Card, ContentElementFactory, ContentElementType, EntityId, isAnyContentElement, + SubmissionItem, } from '@shared/domain'; import { AnyElementContentBody } from '../controller/dto'; import { BoardDoRepo } from '../repo'; @@ -32,7 +34,15 @@ export class ContentElementService { return element; } - async create(parent: Card, type: ContentElementType): Promise { + 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 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/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 e5dc039168c..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,6 @@ -import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { ForbiddenException, forwardRef, Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; import { - AnyBoardDo, + AnyContentElementDo, EntityId, isSubmissionContainerElement, isSubmissionItem, @@ -8,29 +8,41 @@ 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'; +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); } - async updateElementContent(userId: EntityId, elementId: EntityId, content: AnyElementContentBody) { + async updateElementContent( + userId: EntityId, + elementId: EntityId, + content: AnyElementContentBody + ): Promise { let element = await this.elementService.findById(elementId); - await this.checkPermission(userId, element, Action.write); + const parent = await this.elementService.findParentOfId(elementId); + + // TODO refactor + if (isSubmissionItem(parent)) { + await this.checkSubmissionItemEditPermission(userId, parent); + } else { + await this.checkPermission(userId, element, Action.write); + } element = await this.elementService.update(element, content); return element; @@ -44,16 +56,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' ); } @@ -61,9 +69,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' ); } @@ -73,18 +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); - } } 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 e59afa4b49b..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,29 +1,32 @@ -import { ForbiddenException, forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, forwardRef, Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; import { - AnyBoardDo, + ContentElementType, EntityId, + FileElement, + isFileElement, + isRichTextElement, isSubmissionContainerElement, isSubmissionItem, + RichTextElement, SubmissionItem, 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( @@ -33,7 +36,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); @@ -58,46 +61,31 @@ 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.checkSubmissionItemEditPermission(userId, submissionItem); await this.submissionItemService.update(submissionItem, completed); return submissionItem; } - 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'); + async createElement( + userId: EntityId, + submissionItemId: EntityId, + type: ContentElementType + ): Promise { + if (type !== ContentElementType.RICH_TEXT && type !== ContentElementType.FILE) { + throw new BadRequestException(); } - // TODO do this with permission instead of role and using authorizable rules - if (userRoleEnum === UserRoleEnum.STUDENT) { - return true; - } + const submissionItem = await this.submissionItemService.findById(submissionItemId); - return false; - } + await this.checkSubmissionItemEditPermission(userId, 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 element = await this.elementService.create(submissionItem, type); + + 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 cb072f37455..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,4 +1,4 @@ -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'; @@ -19,10 +19,10 @@ 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 - return false; + const allowed = isFileElement(child) || isRichTextElement(child); + + return allowed; } accept(visitor: BoardCompositeVisitor): void { @@ -42,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/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..68110bb0fa7 --- /dev/null +++ b/apps/server/src/shared/domain/entity/boardnode/types/any-board-node.ts @@ -0,0 +1,20 @@ +import { + CardNode, + ColumnBoardNode, + ColumnNode, + ExternalToolElement, + FileElementNode, + LinkElementNode, + RichTextElementNode, + SubmissionContainerElementNode, + SubmissionItemNode, +} from '@shared/domain'; + +export type AnyElementNode = + | FileElementNode + | RichTextElementNode + | SubmissionContainerElementNode + | ExternalToolElement + | LinkElementNode; + +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';