This commit is contained in:
Philipinho
2025-12-24 00:27:25 +00:00
parent f65726ae26
commit c2e722ee5c
9 changed files with 229 additions and 86 deletions
+52 -24
View File
@@ -10,6 +10,7 @@ import {
UseGuards,
} from '@nestjs/common';
import { PageService } from './services/page.service';
import { PagePermissionService } from './services/page-permission.service';
import { CreatePageDto } from './dto/create-page.dto';
import { UpdatePageDto } from './dto/update-page.dto';
import { MovePageDto, MovePageToSpaceDto } from './dto/move-page.dto';
@@ -44,6 +45,7 @@ export class PageController {
private readonly pageRepo: PageRepo,
private readonly pageHistoryService: PageHistoryService,
private readonly spaceAbility: SpaceAbilityFactory,
private readonly pagePermissionService: PagePermissionService,
) {}
@HttpCode(HttpStatus.OK)
@@ -61,10 +63,8 @@ export class PageController {
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();
}
// Checks both space-level and page-level permissions
await this.pagePermissionService.validateCanView(page, user);
return page;
}
@@ -84,6 +84,14 @@ export class PageController {
throw new ForbiddenException();
}
// If creating under a parent page, check page-level edit permission
if (createPageDto.parentPageId) {
const parentPage = await this.pageRepo.findById(createPageDto.parentPageId);
if (parentPage) {
await this.pagePermissionService.validateCanEdit(parentPage, user);
}
}
return this.pageService.create(user.id, workspace.id, createPageDto);
}
@@ -96,10 +104,8 @@ export class PageController {
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();
}
// Checks both space-level and page-level permissions
await this.pagePermissionService.validateCanEdit(page, user);
return this.pageService.update(page, updatePageDto, user.id);
}
@@ -128,10 +134,13 @@ export class PageController {
}
await this.pageService.forceDelete(deletePageDto.pageId, workspace.id);
} else {
// Soft delete requires page manage permissions
// Soft delete requires page manage permissions at space level
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
// Also check page-level edit permission
await this.pagePermissionService.validateCanEdit(page, user);
await this.pageService.removePage(
deletePageDto.pageId,
user.id,
@@ -158,6 +167,9 @@ export class PageController {
throw new ForbiddenException();
}
// Check page-level edit permission (if restoring to a restricted ancestor)
await this.pagePermissionService.validateCanEdit(page, user);
await this.pageRepo.restorePage(pageIdDto.pageId, workspace.id);
return this.pageRepo.findById(pageIdDto.pageId, {
@@ -228,10 +240,8 @@ export class PageController {
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();
}
// Checks both space-level and page-level permissions
await this.pagePermissionService.validateCanView(page, user);
return this.pageHistoryService.findHistoryByPageId(page.id, pagination);
}
@@ -247,13 +257,15 @@ export class PageController {
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();
// Get the page to check permissions
const page = await this.pageRepo.findById(history.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
// Checks both space-level and page-level permissions
await this.pagePermissionService.validateCanView(page, user);
return history;
}
@@ -285,7 +297,7 @@ export class PageController {
throw new ForbiddenException();
}
return this.pageService.getSidebarPages(spaceId, pagination, dto.pageId);
return this.pageService.getSidebarPages(spaceId, pagination, dto.pageId, user.id);
}
@HttpCode(HttpStatus.OK)
@@ -315,6 +327,9 @@ export class PageController {
throw new ForbiddenException();
}
// Check page-level edit permission on the source page
await this.pagePermissionService.validateCanEdit(movedPage, user);
return this.pageService.movePageToSpace(movedPage, dto.spaceId);
}
@@ -326,6 +341,9 @@ export class PageController {
throw new NotFoundException('Page to copy not found');
}
// Check page-level view permission on the source page (need to read to copy)
await this.pagePermissionService.validateCanView(copiedPage, user);
// If spaceId is provided, it's a copy to different space
if (dto.spaceId) {
const abilities = await Promise.all([
@@ -372,6 +390,17 @@ export class PageController {
throw new ForbiddenException();
}
// Check page-level edit permission
await this.pagePermissionService.validateCanEdit(movedPage, user);
// If moving to a new parent, check permission on the target parent
if (dto.parentPageId && dto.parentPageId !== movedPage.parentPageId) {
const targetParent = await this.pageRepo.findById(dto.parentPageId);
if (targetParent) {
await this.pagePermissionService.validateCanEdit(targetParent, user);
}
}
return this.pageService.movePage(dto, movedPage);
}
@@ -383,10 +412,9 @@ export class PageController {
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();
}
// Checks both space-level and page-level permissions
await this.pagePermissionService.validateCanView(page, user);
return this.pageService.getPageBreadCrumbs(page.id);
}
}
@@ -7,6 +7,7 @@ import {
import { CreatePageDto } from '../dto/create-page.dto';
import { UpdatePageDto } from '../dto/update-page.dto';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import {
@@ -47,6 +48,7 @@ export class PageService {
constructor(
private pageRepo: PageRepo,
private pagePermissionRepo: PagePermissionRepo,
private attachmentRepo: AttachmentRepo,
@InjectKysely() private readonly db: KyselyDB,
private readonly storageService: StorageService,
@@ -180,6 +182,7 @@ export class PageService {
spaceId: string,
pagination: PaginationOptions,
pageId?: string,
userId?: string,
): Promise<any> {
let query = this.db
.selectFrom('pages')
@@ -194,7 +197,12 @@ export class PageService {
'creatorId',
'deletedAt',
])
.select((eb) => this.pageRepo.withHasChildren(eb))
.$if(Boolean(userId), (qb) =>
qb.select((eb) => this.pageRepo.withHasChildrenV2(eb, userId)),
)
//.$if(!userId, (qb) =>
// qb.select((eb) => this.pageRepo.withHasChildren(eb)),
// )
.orderBy('position', (ob) => ob.collate('C').asc())
.where('deletedAt', 'is', null)
.where('spaceId', '=', spaceId);
@@ -205,11 +213,22 @@ export class PageService {
query = query.where('parentPageId', 'is', null);
}
const result = executeWithPagination(query, {
const result = await executeWithPagination(query, {
page: pagination.page,
perPage: 250,
});
// Filter by page-level permissions
if (userId && result.items.length > 0) {
const pageIds = result.items.map((p: any) => p.id);
const accessiblePageIds = await this.pagePermissionRepo.filterAccessiblePageIds(
pageIds,
userId,
);
const accessibleSet = new Set(accessiblePageIds);
result.items = result.items.filter((p: any) => accessibleSet.has(p.id));
}
return result;
}