import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, ForbiddenException, NotFoundException, BadRequestException, } from '@nestjs/common'; import { PageService } from './services/page.service'; import { CreatePageDto } from './dto/create-page.dto'; import { UpdatePageDto } from './dto/update-page.dto'; import { MovePageDto, MovePageToSpaceDto } from './dto/move-page.dto'; import { PageHistoryIdDto, PageIdDto, PageInfoDto } from './dto/page.dto'; import { PageHistoryService } from './services/page-history.service'; import { AuthUser } from '../../common/decorators/auth-user.decorator'; import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { User, Workspace } from '@docmost/db/types/entity.types'; import { SidebarPageDto } from './dto/sidebar-page.dto'; import { SpaceCaslAction, SpaceCaslSubject, } from '../casl/interfaces/space-ability.type'; import SpaceAbilityFactory from '../casl/abilities/space-ability.factory'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { RecentPageDto } from './dto/recent-page.dto'; import { CopyPageToSpaceDto } from './dto/copy-page.dto'; @UseGuards(JwtAuthGuard) @Controller('pages') export class PageController { constructor( private readonly pageService: PageService, private readonly pageRepo: PageRepo, private readonly pageHistoryService: PageHistoryService, private readonly spaceAbility: SpaceAbilityFactory, ) {} @HttpCode(HttpStatus.OK) @Post('/info') async getPage(@Body() dto: PageInfoDto, @AuthUser() user: User) { const page = await this.pageRepo.findById(dto.pageId, { includeSpace: true, includeContent: true, includeCreator: true, includeLastUpdatedBy: true, includeContributors: true, }); if (!page) { throw new NotFoundException('Page not found'); } const ability = await this.spaceAbility.createForUser(user, page.spaceId); if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } return page; } @HttpCode(HttpStatus.OK) @Post('create') async create( @Body() createPageDto: CreatePageDto, @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { const ability = await this.spaceAbility.createForUser( user, createPageDto.spaceId, ); if (ability.cannot(SpaceCaslAction.Create, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } return this.pageService.create(user.id, workspace.id, createPageDto); } @HttpCode(HttpStatus.OK) @Post('update') async update(@Body() updatePageDto: UpdatePageDto, @AuthUser() user: User) { const page = await this.pageRepo.findById(updatePageDto.pageId); if (!page) { throw new NotFoundException('Page not found'); } const ability = await this.spaceAbility.createForUser(user, page.spaceId); if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } return this.pageService.update(page, updatePageDto, user.id); } @HttpCode(HttpStatus.OK) @Post('delete') async delete(@Body() pageIdDto: PageIdDto, @AuthUser() user: User) { const page = await this.pageRepo.findById(pageIdDto.pageId); if (!page) { throw new NotFoundException('Page not found'); } const ability = await this.spaceAbility.createForUser(user, page.spaceId); if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } await this.pageService.forceDelete(pageIdDto.pageId); } @HttpCode(HttpStatus.OK) @Post('restore') async restore(@Body() pageIdDto: PageIdDto) { // await this.pageService.restore(deletePageDto.id); } @HttpCode(HttpStatus.OK) @Post('recent') async getRecentPages( @Body() recentPageDto: RecentPageDto, @Body() pagination: PaginationOptions, @AuthUser() user: User, ) { if (recentPageDto.spaceId) { const ability = await this.spaceAbility.createForUser( user, recentPageDto.spaceId, ); if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } return this.pageService.getRecentSpacePages( recentPageDto.spaceId, pagination, ); } return this.pageService.getRecentPages(user.id, pagination); } // TODO: scope to workspaces @HttpCode(HttpStatus.OK) @Post('/history') async getPageHistory( @Body() dto: PageIdDto, @Body() pagination: PaginationOptions, @AuthUser() user: User, ) { const page = await this.pageRepo.findById(dto.pageId); const ability = await this.spaceAbility.createForUser(user, page.spaceId); if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } return this.pageHistoryService.findHistoryByPageId(page.id, pagination); } @HttpCode(HttpStatus.OK) @Post('/history/info') async getPageHistoryInfo( @Body() dto: PageHistoryIdDto, @AuthUser() user: User, ) { const history = await this.pageHistoryService.findById(dto.historyId); if (!history) { throw new NotFoundException('Page history not found'); } const ability = await this.spaceAbility.createForUser( user, history.spaceId, ); if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } return history; } @HttpCode(HttpStatus.OK) @Post('/sidebar-pages') async getSidebarPages( @Body() dto: SidebarPageDto, @Body() pagination: PaginationOptions, @AuthUser() user: User, ) { const ability = await this.spaceAbility.createForUser(user, dto.spaceId); if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } let pageId = null; if (dto.pageId) { const page = await this.pageRepo.findById(dto.pageId); if (page.spaceId !== dto.spaceId) { throw new ForbiddenException(); } pageId = page.id; } return this.pageService.getSidebarPages(dto.spaceId, pagination, pageId); } @HttpCode(HttpStatus.OK) @Post('move-to-space') async movePageToSpace( @Body() dto: MovePageToSpaceDto, @AuthUser() user: User, ) { const movedPage = await this.pageRepo.findById(dto.pageId); if (!movedPage) { throw new NotFoundException('Page to move not found'); } if (movedPage.spaceId === dto.spaceId) { throw new BadRequestException('Page is already in this space'); } const abilities = await Promise.all([ this.spaceAbility.createForUser(user, movedPage.spaceId), this.spaceAbility.createForUser(user, dto.spaceId), ]); if ( abilities.some((ability) => ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page), ) ) { throw new ForbiddenException(); } return this.pageService.movePageToSpace(movedPage, dto.spaceId); } @HttpCode(HttpStatus.OK) @Post('copy-to-space') async copyPageToSpace( @Body() dto: CopyPageToSpaceDto, @AuthUser() user: User, ) { const copiedPage = await this.pageRepo.findById(dto.pageId); if (!copiedPage) { throw new NotFoundException('Page to copy not found'); } if (copiedPage.spaceId === dto.spaceId) { throw new BadRequestException('Page is already in this space'); } const abilities = await Promise.all([ this.spaceAbility.createForUser(user, copiedPage.spaceId), this.spaceAbility.createForUser(user, dto.spaceId), ]); if ( abilities.some((ability) => ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page), ) ) { throw new ForbiddenException(); } return this.pageService.copyPageToSpace(copiedPage, dto.spaceId, user); } @HttpCode(HttpStatus.OK) @Post('move') async movePage(@Body() dto: MovePageDto, @AuthUser() user: User) { const movedPage = await this.pageRepo.findById(dto.pageId); if (!movedPage) { throw new NotFoundException('Moved page not found'); } const ability = await this.spaceAbility.createForUser( user, movedPage.spaceId, ); if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } return this.pageService.movePage(dto, movedPage); } @HttpCode(HttpStatus.OK) @Post('/breadcrumbs') async getPageBreadcrumbs(@Body() dto: PageIdDto, @AuthUser() user: User) { const page = await this.pageRepo.findById(dto.pageId); if (!page) { throw new NotFoundException('Page not found'); } const ability = await this.spaceAbility.createForUser(user, page.spaceId); if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } return this.pageService.getPageBreadCrumbs(page.id); } }