feat(ee): add enterprise ai chat controls (#2421)

* feat(ee): add enterprise ai chat controls

* feat: add comment listing tool to ai chat

* fix: scope comment endpoints to workspace and skip trashed pages
This commit is contained in:
Philip Okugbe
2026-08-24 20:29:21 +01:00
committed by GitHub
parent cd9c166927
commit b7f0b063dc
24 changed files with 308 additions and 30 deletions
+1
View File
@@ -23,6 +23,7 @@ export const Feature = {
PERSONAL_SPACES: 'spaces:personal',
DOCX_EXPORT: 'export:docx',
BASES: 'bases',
AI_CONTROLS: 'ai:controls',
} as const;
export type FeatureKey = (typeof Feature)[keyof typeof Feature];
@@ -54,7 +54,7 @@ export class CommentController {
@AuthWorkspace() workspace: Workspace,
) {
const page = await this.pageRepo.findById(createCommentDto.pageId);
if (!page || page.deletedAt) {
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
throw new NotFoundException('Page not found');
}
@@ -89,9 +89,10 @@ export class CommentController {
@Body()
pagination: PaginationOptions,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const page = await this.pageRepo.findById(input.pageId);
if (!page) {
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
throw new NotFoundException('Page not found');
}
@@ -102,14 +103,18 @@ export class CommentController {
@HttpCode(HttpStatus.OK)
@Post('info')
async findOne(@Body() input: CommentIdDto, @AuthUser() user: User) {
async findOne(
@Body() input: CommentIdDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const comment = await this.commentRepo.findById(input.commentId);
if (!comment) {
throw new NotFoundException('Comment not found');
}
const page = await this.pageRepo.findById(comment.pageId);
if (!page) {
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
throw new NotFoundException('Page not found');
}
@@ -130,7 +135,7 @@ export class CommentController {
}
const page = await this.pageRepo.findById(comment.pageId);
if (!page) {
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
throw new NotFoundException('Page not found');
}
@@ -148,7 +153,7 @@ export class CommentController {
}
const page = await this.pageRepo.findById(comment.pageId);
if (!page) {
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
throw new NotFoundException('Page not found');
}
@@ -68,4 +68,12 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
@IsString()
@IsIn(['read', 'edit'])
defaultPageEditMode: string;
@IsOptional()
@IsBoolean()
aiChatReadOnly: boolean;
@IsOptional()
@IsBoolean()
aiChatWorkspaceKnowledgeOnly: boolean;
}
@@ -334,7 +334,9 @@ export class WorkspaceService {
typeof updateWorkspaceDto.restrictApiToAdmins !== 'undefined' ||
typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined' ||
typeof updateWorkspaceDto.isScimEnabled !== 'undefined' ||
typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined'
typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined' ||
typeof updateWorkspaceDto.aiChatReadOnly !== 'undefined' ||
typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined'
) {
const ws = await this.db
.selectFrom('workspaces')
@@ -374,6 +376,21 @@ export class WorkspaceService {
}
}
if (
typeof updateWorkspaceDto.aiChatReadOnly !== 'undefined' ||
typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined'
) {
if (
!this.licenseCheckService.hasFeature(
ws.licenseKey,
Feature.AI_CONTROLS,
ws.plan,
)
) {
throw new ForbiddenException('This feature requires a valid license');
}
}
if (
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
@@ -516,6 +533,34 @@ export class WorkspaceService {
);
}
if (typeof updateWorkspaceDto.aiChatReadOnly !== 'undefined') {
const prev = settingsBefore?.ai?.chatReadOnly ?? false;
if (prev !== updateWorkspaceDto.aiChatReadOnly) {
before.aiChatReadOnly = prev;
after.aiChatReadOnly = updateWorkspaceDto.aiChatReadOnly;
}
await this.workspaceRepo.updateAiSettings(
workspaceId,
'chatReadOnly',
updateWorkspaceDto.aiChatReadOnly,
trx,
);
}
if (typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined') {
const prev = settingsBefore?.ai?.chatWorkspaceKnowledgeOnly ?? false;
if (prev !== updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly) {
before.aiChatWorkspaceKnowledgeOnly = prev;
after.aiChatWorkspaceKnowledgeOnly = updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly;
}
await this.workspaceRepo.updateAiSettings(
workspaceId,
'chatWorkspaceKnowledgeOnly',
updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly,
trx,
);
}
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
const prev = settingsBefore?.spaces?.allowPersonal ?? false;
if (prev !== updateWorkspaceDto.allowPersonalSpaces) {
@@ -553,6 +598,8 @@ export class WorkspaceService {
delete updateWorkspaceDto.aiChat;
delete updateWorkspaceDto.allowPersonalSpaces;
delete updateWorkspaceDto.defaultPageEditMode;
delete updateWorkspaceDto.aiChatReadOnly;
delete updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly;
await this.workspaceRepo.updateWorkspace(
updateWorkspaceDto,