mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat: better feature flags (#2026)
* feat: feature flag upgrade * fix translations * refactor * fix * fix
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ export class UserController {
|
||||
const workspaceInfo = {
|
||||
...rest,
|
||||
memberCount,
|
||||
hasLicenseKey: Boolean(licenseKey),
|
||||
};
|
||||
|
||||
return { user: authUser, workspace: workspaceInfo };
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 47e76280fd...8b21c6e32e
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user