This commit is contained in:
Philipinho
2025-04-12 17:59:00 +01:00
parent 16a253ec40
commit 8dff3e2240
16 changed files with 293 additions and 72 deletions

View File

@ -1,9 +1,10 @@
import { IsBoolean, IsString } from 'class-validator';
import { IsBoolean, IsOptional, IsString } from 'class-validator';
export class CreateShareDto {
@IsString()
pageId: string;
@IsBoolean()
@IsOptional()
includeSubPages: boolean;
}

View File

@ -17,12 +17,18 @@ export class SpaceIdDto {
spaceId: string;
}
export class ShareInfoDto extends ShareIdDto {
export class ShareInfoDto {
@IsString()
@IsOptional()
shareId: string;
@IsString()
@IsOptional()
pageId: string;
// @IsOptional()
// @IsBoolean()
// includeContent: boolean;
}
export class SharePageIdDto {
@IsString()
@IsNotEmpty()
pageId: string;
}

View File

@ -1,4 +1,5 @@
import {
BadRequestException,
Body,
Controller,
ForbiddenException,
@ -19,7 +20,7 @@ import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
import { ShareService } from './share.service';
import { UpdateShareDto } from './dto/update-page.dto';
import { CreateShareDto } from './dto/create-share.dto';
import { ShareIdDto, ShareInfoDto } from './dto/share.dto';
import { ShareIdDto, ShareInfoDto, SharePageIdDto } from './dto/share.dto';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { Public } from '../../common/decorators/public.decorator';
@ -52,7 +53,32 @@ export class ShareController {
@Body() dto: ShareInfoDto,
@AuthWorkspace() workspace: Workspace,
) {
return this.shareService.getShare(dto, workspace.id);
if (!dto.pageId && !dto.shareId) {
throw new BadRequestException();
}
return this.shareService.getSharedPage(dto, workspace.id);
}
@HttpCode(HttpStatus.OK)
@Post('/status')
async getShareStatus(
@Body() dto: SharePageIdDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page || workspace.id !== page.workspaceId) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Create, SpaceCaslSubject.Share)) {
throw new ForbiddenException();
}
return this.shareService.getShareStatus(page.id, workspace.id);
}
@HttpCode(HttpStatus.OK)
@ -74,10 +100,10 @@ export class ShareController {
}
return this.shareService.createShare({
pageId: page.id,
page,
authUserId: user.id,
workspaceId: workspace.id,
spaceId: page.spaceId,
includeSubPages: createShareDto.includeSubPages,
});
}

View File

@ -1,6 +1,7 @@
import {
BadRequestException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { ShareInfoDto } from './dto/share.dto';
@ -20,9 +21,12 @@ import { ShareRepo } from '@docmost/db/repos/share/share.repo';
import { updateAttachmentAttr } from './share.util';
import { Page } from '@docmost/db/types/entity.types';
import { validate as isValidUUID } from 'uuid';
import { sql } from 'kysely';
@Injectable()
export class ShareService {
private readonly logger = new Logger(ShareService.name);
constructor(
private readonly shareRepo: ShareRepo,
private readonly pageRepo: PageRepo,
@ -33,49 +37,39 @@ export class ShareService {
async createShare(opts: {
authUserId: string;
workspaceId: string;
pageId: string;
spaceId: string;
page: Page;
includeSubPages: boolean;
}) {
const { authUserId, workspaceId, pageId, spaceId } = opts;
let share = null;
const { authUserId, workspaceId, page, includeSubPages } = opts;
try {
const slugId = generateSlugId();
share = await this.shareRepo.insertShare({
slugId,
pageId,
workspaceId,
const shares = await this.shareRepo.findByPageId(page.id);
if (shares) {
return shares;
}
return await this.shareRepo.insertShare({
key: generateSlugId(),
pageId: page.id,
includeSubPages: includeSubPages,
creatorId: authUserId,
spaceId: spaceId,
spaceId: page.spaceId,
workspaceId,
});
} catch (err) {
throw new BadRequestException('Failed to share page');
this.logger.error(err);
throw new BadRequestException('Failed to create page');
}
return share;
}
async getShare(dto: ShareInfoDto, workspaceId: string) {
const share = await this.shareRepo.findById(dto.shareId);
async getSharedPage(dto: ShareInfoDto, workspaceId: string) {
const share = await this.getShareStatus(dto.pageId, workspaceId);
if (!share || share.workspaceId !== workspaceId) {
throw new NotFoundException('Share not found');
if (!share) {
throw new NotFoundException('Shared page not found');
}
let targetPageId = share.pageId;
if (dto.pageId && dto.pageId !== share.pageId) {
// Check if dto.pageId is a descendant of the shared page.
const isDescendant = await this.getShareAncestorPage(
share.pageId,
dto.pageId,
);
if (isDescendant) {
targetPageId = dto.pageId;
} else {
throw new NotFoundException(`Shared page not found`);
}
}
const page = await this.pageRepo.findById(targetPageId, {
const page = await this.pageRepo.findById(dto.pageId, {
includeContent: true,
includeCreator: true,
});
@ -89,6 +83,56 @@ export class ShareService {
return page;
}
async getShareStatus(pageId: string, workspaceId: string) {
// here we try to check if a page was shared directly or if it inherits the share from its closest shared ancestor
const share = await this.db
.withRecursive('page_hierarchy', (cte) =>
cte
.selectFrom('pages')
.select(['id', 'parentPageId', sql`0`.as('level')])
.where(isValidUUID(pageId) ? 'id' : 'slugId', '=', pageId)
.unionAll((union) =>
union
.selectFrom('pages as p')
.select([
'p.id',
'p.parentPageId',
// Increase the level by 1 for each ancestor.
sql`ph.level + 1`.as('level'),
])
.innerJoin('page_hierarchy as ph', 'ph.parentPageId', 'p.id'),
),
)
.selectFrom('page_hierarchy')
.leftJoin('shares', 'shares.pageId', 'page_hierarchy.id')
.select([
'page_hierarchy.id as sharedPageId',
'page_hierarchy.level as level',
'shares.id as shareId',
'shares.key as shareKey',
'shares.includeSubPages as includeSubPages',
'shares.creatorId',
'shares.spaceId',
'shares.workspaceId',
'shares.createdAt',
'shares.updatedAt',
])
.where('shares.id', 'is not', null)
.orderBy('page_hierarchy.level', 'asc')
.executeTakeFirst();
if (!share || share.workspaceId != workspaceId) {
throw new NotFoundException('Shared page not found');
}
if (share.level === 1 && !share.includeSubPages) {
// we can only show a page if its shared ancestor permits it
throw new NotFoundException('Shared page not found');
}
return share;
}
async getShareAncestorPage(
ancestorPageId: string,
childPageId: string,