-
- {notification.actor?.name}
- {" "}
- {getNotificationMessage()}
+ }}
+ />
{notification.page && (
diff --git a/apps/client/src/features/page/components/page-import-modal.tsx b/apps/client/src/features/page/components/page-import-modal.tsx
index 2e4b9368..df6691d5 100644
--- a/apps/client/src/features/page/components/page-import-modal.tsx
+++ b/apps/client/src/features/page/components/page-import-modal.tsx
@@ -28,9 +28,11 @@ import { IPage } from "@/features/page/types/page.types.ts";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ConfluenceIcon } from "@/components/icons/confluence-icon.tsx";
-import { getFileImportSizeLimit, isCloud } from "@/lib/config.ts";
+import { getFileImportSizeLimit } from "@/lib/config.ts";
import { formatBytes } from "@/lib";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
+import { useHasFeature } from "@/ee/hooks/use-feature";
+import { Feature } from "@/ee/features";
+import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
import { getFileTaskById } from "@/features/file-task/services/file-task-service.ts";
import { queryClient } from "@/main.tsx";
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
@@ -82,7 +84,6 @@ interface ImportFormatSelection {
function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
const { t } = useTranslation();
const [treeData, setTreeData] = useAtom(treeDataAtom);
- const [workspace] = useAtom(workspaceAtom);
const [fileTaskId, setFileTaskId] = useState
(null);
const emit = useQueryEmit();
@@ -93,8 +94,9 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
const confluenceFileRef = useRef<() => void>(null);
const zipFileRef = useRef<() => void>(null);
- const canUseConfluence = isCloud() || workspace?.hasLicenseKey;
- const canUseDocx = isCloud() || workspace?.hasLicenseKey;
+ const canUseConfluence = useHasFeature(Feature.CONFLUENCE_IMPORT);
+ const canUseDocx = useHasFeature(Feature.DOCX_IMPORT);
+ const upgradeLabel = useUpgradeLabel();
const handleZipUpload = async (selectedFile: File, source: string) => {
if (!selectedFile) {
@@ -360,7 +362,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
>
{(props) => (
);
}
diff --git a/apps/server/src/common/helpers/utils.ts b/apps/server/src/common/helpers/utils.ts
index e1bb2009..36ff5b63 100644
--- a/apps/server/src/common/helpers/utils.ts
+++ b/apps/server/src/common/helpers/utils.ts
@@ -91,15 +91,6 @@ export function extractBearerTokenFromHeader(
return type === 'Bearer' ? token : undefined;
}
-export function hasLicenseOrEE(opts: {
- licenseKey: string;
- plan: string;
- isCloud: boolean;
-}): boolean {
- const { licenseKey, plan, isCloud } = opts;
- return Boolean(licenseKey) || (isCloud && plan === 'business');
-}
-
/**
* Normalizes a database URL for postgres.js compatibility.
* - Removes `sslmode=no-verify` (not supported by postgres.js), keeps other sslmode values
diff --git a/apps/server/src/core/share/share.controller.ts b/apps/server/src/core/share/share.controller.ts
index 0598dbb0..178921d4 100644
--- a/apps/server/src/core/share/share.controller.ts
+++ b/apps/server/src/core/share/share.controller.ts
@@ -28,8 +28,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { Public } from '../../common/decorators/public.decorator';
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
-import { EnvironmentService } from '../../integrations/environment/environment.service';
-import { hasLicenseOrEE } from '../../common/helpers';
+import { LicenseCheckService } from '../../integrations/environment/license-check.service';
import { AuditEvent, AuditResource } from '../../common/events/audit-events';
import {
AUDIT_SERVICE,
@@ -45,7 +44,7 @@ export class ShareController {
private readonly pageRepo: PageRepo,
private readonly pagePermissionRepo: PagePermissionRepo,
private readonly pageAccessService: PageAccessService,
- private readonly environmentService: EnvironmentService,
+ private readonly licenseCheckService: LicenseCheckService,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
) {}
@@ -81,11 +80,10 @@ export class ShareController {
return {
...shareData,
- hasLicenseKey: hasLicenseOrEE({
- licenseKey: workspace.licenseKey,
- isCloud: this.environmentService.isCloud(),
- plan: workspace.plan,
- }),
+ features: this.licenseCheckService.resolveFeatures(
+ workspace.licenseKey,
+ workspace.plan,
+ ),
};
}
@@ -259,11 +257,10 @@ export class ShareController {
return {
...treeData,
- hasLicenseKey: hasLicenseOrEE({
- licenseKey: workspace.licenseKey,
- isCloud: this.environmentService.isCloud(),
- plan: workspace.plan,
- }),
+ features: this.licenseCheckService.resolveFeatures(
+ workspace.licenseKey,
+ workspace.plan,
+ ),
};
}
}
diff --git a/apps/server/src/core/space/services/space.service.ts b/apps/server/src/core/space/services/space.service.ts
index 12f61299..e512e644 100644
--- a/apps/server/src/core/space/services/space.service.ts
+++ b/apps/server/src/core/space/services/space.service.ts
@@ -139,10 +139,10 @@ export class SpaceService {
});
if (
- !this.licenseCheckService.isValidEELicense(workspace.licenseKey)
+ !this.licenseCheckService.hasFeature(workspace.licenseKey, 'security:settings', workspace.plan)
) {
throw new ForbiddenException(
- 'This feature requires a valid enterprise license',
+ 'This feature requires a valid license',
);
}
}
diff --git a/apps/server/src/core/user/user.controller.ts b/apps/server/src/core/user/user.controller.ts
index cf632648..8d51ce6b 100644
--- a/apps/server/src/core/user/user.controller.ts
+++ b/apps/server/src/core/user/user.controller.ts
@@ -37,7 +37,6 @@ export class UserController {
const workspaceInfo = {
...rest,
memberCount,
- hasLicenseKey: Boolean(licenseKey),
};
return { user: authUser, workspace: workspaceInfo };
diff --git a/apps/server/src/core/workspace/controllers/workspace.controller.ts b/apps/server/src/core/workspace/controllers/workspace.controller.ts
index b54bbea3..f1249998 100644
--- a/apps/server/src/core/workspace/controllers/workspace.controller.ts
+++ b/apps/server/src/core/workspace/controllers/workspace.controller.ts
@@ -32,8 +32,10 @@ import {
} from '../../casl/interfaces/workspace-ability.type';
import { FastifyReply } from 'fastify';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
+import { LicenseCheckService } from '../../../integrations/environment/license-check.service';
import { CheckHostnameDto } from '../dto/check-hostname.dto';
import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
+import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
@UseGuards(JwtAuthGuard)
@Controller('workspace')
@@ -42,7 +44,9 @@ export class WorkspaceController {
private readonly workspaceService: WorkspaceService,
private readonly workspaceInvitationService: WorkspaceInvitationService,
private readonly workspaceAbility: WorkspaceAbilityFactory,
+ private readonly workspaceRepo: WorkspaceRepo,
private environmentService: EnvironmentService,
+ private licenseCheckService: LicenseCheckService,
) {}
@Public()
@@ -58,6 +62,23 @@ export class WorkspaceController {
return this.workspaceService.getWorkspaceInfo(workspace.id);
}
+ @HttpCode(HttpStatus.OK)
+ @Post('entitlements')
+ async getEntitlements(@AuthWorkspace() workspace: Workspace) {
+ let { licenseKey } = workspace;
+ const { plan } = workspace;
+
+ if (!licenseKey) {
+ licenseKey = await this.workspaceRepo.findLicenseKeyById(workspace.id);
+ }
+
+ return {
+ cloud: this.environmentService.isCloud(),
+ tier: this.licenseCheckService.resolveTier(licenseKey, plan),
+ features: this.licenseCheckService.resolveFeatures(licenseKey, plan),
+ };
+ }
+
@HttpCode(HttpStatus.OK)
@Post('update')
async updateWorkspace(
diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts
index 495057a0..a52506cd 100644
--- a/apps/server/src/core/workspace/services/workspace.service.ts
+++ b/apps/server/src/core/workspace/services/workspace.service.ts
@@ -85,7 +85,7 @@ export class WorkspaceService {
async getWorkspacePublicData(workspaceId: string) {
const workspace = await this.db
.selectFrom('workspaces')
- .select(['id', 'name', 'logo', 'hostname', 'enforceSso', 'licenseKey'])
+ .select(['id', 'name', 'logo', 'hostname', 'enforceSso', 'licenseKey', 'plan'])
.select((eb) =>
jsonArrayFrom(
eb
@@ -106,12 +106,9 @@ export class WorkspaceService {
throw new NotFoundException('Workspace not found');
}
- const { licenseKey, ...rest } = workspace;
+ const { licenseKey, plan, ...rest } = workspace;
- return {
- ...rest,
- hasLicenseKey: Boolean(licenseKey),
- };
+ return rest;
}
async create(
@@ -332,14 +329,32 @@ export class WorkspaceService {
) {
const ws = await this.db
.selectFrom('workspaces')
- .select(['id', 'licenseKey', 'trashRetentionDays'])
+ .select(['id', 'licenseKey', 'plan', 'trashRetentionDays'])
.where('id', '=', workspaceId)
.executeTakeFirst();
- if (!this.licenseCheckService.isValidEELicense(ws.licenseKey)) {
- throw new ForbiddenException(
- 'This feature requires a valid enterprise license',
- );
+ if (!ws) {
+ throw new NotFoundException('Workspace not found');
+ }
+
+ if (typeof updateWorkspaceDto.mcpEnabled !== 'undefined') {
+ if (!this.licenseCheckService.hasFeature(ws.licenseKey, 'mcp', ws.plan)) {
+ throw new ForbiddenException(
+ 'This feature requires a valid license',
+ );
+ }
+ }
+
+ if (
+ typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
+ typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
+ typeof updateWorkspaceDto.restrictApiToAdmins !== 'undefined'
+ ) {
+ if (!this.licenseCheckService.hasFeature(ws.licenseKey, 'security:settings', ws.plan)) {
+ throw new ForbiddenException(
+ 'This feature requires a valid license',
+ );
+ }
}
if (
@@ -503,10 +518,7 @@ export class WorkspaceService {
}
const { licenseKey, ...rest } = workspace;
- return {
- ...rest,
- hasLicenseKey: Boolean(licenseKey),
- };
+ return rest;
}
async getWorkspaceUsers(
diff --git a/apps/server/src/ee b/apps/server/src/ee
index 47e76280..8b21c6e3 160000
--- a/apps/server/src/ee
+++ b/apps/server/src/ee
@@ -1 +1 @@
-Subproject commit 47e76280fd55b2ff8f6fc755ec1b52876ca4101e
+Subproject commit 8b21c6e32ee04bdfd121e976b88230ff8ec0b74f
diff --git a/apps/server/src/integrations/environment/license-check.service.ts b/apps/server/src/integrations/environment/license-check.service.ts
index a6051c79..35c2295a 100644
--- a/apps/server/src/integrations/environment/license-check.service.ts
+++ b/apps/server/src/integrations/environment/license-check.service.ts
@@ -25,4 +25,75 @@ export class LicenseCheckService {
return false;
}
}
+
+ hasFeature(licenseKey: string, feature: string, plan?: string): boolean {
+ if (this.environmentService.isCloud()) {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { getFeaturesForCloudPlan } = require('../../ee/licence/feature-registry');
+ return getFeaturesForCloudPlan(plan).has(feature);
+ } catch {
+ return false;
+ }
+ }
+
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const LicenseModule = require('../../ee/licence/license.service');
+ const licenseService = this.moduleRef.get(LicenseModule.LicenseService, {
+ strict: false,
+ });
+ return licenseService.hasFeature(licenseKey, feature);
+ } catch {
+ return false;
+ }
+ }
+
+ getFeatures(licenseKey: string): string[] {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const LicenseModule = require('../../ee/licence/license.service');
+ const licenseService = this.moduleRef.get(LicenseModule.LicenseService, {
+ strict: false,
+ });
+ return licenseService.getFeatures(licenseKey);
+ } catch {
+ return [];
+ }
+ }
+
+ resolveFeatures(licenseKey: string, plan: string): string[] {
+ if (this.environmentService.isCloud()) {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { getFeaturesForCloudPlan } = require('../../ee/licence/feature-registry');
+ return [...getFeaturesForCloudPlan(plan)];
+ } catch {
+ return [];
+ }
+ }
+
+ return this.getFeatures(licenseKey);
+ }
+
+ resolveTier(licenseKey: string, plan: string): string {
+ if (this.environmentService.isCloud()) {
+ return plan ?? 'standard';
+ }
+
+ return this.getLicenseType(licenseKey) ?? 'free';
+ }
+
+ private getLicenseType(licenseKey: string): string | null {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const LicenseModule = require('../../ee/licence/license.service');
+ const licenseService = this.moduleRef.get(LicenseModule.LicenseService, {
+ strict: false,
+ });
+ return licenseService.getLicenseType(licenseKey);
+ } catch {
+ return null;
+ }
+ }
}