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:
forehalo
2025-05-09 04:16:05 +00:00
parent 3db91bdc8e
commit 93e01b4442
34 changed files with 718 additions and 187 deletions

View File

@@ -15,17 +15,14 @@ export const passwordLimitsFragment = `fragment PasswordLimits on PasswordLimits
minLength
maxLength
}`;
export const activateLicenseMutation = {
id: 'activateLicenseMutation' as const,
op: 'activateLicense',
query: `mutation activateLicense($workspaceId: String!, $license: String!) {
activateLicense(workspaceId: $workspaceId, license: $license) {
installedAt
validatedAt
}
}`,
};
export const licenseFragment = `fragment license on License {
expiredAt
installedAt
quantity
recurring
validatedAt
variant
}`;
export const adminServerConfigQuery = {
id: 'adminServerConfigQuery' as const,
op: 'adminServerConfig',
@@ -893,14 +890,6 @@ export const createWorkspaceMutation = {
}`,
};
export const deactivateLicenseMutation = {
id: 'deactivateLicenseMutation' as const,
op: 'deactivateLicense',
query: `mutation deactivateLicense($workspaceId: String!) {
deactivateLicense(workspaceId: $workspaceId)
}`,
};
export const deleteAccountMutation = {
id: 'deleteAccountMutation' as const,
op: 'deleteAccount',
@@ -1065,22 +1054,6 @@ export const getIsOwnerQuery = {
deprecations: ["'isOwner' is deprecated: use WorkspaceType[role] instead"],
};
export const getLicenseQuery = {
id: 'getLicenseQuery' as const,
op: 'getLicense',
query: `query getLicense($workspaceId: String!) {
workspace(id: $workspaceId) {
license {
expiredAt
installedAt
quantity
recurring
validatedAt
}
}
}`,
};
export const getMemberCountByWorkspaceIdQuery = {
id: 'getMemberCountByWorkspaceIdQuery' as const,
op: 'getMemberCountByWorkspaceId',
@@ -1391,6 +1364,50 @@ export const leaveWorkspaceMutation = {
}`,
};
export const activateLicenseMutation = {
id: 'activateLicenseMutation' as const,
op: 'activateLicense',
query: `mutation activateLicense($workspaceId: String!, $license: String!) {
activateLicense(workspaceId: $workspaceId, license: $license) {
...license
}
}
${licenseFragment}`,
};
export const deactivateLicenseMutation = {
id: 'deactivateLicenseMutation' as const,
op: 'deactivateLicense',
query: `mutation deactivateLicense($workspaceId: String!) {
deactivateLicense(workspaceId: $workspaceId)
}`,
};
export const getLicenseQuery = {
id: 'getLicenseQuery' as const,
op: 'getLicense',
query: `query getLicense($workspaceId: String!) {
workspace(id: $workspaceId) {
license {
...license
}
}
}
${licenseFragment}`,
};
export const installLicenseMutation = {
id: 'installLicenseMutation' as const,
op: 'installLicense',
query: `mutation installLicense($workspaceId: String!, $license: Upload!) {
installLicense(workspaceId: $workspaceId, license: $license) {
...license
}
}
${licenseFragment}`,
file: true,
};
export const listNotificationsQuery = {
id: 'listNotificationsQuery' as const,
op: 'listNotifications',

View File

@@ -1,6 +1,7 @@
#import './license.gql'
mutation activateLicense($workspaceId: String!, $license: String!) {
activateLicense(workspaceId: $workspaceId, license: $license) {
installedAt
validatedAt
...license
}
}

View File

@@ -1,11 +1,9 @@
#import './license.gql'
query getLicense($workspaceId: String!) {
workspace(id: $workspaceId) {
license {
expiredAt
installedAt
quantity
recurring
validatedAt
...license
}
}
}

View File

@@ -0,0 +1,7 @@
#import './license.gql'
mutation installLicense($workspaceId: String!, $license: Upload!) {
installLicense(workspaceId: $workspaceId, license: $license) {
...license
}
}

View File

@@ -0,0 +1,8 @@
fragment license on License {
expiredAt
installedAt
quantity
recurring
validatedAt
variant
}

View File

@@ -598,6 +598,7 @@ export type ErrorDataUnion =
| HttpRequestErrorDataType
| InvalidEmailDataType
| InvalidHistoryTimestampDataType
| InvalidLicenseToActivateDataType
| InvalidLicenseUpdateParamsDataType
| InvalidOauthCallbackCodeDataType
| InvalidPasswordLengthDataType
@@ -622,7 +623,6 @@ export type ErrorDataUnion =
| UnsupportedSubscriptionPlanDataType
| ValidationErrorDataType
| VersionRejectedDataType
| WorkspaceMembersExceedLimitToDowngradeDataType
| WorkspacePermissionNotFoundDataType
| WrongSignInCredentialsDataType;
@@ -701,6 +701,7 @@ export enum ErrorNames {
INVALID_PASSWORD_LENGTH = 'INVALID_PASSWORD_LENGTH',
INVALID_RUNTIME_CONFIG_TYPE = 'INVALID_RUNTIME_CONFIG_TYPE',
INVALID_SUBSCRIPTION_PARAMETERS = 'INVALID_SUBSCRIPTION_PARAMETERS',
LICENSE_EXPIRED = 'LICENSE_EXPIRED',
LICENSE_NOT_FOUND = 'LICENSE_NOT_FOUND',
LICENSE_REVEALED = 'LICENSE_REVEALED',
LINK_EXPIRED = 'LINK_EXPIRED',
@@ -749,7 +750,6 @@ export enum ErrorNames {
WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION = 'WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION',
WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION = 'WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION',
WORKSPACE_LICENSE_ALREADY_EXISTS = 'WORKSPACE_LICENSE_ALREADY_EXISTS',
WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE = 'WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE',
WORKSPACE_PERMISSION_NOT_FOUND = 'WORKSPACE_PERMISSION_NOT_FOUND',
WRONG_SIGN_IN_CREDENTIALS = 'WRONG_SIGN_IN_CREDENTIALS',
WRONG_SIGN_IN_METHOD = 'WRONG_SIGN_IN_METHOD',
@@ -837,6 +837,11 @@ export interface InvalidHistoryTimestampDataType {
timestamp: Scalars['String']['output'];
}
export interface InvalidLicenseToActivateDataType {
__typename?: 'InvalidLicenseToActivateDataType';
reason: Scalars['String']['output'];
}
export interface InvalidLicenseUpdateParamsDataType {
__typename?: 'InvalidLicenseUpdateParamsDataType';
reason: Scalars['String']['output'];
@@ -1029,6 +1034,7 @@ export interface License {
quantity: Scalars['Int']['output'];
recurring: SubscriptionRecurring;
validatedAt: Scalars['DateTime']['output'];
variant: Maybe<SubscriptionVariant>;
}
export interface LimitedUserType {
@@ -1166,6 +1172,7 @@ export interface Mutation {
grantMember: Scalars['Boolean']['output'];
/** import users */
importUsers: Array<UserImportResultType>;
installLicense: License;
/** @deprecated use [inviteMembers] instead */
inviteBatch: Array<InviteResult>;
inviteMembers: Array<InviteResult>;
@@ -1391,6 +1398,11 @@ export interface MutationImportUsersArgs {
input: ImportUsersInput;
}
export interface MutationInstallLicenseArgs {
license: Scalars['Upload']['input'];
workspaceId: Scalars['String']['input'];
}
export interface MutationInviteBatchArgs {
emails: Array<Scalars['String']['input']>;
sendInviteMail?: InputMaybe<Scalars['Boolean']['input']>;
@@ -2291,11 +2303,6 @@ export enum WorkspaceMemberStatus {
UnderReview = 'UnderReview',
}
export interface WorkspaceMembersExceedLimitToDowngradeDataType {
__typename?: 'WorkspaceMembersExceedLimitToDowngradeDataType';
limit: Scalars['Int']['output'];
}
export interface WorkspacePermissionNotFoundDataType {
__typename?: 'WorkspacePermissionNotFoundDataType';
spaceId: Scalars['String']['output'];
@@ -2474,20 +2481,6 @@ export interface TokenType {
token: Scalars['String']['output'];
}
export type ActivateLicenseMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
license: Scalars['String']['input'];
}>;
export type ActivateLicenseMutation = {
__typename?: 'Mutation';
activateLicense: {
__typename?: 'License';
installedAt: string;
validatedAt: string;
};
};
export type AdminServerConfigQueryVariables = Exact<{ [key: string]: never }>;
export type AdminServerConfigQuery = {
@@ -3495,15 +3488,6 @@ export type CreateWorkspaceMutation = {
};
};
export type DeactivateLicenseMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
export type DeactivateLicenseMutation = {
__typename?: 'Mutation';
deactivateLicense: boolean;
};
export type DeleteAccountMutationVariables = Exact<{ [key: string]: never }>;
export type DeleteAccountMutation = {
@@ -3693,25 +3677,6 @@ export type GetIsOwnerQueryVariables = Exact<{
export type GetIsOwnerQuery = { __typename?: 'Query'; isOwner: boolean };
export type GetLicenseQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
export type GetLicenseQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
license: {
__typename?: 'License';
expiredAt: string | null;
installedAt: string;
quantity: number;
recurring: SubscriptionRecurring;
validatedAt: string;
} | null;
};
};
export type GetMemberCountByWorkspaceIdQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
@@ -4059,6 +4024,81 @@ export type LeaveWorkspaceMutation = {
leaveWorkspace: boolean;
};
export type ActivateLicenseMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
license: Scalars['String']['input'];
}>;
export type ActivateLicenseMutation = {
__typename?: 'Mutation';
activateLicense: {
__typename?: 'License';
expiredAt: string | null;
installedAt: string;
quantity: number;
recurring: SubscriptionRecurring;
validatedAt: string;
variant: SubscriptionVariant | null;
};
};
export type DeactivateLicenseMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
export type DeactivateLicenseMutation = {
__typename?: 'Mutation';
deactivateLicense: boolean;
};
export type GetLicenseQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
export type GetLicenseQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
license: {
__typename?: 'License';
expiredAt: string | null;
installedAt: string;
quantity: number;
recurring: SubscriptionRecurring;
validatedAt: string;
variant: SubscriptionVariant | null;
} | null;
};
};
export type InstallLicenseMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
license: Scalars['Upload']['input'];
}>;
export type InstallLicenseMutation = {
__typename?: 'Mutation';
installLicense: {
__typename?: 'License';
expiredAt: string | null;
installedAt: string;
quantity: number;
recurring: SubscriptionRecurring;
validatedAt: string;
variant: SubscriptionVariant | null;
};
};
export type LicenseFragment = {
__typename?: 'License';
expiredAt: string | null;
installedAt: string;
quantity: number;
recurring: SubscriptionRecurring;
validatedAt: string;
variant: SubscriptionVariant | null;
};
export type ListNotificationsQueryVariables = Exact<{
pagination: PaginationInput;
}>;
@@ -4795,11 +4835,6 @@ export type Queries =
variables: GetIsOwnerQueryVariables;
response: GetIsOwnerQuery;
}
| {
name: 'getLicenseQuery';
variables: GetLicenseQueryVariables;
response: GetLicenseQuery;
}
| {
name: 'getMemberCountByWorkspaceIdQuery';
variables: GetMemberCountByWorkspaceIdQueryVariables;
@@ -4895,6 +4930,11 @@ export type Queries =
variables: InvoicesQueryVariables;
response: InvoicesQuery;
}
| {
name: 'getLicenseQuery';
variables: GetLicenseQueryVariables;
response: GetLicenseQuery;
}
| {
name: 'listNotificationsQuery';
variables: ListNotificationsQueryVariables;
@@ -4952,11 +4992,6 @@ export type Queries =
};
export type Mutations =
| {
name: 'activateLicenseMutation';
variables: ActivateLicenseMutationVariables;
response: ActivateLicenseMutation;
}
| {
name: 'createChangePasswordUrlMutation';
variables: CreateChangePasswordUrlMutationVariables;
@@ -5162,11 +5197,6 @@ export type Mutations =
variables: CreateWorkspaceMutationVariables;
response: CreateWorkspaceMutation;
}
| {
name: 'deactivateLicenseMutation';
variables: DeactivateLicenseMutationVariables;
response: DeactivateLicenseMutation;
}
| {
name: 'deleteAccountMutation';
variables: DeleteAccountMutationVariables;
@@ -5192,6 +5222,21 @@ export type Mutations =
variables: LeaveWorkspaceMutationVariables;
response: LeaveWorkspaceMutation;
}
| {
name: 'activateLicenseMutation';
variables: ActivateLicenseMutationVariables;
response: ActivateLicenseMutation;
}
| {
name: 'deactivateLicenseMutation';
variables: DeactivateLicenseMutationVariables;
response: DeactivateLicenseMutation;
}
| {
name: 'installLicenseMutation';
variables: InstallLicenseMutationVariables;
response: InstallLicenseMutation;
}
| {
name: 'mentionUserMutation';
variables: MentionUserMutationVariables;