import { ForbiddenException, Injectable } from '@nestjs/common'; import { Page, User } from '@docmost/db/types/entity.types'; import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo'; import SpaceAbilityFactory from '../casl/abilities/space-ability.factory'; import { SpaceCaslAction, SpaceCaslSubject, } from '../casl/interfaces/space-ability.type'; @Injectable() export class PageAccessService { constructor( private readonly pagePermissionRepo: PagePermissionRepo, private readonly spaceAbility: SpaceAbilityFactory, ) {} /** * Validate user can view page, throws ForbiddenException if not. * If page has restrictions: page-level permission determines access. * If no restrictions: space-level permission determines access. */ async validateCanView(page: Page, user: User): Promise { // TODO: cache by pageId and userId. const ability = await this.spaceAbility.createForUser(user, page.spaceId); // User must be at least a space member if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } const canAccess = await this.pagePermissionRepo.canUserAccessPage( user.id, page.id, ); if (!canAccess) { throw new ForbiddenException(); } } /** * Validate user can view page AND return effective canEdit permission. * Combines access check + edit permission in a single query pass. */ async validateCanViewWithPermissions( page: Page, user: User, ): Promise<{ canEdit: boolean; hasRestriction: boolean }> { const ability = await this.spaceAbility.createForUser(user, page.spaceId); if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } const { hasAnyRestriction, canAccess, canEdit } = await this.pagePermissionRepo.canUserEditPage(user.id, page.id); if (hasAnyRestriction && !canAccess) { throw new ForbiddenException(); } return { canEdit: hasAnyRestriction ? canEdit : ability.can(SpaceCaslAction.Edit, SpaceCaslSubject.Page), hasRestriction: hasAnyRestriction, }; } /** * Validate user can edit page, throws ForbiddenException if not. * If page has restrictions: page-level writer permission determines access. * If no restrictions: space-level edit permission determines access. */ async validateCanEdit( page: Page, user: User, ): Promise<{ hasRestriction: boolean }> { const ability = await this.spaceAbility.createForUser(user, page.spaceId); // User must be at least a space member if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } const { hasAnyRestriction, canEdit } = await this.pagePermissionRepo.canUserEditPage(user.id, page.id); if (hasAnyRestriction) { // Page has restrictions - use page-level permission if (!canEdit) { throw new ForbiddenException(); } } else { // No restrictions - use space-level permission if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) { throw new ForbiddenException(); } } return { hasRestriction: hasAnyRestriction }; } }