mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00: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:
2
.github/workflows/build-images.yml
vendored
2
.github/workflows/build-images.yml
vendored
@@ -136,6 +136,8 @@ jobs:
|
|||||||
extra-flags: workspaces focus @affine/server-native
|
extra-flags: workspaces focus @affine/server-native
|
||||||
- name: Build Rust
|
- name: Build Rust
|
||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
|
env:
|
||||||
|
AFFINE_PRO_PUBLIC_KEY: ${{ secrets.AFFINE_PRO_PUBLIC_KEY }}
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.targets.name }}
|
target: ${{ matrix.targets.name }}
|
||||||
package: '@affine/server-native'
|
package: '@affine/server-native'
|
||||||
|
|||||||
2
packages/backend/native/index.d.ts
vendored
2
packages/backend/native/index.d.ts
vendored
@@ -4,6 +4,8 @@ export declare class Tokenizer {
|
|||||||
count(content: string, allowedSpecial?: Array<string> | undefined | null): number
|
count(content: string, allowedSpecial?: Array<string> | undefined | null): number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
|
||||||
|
|
||||||
export declare function fromModelName(modelName: string): Tokenizer | null
|
export declare function fromModelName(modelName: string): Tokenizer | null
|
||||||
|
|
||||||
export declare function getMime(input: Uint8Array): string
|
export declare function getMime(input: Uint8Array): string
|
||||||
|
|||||||
@@ -52,3 +52,6 @@ pub fn merge_updates_in_apply_way(updates: Vec<Buffer>) -> Result<Buffer> {
|
|||||||
|
|
||||||
Ok(buf.into())
|
Ok(buf.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub const AFFINE_PRO_PUBLIC_KEY: Option<&'static str> = std::option_env!("AFFINE_PRO_PUBLIC_KEY",);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "installed_licenses" ADD COLUMN "license" BYTEA,
|
||||||
|
ADD COLUMN "variant" VARCHAR;
|
||||||
@@ -767,10 +767,12 @@ model InstalledLicense {
|
|||||||
workspaceId String @unique @map("workspace_id") @db.VarChar
|
workspaceId String @unique @map("workspace_id") @db.VarChar
|
||||||
quantity Int @default(1) @db.Integer
|
quantity Int @default(1) @db.Integer
|
||||||
recurring String @db.VarChar
|
recurring String @db.VarChar
|
||||||
|
variant String? @db.VarChar
|
||||||
installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3)
|
installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3)
|
||||||
validateKey String @map("validate_key") @db.VarChar
|
validateKey String @map("validate_key") @db.VarChar
|
||||||
validatedAt DateTime @map("validated_at") @db.Timestamptz(3)
|
validatedAt DateTime @map("validated_at") @db.Timestamptz(3)
|
||||||
expiredAt DateTime? @map("expired_at") @db.Timestamptz(3)
|
expiredAt DateTime? @map("expired_at") @db.Timestamptz(3)
|
||||||
|
license Bytes? @db.ByteA
|
||||||
|
|
||||||
@@map("installed_licenses")
|
@@map("installed_licenses")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,11 +77,24 @@ export class TestingApp extends NestApplication {
|
|||||||
assert(init.body, 'body is required for gql request');
|
assert(init.body, 'body is required for gql request');
|
||||||
assert(init.headers, 'headers is required for gql request');
|
assert(init.headers, 'headers is required for gql request');
|
||||||
|
|
||||||
const res = await this.request('post', '/graphql')
|
const req = this.request('post', '/graphql')
|
||||||
.send(init?.body)
|
|
||||||
.set('accept', 'application/json')
|
.set('accept', 'application/json')
|
||||||
.set(init.headers as Record<string, string>);
|
.set(init.headers as Record<string, string>);
|
||||||
|
|
||||||
|
if (init.body instanceof FormData) {
|
||||||
|
for (const [key, value] of init.body.entries()) {
|
||||||
|
if (value instanceof File) {
|
||||||
|
req.attach(key, Buffer.from(await value.arrayBuffer()));
|
||||||
|
} else {
|
||||||
|
req.field(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
req.send(init.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await req;
|
||||||
|
|
||||||
return new Response(Buffer.from(JSON.stringify(res.body)), {
|
return new Response(Buffer.from(JSON.stringify(res.body)), {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
headers: res.headers,
|
headers: res.headers,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,144 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
import { installLicenseMutation, SubscriptionVariant } from '@affine/graphql';
|
||||||
|
|
||||||
|
import { Workspace, WorkspaceRole } from '../../../models';
|
||||||
|
import {
|
||||||
|
createApp,
|
||||||
|
e2e,
|
||||||
|
MockedUser,
|
||||||
|
Mockers,
|
||||||
|
refreshEnv,
|
||||||
|
type TestingApp,
|
||||||
|
} from '../test';
|
||||||
|
|
||||||
|
const testWorkspaceId = 'd6f52bc7-d62a-4822-804a-335fa7dfe5a6';
|
||||||
|
const testPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkXqe9HR32rSgaNxaePXbwHxoanUq
|
||||||
|
1DcQTV1twn+G47/HiY+rj7oOw3cLNzOVe7+4Uxn8SMZ/XLImtqFQ6I4WVg==
|
||||||
|
-----END PUBLIC KEY-----`;
|
||||||
|
|
||||||
|
const fixturesDir = join(import.meta.dirname, '__fixtures__');
|
||||||
|
function getLicense(file: string) {
|
||||||
|
return new File([readFileSync(join(fixturesDir, file))], 'test-license.lic', {
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const licenses = {
|
||||||
|
valid: getLicense('valid.license'),
|
||||||
|
expired: getLicense('expired.license'),
|
||||||
|
expiredEndAt: getLicense('expired-end-at.license'),
|
||||||
|
};
|
||||||
|
|
||||||
|
let app: TestingApp;
|
||||||
|
let workspace: Workspace;
|
||||||
|
let owner: MockedUser;
|
||||||
|
|
||||||
|
e2e.before(async () => {
|
||||||
|
process.env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||||
|
process.env.AFFiNE_PRO_PUBLIC_KEY = testPublicKey;
|
||||||
|
refreshEnv();
|
||||||
|
|
||||||
|
app = await createApp();
|
||||||
|
await app.models.workspace.delete(testWorkspaceId);
|
||||||
|
owner = await app.signup();
|
||||||
|
workspace = await app.create(Mockers.Workspace, {
|
||||||
|
id: testWorkspaceId,
|
||||||
|
owner,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e.beforeEach(async () => {
|
||||||
|
await app.login(owner);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e.after.always(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should install file license', async t => {
|
||||||
|
const res = await app.gql({
|
||||||
|
query: installLicenseMutation,
|
||||||
|
variables: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
license: licenses.valid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(res.installLicense.variant, SubscriptionVariant.Onetime);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should not allow to install license if not owner', async t => {
|
||||||
|
const user = await app.signup();
|
||||||
|
await app.create(Mockers.WorkspaceUser, {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
userId: user.id,
|
||||||
|
type: WorkspaceRole.Collaborator,
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.throwsAsync(
|
||||||
|
app.gql({
|
||||||
|
query: installLicenseMutation,
|
||||||
|
variables: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
license: licenses.valid,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: `You do not have permission to access Space ${workspace.id}.`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e(`should not install other workspace's license file`, async t => {
|
||||||
|
const owner = await app.signup();
|
||||||
|
const workspace = await app.create(Mockers.Workspace, {
|
||||||
|
owner,
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.throwsAsync(
|
||||||
|
app.gql({
|
||||||
|
query: installLicenseMutation,
|
||||||
|
variables: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
license: licenses.valid,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Invalid license to activate. Workspace mismatched with license.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should not install expired license', async t => {
|
||||||
|
await t.throwsAsync(
|
||||||
|
app.gql({
|
||||||
|
query: installLicenseMutation,
|
||||||
|
variables: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
license: licenses.expired,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: 'License has expired.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should not install license with expired end date', async t => {
|
||||||
|
await t.throwsAsync(
|
||||||
|
app.gql({
|
||||||
|
query: installLicenseMutation,
|
||||||
|
variables: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
license: licenses.expiredEndAt,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: 'License has expired.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import test, { registerCompletionHandler } from 'ava';
|
import test, { registerCompletionHandler } from 'ava';
|
||||||
|
|
||||||
|
import { Env } from '../../env';
|
||||||
import { type TestingApp } from './create-app';
|
import { type TestingApp } from './create-app';
|
||||||
|
|
||||||
export const e2e = test;
|
export const e2e = test;
|
||||||
@@ -9,3 +10,11 @@ export const app: TestingApp = globalThis.app;
|
|||||||
registerCompletionHandler(async () => {
|
registerCompletionHandler(async () => {
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function refreshEnv() {
|
||||||
|
globalThis.env = new Env();
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from '../mocks';
|
||||||
|
export { createApp } from './create-app';
|
||||||
|
export type { TestingApp };
|
||||||
|
|||||||
@@ -811,18 +811,17 @@ export const USER_FRIENDLY_ERRORS = {
|
|||||||
},
|
},
|
||||||
invalid_license_to_activate: {
|
invalid_license_to_activate: {
|
||||||
type: 'bad_request',
|
type: 'bad_request',
|
||||||
message: 'Invalid license to activate.',
|
args: { reason: 'string' },
|
||||||
|
message: ({ reason }) => `Invalid license to activate. ${reason}`,
|
||||||
},
|
},
|
||||||
invalid_license_update_params: {
|
invalid_license_update_params: {
|
||||||
type: 'invalid_input',
|
type: 'invalid_input',
|
||||||
args: { reason: 'string' },
|
args: { reason: 'string' },
|
||||||
message: ({ reason }) => `Invalid license update params. ${reason}`,
|
message: ({ reason }) => `Invalid license update params. ${reason}`,
|
||||||
},
|
},
|
||||||
workspace_members_exceed_limit_to_downgrade: {
|
license_expired: {
|
||||||
type: 'bad_request',
|
type: 'bad_request',
|
||||||
args: { limit: 'number' },
|
message: 'License has expired.',
|
||||||
message: ({ limit }) =>
|
|
||||||
`You cannot downgrade the workspace from team workspace because there are more than ${limit} members that are currently active.`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// version errors
|
// version errors
|
||||||
|
|||||||
@@ -914,10 +914,14 @@ export class LicenseNotFound extends UserFriendlyError {
|
|||||||
super('resource_not_found', 'license_not_found', message);
|
super('resource_not_found', 'license_not_found', message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ObjectType()
|
||||||
|
class InvalidLicenseToActivateDataType {
|
||||||
|
@Field() reason!: string
|
||||||
|
}
|
||||||
|
|
||||||
export class InvalidLicenseToActivate extends UserFriendlyError {
|
export class InvalidLicenseToActivate extends UserFriendlyError {
|
||||||
constructor(message?: string) {
|
constructor(args: InvalidLicenseToActivateDataType, message?: string | ((args: InvalidLicenseToActivateDataType) => string)) {
|
||||||
super('bad_request', 'invalid_license_to_activate', message);
|
super('bad_request', 'invalid_license_to_activate', message, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -930,14 +934,10 @@ export class InvalidLicenseUpdateParams extends UserFriendlyError {
|
|||||||
super('invalid_input', 'invalid_license_update_params', message, args);
|
super('invalid_input', 'invalid_license_update_params', message, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ObjectType()
|
|
||||||
class WorkspaceMembersExceedLimitToDowngradeDataType {
|
|
||||||
@Field() limit!: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkspaceMembersExceedLimitToDowngrade extends UserFriendlyError {
|
export class LicenseExpired extends UserFriendlyError {
|
||||||
constructor(args: WorkspaceMembersExceedLimitToDowngradeDataType, message?: string | ((args: WorkspaceMembersExceedLimitToDowngradeDataType) => string)) {
|
constructor(message?: string) {
|
||||||
super('bad_request', 'workspace_members_exceed_limit_to_downgrade', message, args);
|
super('bad_request', 'license_expired', message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -1100,7 +1100,7 @@ export enum ErrorNames {
|
|||||||
LICENSE_NOT_FOUND,
|
LICENSE_NOT_FOUND,
|
||||||
INVALID_LICENSE_TO_ACTIVATE,
|
INVALID_LICENSE_TO_ACTIVATE,
|
||||||
INVALID_LICENSE_UPDATE_PARAMS,
|
INVALID_LICENSE_UPDATE_PARAMS,
|
||||||
WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE,
|
LICENSE_EXPIRED,
|
||||||
UNSUPPORTED_CLIENT_VERSION,
|
UNSUPPORTED_CLIENT_VERSION,
|
||||||
NOTIFICATION_NOT_FOUND,
|
NOTIFICATION_NOT_FOUND,
|
||||||
MENTION_USER_DOC_ACCESS_DENIED,
|
MENTION_USER_DOC_ACCESS_DENIED,
|
||||||
@@ -1114,5 +1114,5 @@ registerEnumType(ErrorNames, {
|
|||||||
export const ErrorDataUnionType = createUnionType({
|
export const ErrorDataUnionType = createUnionType({
|
||||||
name: 'ErrorDataUnion',
|
name: 'ErrorDataUnion',
|
||||||
types: () =>
|
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,
|
// we are using a stub to make sure the iv is always 0,
|
||||||
// the encrypted result will always be the same
|
// the encrypted result will always be the same
|
||||||
t.is(encrypted, 'AAAAAAAAAAAAAAAAWUDlJRhzP+SZ3avvmLcgnou+q4E11w==');
|
t.is(encrypted, 'AAAAAAAAAAAAAAAAOXbR/9glITL3BcO3kPd6fGOMasSkPQ==');
|
||||||
t.is(decrypted, data);
|
t.is(decrypted, data);
|
||||||
|
|
||||||
stub.restore();
|
stub.restore();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
createCipheriv,
|
createCipheriv,
|
||||||
createDecipheriv,
|
createDecipheriv,
|
||||||
createHash,
|
createHash,
|
||||||
createPrivateKey,
|
|
||||||
createPublicKey,
|
createPublicKey,
|
||||||
createSign,
|
createSign,
|
||||||
createVerify,
|
createVerify,
|
||||||
@@ -12,12 +11,13 @@ import {
|
|||||||
timingSafeEqual,
|
timingSafeEqual,
|
||||||
} from 'node:crypto';
|
} from 'node:crypto';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
hash as hashPassword,
|
hash as hashPassword,
|
||||||
verify as verifyPassword,
|
verify as verifyPassword,
|
||||||
} from '@node-rs/argon2';
|
} from '@node-rs/argon2';
|
||||||
|
|
||||||
|
import { AFFINE_PRO_PUBLIC_KEY } from '../../native';
|
||||||
import { Config } from '../config';
|
import { Config } from '../config';
|
||||||
import { OnEvent } from '../event';
|
import { OnEvent } from '../event';
|
||||||
|
|
||||||
@@ -37,20 +37,7 @@ function generatePrivateKey(): string {
|
|||||||
return key.toString('utf8');
|
return key.toString('utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPrivateKey(privateKey: string) {
|
function generatePublicKey(privateKey: string) {
|
||||||
return createPrivateKey({
|
|
||||||
key: Buffer.from(privateKey),
|
|
||||||
format: 'pem',
|
|
||||||
type: 'sec1',
|
|
||||||
})
|
|
||||||
.export({
|
|
||||||
format: 'pem',
|
|
||||||
type: 'pkcs8',
|
|
||||||
})
|
|
||||||
.toString('utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPublicKey(privateKey: string) {
|
|
||||||
return createPublicKey({
|
return createPublicKey({
|
||||||
key: Buffer.from(privateKey),
|
key: Buffer.from(privateKey),
|
||||||
})
|
})
|
||||||
@@ -59,7 +46,9 @@ function readPublicKey(privateKey: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CryptoHelper {
|
export class CryptoHelper implements OnModuleInit {
|
||||||
|
logger = new Logger(CryptoHelper.name);
|
||||||
|
|
||||||
keyPair!: {
|
keyPair!: {
|
||||||
publicKey: Buffer;
|
publicKey: Buffer;
|
||||||
privateKey: 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) {}
|
constructor(private readonly config: Config) {}
|
||||||
|
|
||||||
@OnEvent('config.init')
|
@OnEvent('config.init')
|
||||||
@@ -84,9 +81,8 @@ export class CryptoHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setup() {
|
private setup() {
|
||||||
const key = this.config.crypto.privateKey || generatePrivateKey();
|
const privateKey = this.config.crypto.privateKey || generatePrivateKey();
|
||||||
const privateKey = readPrivateKey(key);
|
const publicKey = generatePublicKey(privateKey);
|
||||||
const publicKey = readPublicKey(key);
|
|
||||||
|
|
||||||
this.keyPair = {
|
this.keyPair = {
|
||||||
publicKey: Buffer.from(publicKey),
|
publicKey: Buffer.from(publicKey),
|
||||||
@@ -187,4 +183,18 @@ export class CryptoHelper {
|
|||||||
sha256(data: string) {
|
sha256(data: string) {
|
||||||
return createHash('sha256').update(data).digest();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,6 +257,11 @@ export class WorkspaceService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
quantity
|
quantity
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!pendings.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const owner = await this.models.workspaceUser.getOwner(workspaceId);
|
const owner = await this.models.workspaceUser.getOwner(workspaceId);
|
||||||
for (const member of pendings) {
|
for (const member of pendings) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { resolve } from 'node:path';
|
import { homedir } from 'node:os';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
import pkg from '../package.json' with { type: 'json' };
|
import pkg from '../package.json' with { type: 'json' };
|
||||||
@@ -9,6 +10,8 @@ declare global {
|
|||||||
var env: Readonly<Env>;
|
var env: Readonly<Env>;
|
||||||
// oxlint-disable-next-line no-var
|
// oxlint-disable-next-line no-var
|
||||||
var readEnv: <T>(key: string, defaultValue: T, availableValues?: T[]) => T;
|
var readEnv: <T>(key: string, defaultValue: T, availableValues?: T[]) => T;
|
||||||
|
// oxlint-disable-next-line no-var
|
||||||
|
var CUSTOM_CONFIG_PATH: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +53,7 @@ export type AppEnv = {
|
|||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
globalThis.CUSTOM_CONFIG_PATH = join(homedir(), '.affine/config');
|
||||||
globalThis.readEnv = function readEnv<T>(
|
globalThis.readEnv = function readEnv<T>(
|
||||||
env: string,
|
env: string,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
|
|||||||
@@ -427,6 +427,6 @@ export class WorkspaceUserModel extends BaseModel {
|
|||||||
data: { status: WorkspaceMemberStatus.NeedMoreSeat },
|
data: { status: WorkspaceMemberStatus.NeedMoreSeat },
|
||||||
});
|
});
|
||||||
|
|
||||||
return groups.Email;
|
return groups.Email ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ export const parseDoc = serverNativeModule.parseDoc;
|
|||||||
export const Tokenizer = serverNativeModule.Tokenizer;
|
export const Tokenizer = serverNativeModule.Tokenizer;
|
||||||
export const fromModelName = serverNativeModule.fromModelName;
|
export const fromModelName = serverNativeModule.fromModelName;
|
||||||
export const htmlSanitize = serverNativeModule.htmlSanitize;
|
export const htmlSanitize = serverNativeModule.htmlSanitize;
|
||||||
|
export const AFFINE_PRO_PUBLIC_KEY = serverNativeModule.AFFINE_PRO_PUBLIC_KEY;
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ import {
|
|||||||
ResolveField,
|
ResolveField,
|
||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
|
import GraphQLUpload, {
|
||||||
|
type FileUpload,
|
||||||
|
} from 'graphql-upload/GraphQLUpload.mjs';
|
||||||
|
|
||||||
import { UseNamedGuard } from '../../base';
|
import { toBuffer, UseNamedGuard } from '../../base';
|
||||||
import { CurrentUser } from '../../core/auth';
|
import { CurrentUser } from '../../core/auth';
|
||||||
import { AccessController } from '../../core/permission';
|
import { AccessController } from '../../core/permission';
|
||||||
import { WorkspaceType } from '../../core/workspaces';
|
import { WorkspaceType } from '../../core/workspaces';
|
||||||
import { SubscriptionRecurring } from '../payment/types';
|
import { SubscriptionRecurring, SubscriptionVariant } from '../payment/types';
|
||||||
import { LicenseService } from './service';
|
import { LicenseService } from './service';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -24,6 +27,9 @@ export class License {
|
|||||||
@Field(() => SubscriptionRecurring)
|
@Field(() => SubscriptionRecurring)
|
||||||
recurring!: string;
|
recurring!: string;
|
||||||
|
|
||||||
|
@Field(() => SubscriptionVariant, { nullable: true })
|
||||||
|
variant!: string | null;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
installedAt!: Date;
|
installedAt!: Date;
|
||||||
|
|
||||||
@@ -82,7 +88,7 @@ export class LicenseResolver {
|
|||||||
.workspace(workspaceId)
|
.workspace(workspaceId)
|
||||||
.assert('Workspace.Payment.Manage');
|
.assert('Workspace.Payment.Manage');
|
||||||
|
|
||||||
return this.service.deactivateTeamLicense(workspaceId);
|
return this.service.removeTeamLicense(workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => String)
|
@Mutation(() => String)
|
||||||
@@ -99,4 +105,22 @@ export class LicenseResolver {
|
|||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => License)
|
||||||
|
async installLicense(
|
||||||
|
@CurrentUser() user: CurrentUser,
|
||||||
|
@Args('workspaceId') workspaceId: string,
|
||||||
|
@Args('license', { type: () => GraphQLUpload }) licenseFile: FileUpload
|
||||||
|
) {
|
||||||
|
await this.ac
|
||||||
|
.user(user.id)
|
||||||
|
.workspace(workspaceId)
|
||||||
|
.assert('Workspace.Payment.Manage');
|
||||||
|
|
||||||
|
const buffer = await toBuffer(licenseFile.createReadStream());
|
||||||
|
|
||||||
|
const license = await this.service.installLicense(workspaceId, buffer);
|
||||||
|
|
||||||
|
return license;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
|
import { createDecipheriv, createVerify } from 'node:crypto';
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import { InstalledLicense, PrismaClient } from '@prisma/client';
|
import { InstalledLicense, PrismaClient } from '@prisma/client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CryptoHelper,
|
||||||
EventBus,
|
EventBus,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
|
InvalidLicenseToActivate,
|
||||||
|
LicenseExpired,
|
||||||
LicenseNotFound,
|
LicenseNotFound,
|
||||||
OnEvent,
|
OnEvent,
|
||||||
UserFriendlyError,
|
UserFriendlyError,
|
||||||
WorkspaceLicenseAlreadyExists,
|
WorkspaceLicenseAlreadyExists,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { Models } from '../../models';
|
import { Models } from '../../models';
|
||||||
import { SubscriptionPlan, SubscriptionRecurring } from '../payment/types';
|
import {
|
||||||
|
SubscriptionPlan,
|
||||||
|
SubscriptionRecurring,
|
||||||
|
SubscriptionVariant,
|
||||||
|
} from '../payment/types';
|
||||||
|
|
||||||
interface License {
|
interface License {
|
||||||
plan: SubscriptionPlan;
|
plan: SubscriptionPlan;
|
||||||
@@ -20,6 +30,27 @@ interface License {
|
|||||||
endAt: number;
|
endAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BaseLicenseSchema = z.object({
|
||||||
|
entity: z.string().nonempty(),
|
||||||
|
issuer: z.string().nonempty(),
|
||||||
|
issuedAt: z.string().datetime(),
|
||||||
|
expiresAt: z.string().datetime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const TeamLicenseSchema = z
|
||||||
|
.object({
|
||||||
|
subject: z.literal(SubscriptionPlan.SelfHostedTeam),
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().nonempty(),
|
||||||
|
workspaceId: z.string().nonempty(),
|
||||||
|
plan: z.literal(SubscriptionPlan.SelfHostedTeam),
|
||||||
|
recurring: z.nativeEnum(SubscriptionRecurring),
|
||||||
|
quantity: z.number().positive(),
|
||||||
|
endAt: z.string().datetime(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.extend(BaseLicenseSchema.shape);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LicenseService {
|
export class LicenseService {
|
||||||
private readonly logger = new Logger(LicenseService.name);
|
private readonly logger = new Logger(LicenseService.name);
|
||||||
@@ -27,7 +58,8 @@ export class LicenseService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly db: PrismaClient,
|
private readonly db: PrismaClient,
|
||||||
private readonly event: EventBus,
|
private readonly event: EventBus,
|
||||||
private readonly models: Models
|
private readonly models: Models,
|
||||||
|
private readonly crypto: CryptoHelper
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@OnEvent('workspace.subscription.activated')
|
@OnEvent('workspace.subscription.activated')
|
||||||
@@ -79,6 +111,7 @@ export class LicenseService {
|
|||||||
expiredAt: true,
|
expiredAt: true,
|
||||||
quantity: true,
|
quantity: true,
|
||||||
recurring: true,
|
recurring: true,
|
||||||
|
variant: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -86,6 +119,51 @@ export class LicenseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async installLicense(workspaceId: string, license: Buffer) {
|
||||||
|
const payload = this.decryptWorkspaceTeamLicense(workspaceId, license);
|
||||||
|
const data = payload.data;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (new Date(payload.expiresAt) < now || new Date(data.endAt) < now) {
|
||||||
|
throw new LicenseExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
const installed = await this.db.installedLicense.upsert({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
key: data.id,
|
||||||
|
expiredAt: new Date(data.endAt),
|
||||||
|
validatedAt: new Date(),
|
||||||
|
recurring: data.recurring,
|
||||||
|
quantity: data.quantity,
|
||||||
|
variant: SubscriptionVariant.Onetime,
|
||||||
|
license,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
key: data.id,
|
||||||
|
workspaceId,
|
||||||
|
expiredAt: new Date(data.endAt),
|
||||||
|
validateKey: '',
|
||||||
|
validatedAt: new Date(),
|
||||||
|
recurring: data.recurring,
|
||||||
|
quantity: data.quantity,
|
||||||
|
variant: SubscriptionVariant.Onetime,
|
||||||
|
license,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.event.emitAsync('workspace.subscription.activated', {
|
||||||
|
workspaceId,
|
||||||
|
plan: data.plan,
|
||||||
|
recurring: data.recurring,
|
||||||
|
quantity: data.quantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
async activateTeamLicense(workspaceId: string, licenseKey: string) {
|
async activateTeamLicense(workspaceId: string, licenseKey: string) {
|
||||||
const installedLicense = await this.getLicense(workspaceId);
|
const installedLicense = await this.getLicense(workspaceId);
|
||||||
|
|
||||||
@@ -132,7 +210,7 @@ export class LicenseService {
|
|||||||
return license;
|
return license;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deactivateTeamLicense(workspaceId: string) {
|
async removeTeamLicense(workspaceId: string) {
|
||||||
const license = await this.db.installedLicense.findUnique({
|
const license = await this.db.installedLicense.findUnique({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -143,24 +221,31 @@ export class LicenseService {
|
|||||||
throw new LicenseNotFound();
|
throw new LicenseNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.fetchAffinePro(`/api/team/licenses/${license.key}/deactivate`, {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.installedLicense.deleteMany({
|
await this.db.installedLicense.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId: license.workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (license.variant !== SubscriptionVariant.Onetime) {
|
||||||
|
await this.deactivateTeamLicense(license);
|
||||||
|
}
|
||||||
|
|
||||||
this.event.emit('workspace.subscription.canceled', {
|
this.event.emit('workspace.subscription.canceled', {
|
||||||
workspaceId,
|
workspaceId: license.workspaceId,
|
||||||
plan: SubscriptionPlan.SelfHostedTeam,
|
plan: SubscriptionPlan.SelfHostedTeam,
|
||||||
recurring: SubscriptionRecurring.Monthly,
|
recurring: license.recurring as SubscriptionRecurring,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deactivateTeamLicense(license: InstalledLicense) {
|
||||||
|
await this.fetchAffinePro(`/api/team/licenses/${license.key}/deactivate`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateTeamRecurring(key: string, recurring: SubscriptionRecurring) {
|
async updateTeamRecurring(key: string, recurring: SubscriptionRecurring) {
|
||||||
await this.fetchAffinePro(`/api/team/licenses/${key}/recurring`, {
|
await this.fetchAffinePro(`/api/team/licenses/${key}/recurring`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -222,7 +307,7 @@ export class LicenseService {
|
|||||||
let tried = 0;
|
let tried = 0;
|
||||||
while (tried++ < 10) {
|
while (tried++ < 10) {
|
||||||
try {
|
try {
|
||||||
const res = await this.revalidateLicense(license);
|
const res = await this.revalidateRecurringLicense(license);
|
||||||
|
|
||||||
if (res?.quantity === memberRequired) {
|
if (res?.quantity === memberRequired) {
|
||||||
return;
|
return;
|
||||||
@@ -249,11 +334,15 @@ export class LicenseService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const license of licenses) {
|
for (const license of licenses) {
|
||||||
await this.revalidateLicense(license);
|
if (license.variant === SubscriptionVariant.Onetime) {
|
||||||
|
this.revalidateOnetimeLicense(license);
|
||||||
|
} else {
|
||||||
|
await this.revalidateRecurringLicense(license);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async revalidateLicense(license: InstalledLicense) {
|
private async revalidateRecurringLicense(license: InstalledLicense) {
|
||||||
try {
|
try {
|
||||||
const res = await this.fetchAffinePro<License>(
|
const res = await this.fetchAffinePro<License>(
|
||||||
`/api/team/licenses/${license.key}/health`,
|
`/api/team/licenses/${license.key}/health`,
|
||||||
@@ -342,4 +431,132 @@ export class LicenseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private revalidateOnetimeLicense(license: InstalledLicense) {
|
||||||
|
const buf = license.license;
|
||||||
|
let valid = !!buf;
|
||||||
|
|
||||||
|
if (buf) {
|
||||||
|
try {
|
||||||
|
const { data } = this.decryptWorkspaceTeamLicense(
|
||||||
|
license.workspaceId,
|
||||||
|
Buffer.from(buf)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (new Date(data.endAt) < new Date()) {
|
||||||
|
valid = false;
|
||||||
|
} else {
|
||||||
|
this.event.emit('workspace.subscription.activated', {
|
||||||
|
workspaceId: license.workspaceId,
|
||||||
|
plan: data.plan,
|
||||||
|
recurring: data.recurring,
|
||||||
|
quantity: data.quantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
this.event.emit('workspace.subscription.canceled', {
|
||||||
|
workspaceId: license.workspaceId,
|
||||||
|
plan: SubscriptionPlan.SelfHostedTeam,
|
||||||
|
recurring: SubscriptionRecurring.Monthly,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private decryptWorkspaceTeamLicense(workspaceId: string, buf: Buffer) {
|
||||||
|
if (!this.crypto.AFFiNEProPublicKey) {
|
||||||
|
throw new InternalServerError(
|
||||||
|
'License public key is not loaded. Please contact with Affine support.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we use workspace id as aes key hash plain text content
|
||||||
|
// verify signature to make sure the payload or signature is not forged
|
||||||
|
const {
|
||||||
|
payload: payloadStr,
|
||||||
|
signature,
|
||||||
|
iv,
|
||||||
|
} = this.decryptLicense(workspaceId, buf);
|
||||||
|
|
||||||
|
const verifier = createVerify('rsa-sha256');
|
||||||
|
verifier.update(iv);
|
||||||
|
verifier.update(payloadStr);
|
||||||
|
const valid = verifier.verify(
|
||||||
|
this.crypto.AFFiNEProPublicKey,
|
||||||
|
signature,
|
||||||
|
'hex'
|
||||||
|
);
|
||||||
|
if (!valid) {
|
||||||
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'Invalid license signature.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.parse(payloadStr);
|
||||||
|
|
||||||
|
const parseResult = TeamLicenseSchema.safeParse(payload);
|
||||||
|
|
||||||
|
if (!parseResult.success) {
|
||||||
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'Invalid license payload.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseResult.data.data.workspaceId !== workspaceId) {
|
||||||
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'Workspace mismatched with license.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResult.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private decryptLicense(workspaceId: string, buf: Buffer) {
|
||||||
|
if (buf.length < 2) {
|
||||||
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'Invalid license file.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ivLength = buf.readUint8(0);
|
||||||
|
const authTagLength = buf.readUInt8(1);
|
||||||
|
|
||||||
|
const iv = buf.subarray(2, 2 + ivLength);
|
||||||
|
const tag = buf.subarray(2 + ivLength, 2 + ivLength + authTagLength);
|
||||||
|
const payload = buf.subarray(2 + ivLength + authTagLength);
|
||||||
|
|
||||||
|
const aesKey = this.crypto.sha256(
|
||||||
|
`WORKSPACE_PAYLOAD_AES_KEY:${workspaceId}`
|
||||||
|
);
|
||||||
|
const decipher = createDecipheriv('aes-256-gcm', aesKey, iv, {
|
||||||
|
authTagLength,
|
||||||
|
});
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
|
const decrypted = Buffer.concat([
|
||||||
|
decipher.update(payload),
|
||||||
|
decipher.final(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const data = JSON.parse(decrypted.toString('utf-8')) as {
|
||||||
|
payload: string;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
iv,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// we use workspace id as aes key hash plain text content
|
||||||
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'Workspace mismatched with license.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ export class LicenseController {
|
|||||||
await using lock = await this.mutex.acquire(`license-activation:${key}`);
|
await using lock = await this.mutex.acquire(`license-activation:${key}`);
|
||||||
|
|
||||||
if (!lock) {
|
if (!lock) {
|
||||||
throw new InvalidLicenseToActivate();
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'Too Many Requests',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const license = await this.db.license.findUnique({
|
const license = await this.db.license.findUnique({
|
||||||
@@ -72,7 +74,9 @@ export class LicenseController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!license) {
|
if (!license) {
|
||||||
throw new InvalidLicenseToActivate();
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'License not found',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await this.manager.getSubscription({
|
const subscription = await this.manager.getSubscription({
|
||||||
@@ -85,7 +89,9 @@ export class LicenseController {
|
|||||||
license.installedAt ||
|
license.installedAt ||
|
||||||
subscription.status !== SubscriptionStatus.Active
|
subscription.status !== SubscriptionStatus.Active
|
||||||
) {
|
) {
|
||||||
throw new InvalidLicenseToActivate();
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'Invalid license',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateKey = randomUUID();
|
const validateKey = randomUUID();
|
||||||
@@ -144,7 +150,9 @@ export class LicenseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (license.validateKey && license.validateKey !== revalidateKey) {
|
if (license.validateKey && license.validateKey !== revalidateKey) {
|
||||||
throw new InvalidLicenseToActivate();
|
throw new InvalidLicenseToActivate({
|
||||||
|
reason: 'Invalid validate key',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateKey = randomUUID();
|
const validateKey = randomUUID();
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
|
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
|
|
||||||
import { createGlobalEnv } from './env';
|
import { createGlobalEnv } from './env';
|
||||||
|
|
||||||
const CUSTOM_CONFIG_PATH = `${homedir()}/.affine/config`;
|
|
||||||
|
|
||||||
function loadPrivateKey() {
|
function loadPrivateKey() {
|
||||||
const file = join(CUSTOM_CONFIG_PATH, 'private.key');
|
const file = join(CUSTOM_CONFIG_PATH, 'private.key');
|
||||||
if (!process.env.AFFINE_PRIVATE_KEY && existsSync(file)) {
|
if (!process.env.AFFINE_PRIVATE_KEY && existsSync(file)) {
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ type EditorType {
|
|||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
|
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
|
||||||
|
|
||||||
enum ErrorNames {
|
enum ErrorNames {
|
||||||
ACCESS_DENIED
|
ACCESS_DENIED
|
||||||
@@ -541,6 +541,7 @@ 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_NOT_FOUND
|
LICENSE_NOT_FOUND
|
||||||
LICENSE_REVEALED
|
LICENSE_REVEALED
|
||||||
LINK_EXPIRED
|
LINK_EXPIRED
|
||||||
@@ -589,7 +590,6 @@ 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_PERMISSION_NOT_FOUND
|
WORKSPACE_PERMISSION_NOT_FOUND
|
||||||
WRONG_SIGN_IN_CREDENTIALS
|
WRONG_SIGN_IN_CREDENTIALS
|
||||||
WRONG_SIGN_IN_METHOD
|
WRONG_SIGN_IN_METHOD
|
||||||
@@ -671,6 +671,10 @@ type InvalidHistoryTimestampDataType {
|
|||||||
timestamp: String!
|
timestamp: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvalidLicenseToActivateDataType {
|
||||||
|
reason: String!
|
||||||
|
}
|
||||||
|
|
||||||
type InvalidLicenseUpdateParamsDataType {
|
type InvalidLicenseUpdateParamsDataType {
|
||||||
reason: String!
|
reason: String!
|
||||||
}
|
}
|
||||||
@@ -880,6 +884,7 @@ type License {
|
|||||||
quantity: Int!
|
quantity: Int!
|
||||||
recurring: SubscriptionRecurring!
|
recurring: SubscriptionRecurring!
|
||||||
validatedAt: DateTime!
|
validatedAt: DateTime!
|
||||||
|
variant: SubscriptionVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
type LimitedUserType {
|
type LimitedUserType {
|
||||||
@@ -1034,6 +1039,7 @@ type Mutation {
|
|||||||
|
|
||||||
"""import users"""
|
"""import users"""
|
||||||
importUsers(input: ImportUsersInput!): [UserImportResultType!]!
|
importUsers(input: ImportUsersInput!): [UserImportResultType!]!
|
||||||
|
installLicense(license: Upload!, workspaceId: String!): License!
|
||||||
inviteBatch(emails: [String!]!, sendInviteMail: Boolean @deprecated(reason: "never used"), workspaceId: String!): [InviteResult!]! @deprecated(reason: "use [inviteMembers] instead")
|
inviteBatch(emails: [String!]!, sendInviteMail: Boolean @deprecated(reason: "never used"), workspaceId: String!): [InviteResult!]! @deprecated(reason: "use [inviteMembers] instead")
|
||||||
inviteMembers(emails: [String!]!, workspaceId: String!): [InviteResult!]!
|
inviteMembers(emails: [String!]!, workspaceId: String!): [InviteResult!]!
|
||||||
leaveWorkspace(sendLeaveMail: Boolean @deprecated(reason: "no used anymore"), workspaceId: String!, workspaceName: String @deprecated(reason: "no longer used")): Boolean!
|
leaveWorkspace(sendLeaveMail: Boolean @deprecated(reason: "no used anymore"), workspaceId: String!, workspaceName: String @deprecated(reason: "no longer used")): Boolean!
|
||||||
@@ -1730,10 +1736,6 @@ enum WorkspaceMemberStatus {
|
|||||||
UnderReview
|
UnderReview
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceMembersExceedLimitToDowngradeDataType {
|
|
||||||
limit: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkspacePermissionNotFoundDataType {
|
type WorkspacePermissionNotFoundDataType {
|
||||||
spaceId: String!
|
spaceId: String!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,14 @@ export const passwordLimitsFragment = `fragment PasswordLimits on PasswordLimits
|
|||||||
minLength
|
minLength
|
||||||
maxLength
|
maxLength
|
||||||
}`;
|
}`;
|
||||||
export const activateLicenseMutation = {
|
export const licenseFragment = `fragment license on License {
|
||||||
id: 'activateLicenseMutation' as const,
|
expiredAt
|
||||||
op: 'activateLicense',
|
installedAt
|
||||||
query: `mutation activateLicense($workspaceId: String!, $license: String!) {
|
quantity
|
||||||
activateLicense(workspaceId: $workspaceId, license: $license) {
|
recurring
|
||||||
installedAt
|
validatedAt
|
||||||
validatedAt
|
variant
|
||||||
}
|
}`;
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const adminServerConfigQuery = {
|
export const adminServerConfigQuery = {
|
||||||
id: 'adminServerConfigQuery' as const,
|
id: 'adminServerConfigQuery' as const,
|
||||||
op: 'adminServerConfig',
|
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 = {
|
export const deleteAccountMutation = {
|
||||||
id: 'deleteAccountMutation' as const,
|
id: 'deleteAccountMutation' as const,
|
||||||
op: 'deleteAccount',
|
op: 'deleteAccount',
|
||||||
@@ -1065,22 +1054,6 @@ export const getIsOwnerQuery = {
|
|||||||
deprecations: ["'isOwner' is deprecated: use WorkspaceType[role] instead"],
|
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 = {
|
export const getMemberCountByWorkspaceIdQuery = {
|
||||||
id: 'getMemberCountByWorkspaceIdQuery' as const,
|
id: 'getMemberCountByWorkspaceIdQuery' as const,
|
||||||
op: 'getMemberCountByWorkspaceId',
|
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 = {
|
export const listNotificationsQuery = {
|
||||||
id: 'listNotificationsQuery' as const,
|
id: 'listNotificationsQuery' as const,
|
||||||
op: 'listNotifications',
|
op: 'listNotifications',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
#import './license.gql'
|
||||||
|
|
||||||
mutation activateLicense($workspaceId: String!, $license: String!) {
|
mutation activateLicense($workspaceId: String!, $license: String!) {
|
||||||
activateLicense(workspaceId: $workspaceId, license: $license) {
|
activateLicense(workspaceId: $workspaceId, license: $license) {
|
||||||
installedAt
|
...license
|
||||||
validatedAt
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
|
#import './license.gql'
|
||||||
|
|
||||||
query getLicense($workspaceId: String!) {
|
query getLicense($workspaceId: String!) {
|
||||||
workspace(id: $workspaceId) {
|
workspace(id: $workspaceId) {
|
||||||
license {
|
license {
|
||||||
expiredAt
|
...license
|
||||||
installedAt
|
|
||||||
quantity
|
|
||||||
recurring
|
|
||||||
validatedAt
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#import './license.gql'
|
||||||
|
|
||||||
|
mutation installLicense($workspaceId: String!, $license: Upload!) {
|
||||||
|
installLicense(workspaceId: $workspaceId, license: $license) {
|
||||||
|
...license
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/common/graphql/src/graphql/license/license.gql
Normal file
8
packages/common/graphql/src/graphql/license/license.gql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fragment license on License {
|
||||||
|
expiredAt
|
||||||
|
installedAt
|
||||||
|
quantity
|
||||||
|
recurring
|
||||||
|
validatedAt
|
||||||
|
variant
|
||||||
|
}
|
||||||
@@ -598,6 +598,7 @@ export type ErrorDataUnion =
|
|||||||
| HttpRequestErrorDataType
|
| HttpRequestErrorDataType
|
||||||
| InvalidEmailDataType
|
| InvalidEmailDataType
|
||||||
| InvalidHistoryTimestampDataType
|
| InvalidHistoryTimestampDataType
|
||||||
|
| InvalidLicenseToActivateDataType
|
||||||
| InvalidLicenseUpdateParamsDataType
|
| InvalidLicenseUpdateParamsDataType
|
||||||
| InvalidOauthCallbackCodeDataType
|
| InvalidOauthCallbackCodeDataType
|
||||||
| InvalidPasswordLengthDataType
|
| InvalidPasswordLengthDataType
|
||||||
@@ -622,7 +623,6 @@ export type ErrorDataUnion =
|
|||||||
| UnsupportedSubscriptionPlanDataType
|
| UnsupportedSubscriptionPlanDataType
|
||||||
| ValidationErrorDataType
|
| ValidationErrorDataType
|
||||||
| VersionRejectedDataType
|
| VersionRejectedDataType
|
||||||
| WorkspaceMembersExceedLimitToDowngradeDataType
|
|
||||||
| WorkspacePermissionNotFoundDataType
|
| WorkspacePermissionNotFoundDataType
|
||||||
| WrongSignInCredentialsDataType;
|
| WrongSignInCredentialsDataType;
|
||||||
|
|
||||||
@@ -701,6 +701,7 @@ export enum ErrorNames {
|
|||||||
INVALID_PASSWORD_LENGTH = 'INVALID_PASSWORD_LENGTH',
|
INVALID_PASSWORD_LENGTH = 'INVALID_PASSWORD_LENGTH',
|
||||||
INVALID_RUNTIME_CONFIG_TYPE = 'INVALID_RUNTIME_CONFIG_TYPE',
|
INVALID_RUNTIME_CONFIG_TYPE = 'INVALID_RUNTIME_CONFIG_TYPE',
|
||||||
INVALID_SUBSCRIPTION_PARAMETERS = 'INVALID_SUBSCRIPTION_PARAMETERS',
|
INVALID_SUBSCRIPTION_PARAMETERS = 'INVALID_SUBSCRIPTION_PARAMETERS',
|
||||||
|
LICENSE_EXPIRED = 'LICENSE_EXPIRED',
|
||||||
LICENSE_NOT_FOUND = 'LICENSE_NOT_FOUND',
|
LICENSE_NOT_FOUND = 'LICENSE_NOT_FOUND',
|
||||||
LICENSE_REVEALED = 'LICENSE_REVEALED',
|
LICENSE_REVEALED = 'LICENSE_REVEALED',
|
||||||
LINK_EXPIRED = 'LINK_EXPIRED',
|
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_FOR_TEAM_SUBSCRIPTION = 'WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION',
|
||||||
WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION = 'WORKSPACE_ID_REQUIRED_TO_UPDATE_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_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',
|
WORKSPACE_PERMISSION_NOT_FOUND = 'WORKSPACE_PERMISSION_NOT_FOUND',
|
||||||
WRONG_SIGN_IN_CREDENTIALS = 'WRONG_SIGN_IN_CREDENTIALS',
|
WRONG_SIGN_IN_CREDENTIALS = 'WRONG_SIGN_IN_CREDENTIALS',
|
||||||
WRONG_SIGN_IN_METHOD = 'WRONG_SIGN_IN_METHOD',
|
WRONG_SIGN_IN_METHOD = 'WRONG_SIGN_IN_METHOD',
|
||||||
@@ -837,6 +837,11 @@ export interface InvalidHistoryTimestampDataType {
|
|||||||
timestamp: Scalars['String']['output'];
|
timestamp: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InvalidLicenseToActivateDataType {
|
||||||
|
__typename?: 'InvalidLicenseToActivateDataType';
|
||||||
|
reason: Scalars['String']['output'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface InvalidLicenseUpdateParamsDataType {
|
export interface InvalidLicenseUpdateParamsDataType {
|
||||||
__typename?: 'InvalidLicenseUpdateParamsDataType';
|
__typename?: 'InvalidLicenseUpdateParamsDataType';
|
||||||
reason: Scalars['String']['output'];
|
reason: Scalars['String']['output'];
|
||||||
@@ -1029,6 +1034,7 @@ export interface License {
|
|||||||
quantity: Scalars['Int']['output'];
|
quantity: Scalars['Int']['output'];
|
||||||
recurring: SubscriptionRecurring;
|
recurring: SubscriptionRecurring;
|
||||||
validatedAt: Scalars['DateTime']['output'];
|
validatedAt: Scalars['DateTime']['output'];
|
||||||
|
variant: Maybe<SubscriptionVariant>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LimitedUserType {
|
export interface LimitedUserType {
|
||||||
@@ -1166,6 +1172,7 @@ export interface Mutation {
|
|||||||
grantMember: Scalars['Boolean']['output'];
|
grantMember: Scalars['Boolean']['output'];
|
||||||
/** import users */
|
/** import users */
|
||||||
importUsers: Array<UserImportResultType>;
|
importUsers: Array<UserImportResultType>;
|
||||||
|
installLicense: License;
|
||||||
/** @deprecated use [inviteMembers] instead */
|
/** @deprecated use [inviteMembers] instead */
|
||||||
inviteBatch: Array<InviteResult>;
|
inviteBatch: Array<InviteResult>;
|
||||||
inviteMembers: Array<InviteResult>;
|
inviteMembers: Array<InviteResult>;
|
||||||
@@ -1391,6 +1398,11 @@ export interface MutationImportUsersArgs {
|
|||||||
input: ImportUsersInput;
|
input: ImportUsersInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MutationInstallLicenseArgs {
|
||||||
|
license: Scalars['Upload']['input'];
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface MutationInviteBatchArgs {
|
export interface MutationInviteBatchArgs {
|
||||||
emails: Array<Scalars['String']['input']>;
|
emails: Array<Scalars['String']['input']>;
|
||||||
sendInviteMail?: InputMaybe<Scalars['Boolean']['input']>;
|
sendInviteMail?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
@@ -2291,11 +2303,6 @@ export enum WorkspaceMemberStatus {
|
|||||||
UnderReview = 'UnderReview',
|
UnderReview = 'UnderReview',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceMembersExceedLimitToDowngradeDataType {
|
|
||||||
__typename?: 'WorkspaceMembersExceedLimitToDowngradeDataType';
|
|
||||||
limit: Scalars['Int']['output'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspacePermissionNotFoundDataType {
|
export interface WorkspacePermissionNotFoundDataType {
|
||||||
__typename?: 'WorkspacePermissionNotFoundDataType';
|
__typename?: 'WorkspacePermissionNotFoundDataType';
|
||||||
spaceId: Scalars['String']['output'];
|
spaceId: Scalars['String']['output'];
|
||||||
@@ -2474,20 +2481,6 @@ export interface TokenType {
|
|||||||
token: Scalars['String']['output'];
|
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 AdminServerConfigQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type AdminServerConfigQuery = {
|
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 DeleteAccountMutationVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type DeleteAccountMutation = {
|
export type DeleteAccountMutation = {
|
||||||
@@ -3693,25 +3677,6 @@ export type GetIsOwnerQueryVariables = Exact<{
|
|||||||
|
|
||||||
export type GetIsOwnerQuery = { __typename?: 'Query'; isOwner: boolean };
|
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<{
|
export type GetMemberCountByWorkspaceIdQueryVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -4059,6 +4024,81 @@ export type LeaveWorkspaceMutation = {
|
|||||||
leaveWorkspace: boolean;
|
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<{
|
export type ListNotificationsQueryVariables = Exact<{
|
||||||
pagination: PaginationInput;
|
pagination: PaginationInput;
|
||||||
}>;
|
}>;
|
||||||
@@ -4795,11 +4835,6 @@ export type Queries =
|
|||||||
variables: GetIsOwnerQueryVariables;
|
variables: GetIsOwnerQueryVariables;
|
||||||
response: GetIsOwnerQuery;
|
response: GetIsOwnerQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'getLicenseQuery';
|
|
||||||
variables: GetLicenseQueryVariables;
|
|
||||||
response: GetLicenseQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'getMemberCountByWorkspaceIdQuery';
|
name: 'getMemberCountByWorkspaceIdQuery';
|
||||||
variables: GetMemberCountByWorkspaceIdQueryVariables;
|
variables: GetMemberCountByWorkspaceIdQueryVariables;
|
||||||
@@ -4895,6 +4930,11 @@ export type Queries =
|
|||||||
variables: InvoicesQueryVariables;
|
variables: InvoicesQueryVariables;
|
||||||
response: InvoicesQuery;
|
response: InvoicesQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getLicenseQuery';
|
||||||
|
variables: GetLicenseQueryVariables;
|
||||||
|
response: GetLicenseQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'listNotificationsQuery';
|
name: 'listNotificationsQuery';
|
||||||
variables: ListNotificationsQueryVariables;
|
variables: ListNotificationsQueryVariables;
|
||||||
@@ -4952,11 +4992,6 @@ export type Queries =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Mutations =
|
export type Mutations =
|
||||||
| {
|
|
||||||
name: 'activateLicenseMutation';
|
|
||||||
variables: ActivateLicenseMutationVariables;
|
|
||||||
response: ActivateLicenseMutation;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'createChangePasswordUrlMutation';
|
name: 'createChangePasswordUrlMutation';
|
||||||
variables: CreateChangePasswordUrlMutationVariables;
|
variables: CreateChangePasswordUrlMutationVariables;
|
||||||
@@ -5162,11 +5197,6 @@ export type Mutations =
|
|||||||
variables: CreateWorkspaceMutationVariables;
|
variables: CreateWorkspaceMutationVariables;
|
||||||
response: CreateWorkspaceMutation;
|
response: CreateWorkspaceMutation;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'deactivateLicenseMutation';
|
|
||||||
variables: DeactivateLicenseMutationVariables;
|
|
||||||
response: DeactivateLicenseMutation;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'deleteAccountMutation';
|
name: 'deleteAccountMutation';
|
||||||
variables: DeleteAccountMutationVariables;
|
variables: DeleteAccountMutationVariables;
|
||||||
@@ -5192,6 +5222,21 @@ export type Mutations =
|
|||||||
variables: LeaveWorkspaceMutationVariables;
|
variables: LeaveWorkspaceMutationVariables;
|
||||||
response: LeaveWorkspaceMutation;
|
response: LeaveWorkspaceMutation;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'activateLicenseMutation';
|
||||||
|
variables: ActivateLicenseMutationVariables;
|
||||||
|
response: ActivateLicenseMutation;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: 'deactivateLicenseMutation';
|
||||||
|
variables: DeactivateLicenseMutationVariables;
|
||||||
|
response: DeactivateLicenseMutation;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: 'installLicenseMutation';
|
||||||
|
variables: InstallLicenseMutationVariables;
|
||||||
|
response: InstallLicenseMutation;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'mentionUserMutation';
|
name: 'mentionUserMutation';
|
||||||
variables: MentionUserMutationVariables;
|
variables: MentionUserMutationVariables;
|
||||||
|
|||||||
@@ -8441,9 +8441,11 @@ export function useAFFiNEI18N(): {
|
|||||||
*/
|
*/
|
||||||
["error.LICENSE_NOT_FOUND"](): string;
|
["error.LICENSE_NOT_FOUND"](): string;
|
||||||
/**
|
/**
|
||||||
* `Invalid license to activate.`
|
* `Invalid license to activate. {{reason}}`
|
||||||
*/
|
*/
|
||||||
["error.INVALID_LICENSE_TO_ACTIVATE"](): string;
|
["error.INVALID_LICENSE_TO_ACTIVATE"](options: {
|
||||||
|
readonly reason: string;
|
||||||
|
}): string;
|
||||||
/**
|
/**
|
||||||
* `Invalid license update params. {{reason}}`
|
* `Invalid license update params. {{reason}}`
|
||||||
*/
|
*/
|
||||||
@@ -8451,11 +8453,9 @@ export function useAFFiNEI18N(): {
|
|||||||
readonly reason: string;
|
readonly reason: string;
|
||||||
}): string;
|
}): string;
|
||||||
/**
|
/**
|
||||||
* `You cannot downgrade the workspace from team workspace because there are more than {{limit}} members that are currently active.`
|
* `License has expired.`
|
||||||
*/
|
*/
|
||||||
["error.WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE"](options: {
|
["error.LICENSE_EXPIRED"](): string;
|
||||||
readonly limit: string;
|
|
||||||
}): string;
|
|
||||||
/**
|
/**
|
||||||
* `Unsupported client with version [{{clientVersion}}], required version is [{{requiredVersion}}].`
|
* `Unsupported client with version [{{clientVersion}}], required version is [{{requiredVersion}}].`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2087,9 +2087,9 @@
|
|||||||
"error.LICENSE_REVEALED": "License key has been revealed. Please check your mail box of the one provided during checkout.",
|
"error.LICENSE_REVEALED": "License key has been revealed. Please check your mail box of the one provided during checkout.",
|
||||||
"error.WORKSPACE_LICENSE_ALREADY_EXISTS": "Workspace already has a license applied.",
|
"error.WORKSPACE_LICENSE_ALREADY_EXISTS": "Workspace already has a license applied.",
|
||||||
"error.LICENSE_NOT_FOUND": "License not found.",
|
"error.LICENSE_NOT_FOUND": "License not found.",
|
||||||
"error.INVALID_LICENSE_TO_ACTIVATE": "Invalid license to activate.",
|
"error.INVALID_LICENSE_TO_ACTIVATE": "Invalid license to activate. {{reason}}",
|
||||||
"error.INVALID_LICENSE_UPDATE_PARAMS": "Invalid license update params. {{reason}}",
|
"error.INVALID_LICENSE_UPDATE_PARAMS": "Invalid license update params. {{reason}}",
|
||||||
"error.WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE": "You cannot downgrade the workspace from team workspace because there are more than {{limit}} members that are currently active.",
|
"error.LICENSE_EXPIRED": "License has expired.",
|
||||||
"error.UNSUPPORTED_CLIENT_VERSION": "Unsupported client with version [{{clientVersion}}], required version is [{{requiredVersion}}].",
|
"error.UNSUPPORTED_CLIENT_VERSION": "Unsupported client with version [{{clientVersion}}], required version is [{{requiredVersion}}].",
|
||||||
"error.NOTIFICATION_NOT_FOUND": "Notification not found.",
|
"error.NOTIFICATION_NOT_FOUND": "Notification not found.",
|
||||||
"error.MENTION_USER_DOC_ACCESS_DENIED": "Mentioned user can not access doc {{docId}}.",
|
"error.MENTION_USER_DOC_ACCESS_DENIED": "Mentioned user can not access doc {{docId}}.",
|
||||||
|
|||||||
@@ -120,9 +120,19 @@ const defaultDevServerConfig: DevServerConfiguration = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
headers: {
|
headers: (req): Record<string, string | string[]> => {
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
if (
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
[/^\/api/, /^\/socket\.io/, /^\/graphql/].some(path =>
|
||||||
|
path.test(req.path)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
proxy: [
|
proxy: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user