mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-31 13:49:12 +08:00
feat(server): support installable license (#12181)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for installing self-hosted team licenses via encrypted license files. - Introduced a new "Onetime" license variant for self-hosted environments. - Added a GraphQL mutation to upload and install license files. - License details now display the license variant. - **Bug Fixes** - Improved error messages for license activation and expiration, including dynamic reasons. - **Localization** - Updated and improved license-related error messages for better clarity. - **Tests** - Added comprehensive end-to-end tests for license installation scenarios. - **Chores** - Enhanced environment variable handling and public key management for license verification. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -811,18 +811,17 @@ export const USER_FRIENDLY_ERRORS = {
|
||||
},
|
||||
invalid_license_to_activate: {
|
||||
type: 'bad_request',
|
||||
message: 'Invalid license to activate.',
|
||||
args: { reason: 'string' },
|
||||
message: ({ reason }) => `Invalid license to activate. ${reason}`,
|
||||
},
|
||||
invalid_license_update_params: {
|
||||
type: 'invalid_input',
|
||||
args: { reason: 'string' },
|
||||
message: ({ reason }) => `Invalid license update params. ${reason}`,
|
||||
},
|
||||
workspace_members_exceed_limit_to_downgrade: {
|
||||
license_expired: {
|
||||
type: 'bad_request',
|
||||
args: { limit: 'number' },
|
||||
message: ({ limit }) =>
|
||||
`You cannot downgrade the workspace from team workspace because there are more than ${limit} members that are currently active.`,
|
||||
message: 'License has expired.',
|
||||
},
|
||||
|
||||
// version errors
|
||||
|
||||
@@ -914,10 +914,14 @@ export class LicenseNotFound extends UserFriendlyError {
|
||||
super('resource_not_found', 'license_not_found', message);
|
||||
}
|
||||
}
|
||||
@ObjectType()
|
||||
class InvalidLicenseToActivateDataType {
|
||||
@Field() reason!: string
|
||||
}
|
||||
|
||||
export class InvalidLicenseToActivate extends UserFriendlyError {
|
||||
constructor(message?: string) {
|
||||
super('bad_request', 'invalid_license_to_activate', message);
|
||||
constructor(args: InvalidLicenseToActivateDataType, message?: string | ((args: InvalidLicenseToActivateDataType) => string)) {
|
||||
super('bad_request', 'invalid_license_to_activate', message, args);
|
||||
}
|
||||
}
|
||||
@ObjectType()
|
||||
@@ -930,14 +934,10 @@ export class InvalidLicenseUpdateParams extends UserFriendlyError {
|
||||
super('invalid_input', 'invalid_license_update_params', message, args);
|
||||
}
|
||||
}
|
||||
@ObjectType()
|
||||
class WorkspaceMembersExceedLimitToDowngradeDataType {
|
||||
@Field() limit!: number
|
||||
}
|
||||
|
||||
export class WorkspaceMembersExceedLimitToDowngrade extends UserFriendlyError {
|
||||
constructor(args: WorkspaceMembersExceedLimitToDowngradeDataType, message?: string | ((args: WorkspaceMembersExceedLimitToDowngradeDataType) => string)) {
|
||||
super('bad_request', 'workspace_members_exceed_limit_to_downgrade', message, args);
|
||||
export class LicenseExpired extends UserFriendlyError {
|
||||
constructor(message?: string) {
|
||||
super('bad_request', 'license_expired', message);
|
||||
}
|
||||
}
|
||||
@ObjectType()
|
||||
@@ -1100,7 +1100,7 @@ export enum ErrorNames {
|
||||
LICENSE_NOT_FOUND,
|
||||
INVALID_LICENSE_TO_ACTIVATE,
|
||||
INVALID_LICENSE_UPDATE_PARAMS,
|
||||
WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE,
|
||||
LICENSE_EXPIRED,
|
||||
UNSUPPORTED_CLIENT_VERSION,
|
||||
NOTIFICATION_NOT_FOUND,
|
||||
MENTION_USER_DOC_ACCESS_DENIED,
|
||||
@@ -1114,5 +1114,5 @@ registerEnumType(ErrorNames, {
|
||||
export const ErrorDataUnionType = createUnionType({
|
||||
name: 'ErrorDataUnion',
|
||||
types: () =>
|
||||
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType] as const,
|
||||
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType] as const,
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ test('should be able to encrypt and decrypt', t => {
|
||||
|
||||
// we are using a stub to make sure the iv is always 0,
|
||||
// the encrypted result will always be the same
|
||||
t.is(encrypted, 'AAAAAAAAAAAAAAAAWUDlJRhzP+SZ3avvmLcgnou+q4E11w==');
|
||||
t.is(encrypted, 'AAAAAAAAAAAAAAAAOXbR/9glITL3BcO3kPd6fGOMasSkPQ==');
|
||||
t.is(decrypted, data);
|
||||
|
||||
stub.restore();
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
createHash,
|
||||
createPrivateKey,
|
||||
createPublicKey,
|
||||
createSign,
|
||||
createVerify,
|
||||
@@ -12,12 +11,13 @@ import {
|
||||
timingSafeEqual,
|
||||
} from 'node:crypto';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import {
|
||||
hash as hashPassword,
|
||||
verify as verifyPassword,
|
||||
} from '@node-rs/argon2';
|
||||
|
||||
import { AFFINE_PRO_PUBLIC_KEY } from '../../native';
|
||||
import { Config } from '../config';
|
||||
import { OnEvent } from '../event';
|
||||
|
||||
@@ -37,20 +37,7 @@ function generatePrivateKey(): string {
|
||||
return key.toString('utf8');
|
||||
}
|
||||
|
||||
function readPrivateKey(privateKey: string) {
|
||||
return createPrivateKey({
|
||||
key: Buffer.from(privateKey),
|
||||
format: 'pem',
|
||||
type: 'sec1',
|
||||
})
|
||||
.export({
|
||||
format: 'pem',
|
||||
type: 'pkcs8',
|
||||
})
|
||||
.toString('utf8');
|
||||
}
|
||||
|
||||
function readPublicKey(privateKey: string) {
|
||||
function generatePublicKey(privateKey: string) {
|
||||
return createPublicKey({
|
||||
key: Buffer.from(privateKey),
|
||||
})
|
||||
@@ -59,7 +46,9 @@ function readPublicKey(privateKey: string) {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CryptoHelper {
|
||||
export class CryptoHelper implements OnModuleInit {
|
||||
logger = new Logger(CryptoHelper.name);
|
||||
|
||||
keyPair!: {
|
||||
publicKey: Buffer;
|
||||
privateKey: Buffer;
|
||||
@@ -69,6 +58,14 @@ export class CryptoHelper {
|
||||
};
|
||||
};
|
||||
|
||||
AFFiNEProPublicKey: Buffer | null = null;
|
||||
|
||||
onModuleInit() {
|
||||
if (env.selfhosted) {
|
||||
this.AFFiNEProPublicKey = this.loadAFFiNEProPublicKey();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private readonly config: Config) {}
|
||||
|
||||
@OnEvent('config.init')
|
||||
@@ -84,9 +81,8 @@ export class CryptoHelper {
|
||||
}
|
||||
|
||||
private setup() {
|
||||
const key = this.config.crypto.privateKey || generatePrivateKey();
|
||||
const privateKey = readPrivateKey(key);
|
||||
const publicKey = readPublicKey(key);
|
||||
const privateKey = this.config.crypto.privateKey || generatePrivateKey();
|
||||
const publicKey = generatePublicKey(privateKey);
|
||||
|
||||
this.keyPair = {
|
||||
publicKey: Buffer.from(publicKey),
|
||||
@@ -187,4 +183,18 @@ export class CryptoHelper {
|
||||
sha256(data: string) {
|
||||
return createHash('sha256').update(data).digest();
|
||||
}
|
||||
|
||||
private loadAFFiNEProPublicKey() {
|
||||
if (AFFINE_PRO_PUBLIC_KEY) {
|
||||
return Buffer.from(AFFINE_PRO_PUBLIC_KEY);
|
||||
} else {
|
||||
this.logger.warn('AFFINE_PRO_PUBLIC_KEY is not set at compile time.');
|
||||
}
|
||||
|
||||
if (!env.prod && process.env.AFFiNE_PRO_PUBLIC_KEY) {
|
||||
return Buffer.from(process.env.AFFiNE_PRO_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user