mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 18:14:58 +08:00
feat(ee): page-level access/permissions (#1971)
* Add page_hierarchy table * feat(ee): page-level permissions * pagination * rename migration fixes * fix * tabs * fix theme * cleanup * sync * page permissions notification * other fixes * sharing disbled * fix column nodes * toggle error handling
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PageAccessService } from './page-access.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PageAccessService],
|
||||
exports: [PageAccessService],
|
||||
})
|
||||
export class PageAccessModule {}
|
||||
@@ -0,0 +1,102 @@
|
||||
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<void> {
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user