feat(core): add self host team plan (#9569)

This commit is contained in:
JimmFly
2025-02-07 03:35:24 +00:00
parent 5710e8c639
commit e68bdbde3e
37 changed files with 1702 additions and 72 deletions

View File

@@ -19,6 +19,8 @@ export { EventSourceService } from './services/eventsource';
export { FetchService } from './services/fetch';
export { GraphQLService } from './services/graphql';
export { InvoicesService } from './services/invoices';
export { SelfhostGenerateLicenseService } from './services/selfhost-generate-license';
export { SelfhostLicenseService } from './services/selfhost-license';
export { ServerService } from './services/server';
export { ServersService } from './services/servers';
export { SubscriptionService } from './services/subscription';
@@ -58,6 +60,8 @@ import { EventSourceService } from './services/eventsource';
import { FetchService } from './services/fetch';
import { GraphQLService } from './services/graphql';
import { InvoicesService } from './services/invoices';
import { SelfhostGenerateLicenseService } from './services/selfhost-generate-license';
import { SelfhostLicenseService } from './services/selfhost-license';
import { ServerService } from './services/server';
import { ServersService } from './services/servers';
import { SubscriptionService } from './services/subscription';
@@ -70,6 +74,8 @@ import { WorkspaceSubscriptionService } from './services/workspace-subscription'
import { AuthStore } from './stores/auth';
import { CloudDocMetaStore } from './stores/cloud-doc-meta';
import { InvoicesStore } from './stores/invoices';
import { SelfhostGenerateLicenseStore } from './stores/selfhost-generate-license';
import { SelfhostLicenseStore } from './stores/selfhost-license';
import { ServerConfigStore } from './stores/server-config';
import { ServerListStore } from './stores/server-list';
import { SubscriptionStore } from './stores/subscription';
@@ -128,7 +134,9 @@ export function configureCloudModule(framework: Framework) {
.store(UserFeatureStore, [GraphQLService])
.service(InvoicesService)
.store(InvoicesStore, [GraphQLService])
.entity(Invoices, [InvoicesStore]);
.entity(Invoices, [InvoicesStore])
.service(SelfhostGenerateLicenseService, [SelfhostGenerateLicenseStore])
.store(SelfhostGenerateLicenseStore, [GraphQLService]);
framework
.scope(WorkspaceScope)
@@ -142,5 +150,7 @@ export function configureCloudModule(framework: Framework) {
.service(WorkspaceSubscriptionService, [WorkspaceServerService])
.entity(WorkspaceSubscription, [WorkspaceService, WorkspaceServerService])
.service(WorkspaceInvoicesService)
.entity(WorkspaceInvoices, [WorkspaceService, WorkspaceServerService]);
.entity(WorkspaceInvoices, [WorkspaceService, WorkspaceServerService])
.service(SelfhostLicenseService, [SelfhostLicenseStore, WorkspaceService])
.store(SelfhostLicenseStore, [WorkspaceServerService]);
}

View File

@@ -0,0 +1,54 @@
import { UserFriendlyError } from '@affine/graphql';
import {
backoffRetry,
effect,
fromPromise,
LiveData,
onComplete,
onStart,
Service,
} from '@toeverything/infra';
import { catchError, EMPTY, exhaustMap, mergeMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../error';
import type { SelfhostGenerateLicenseStore } from '../stores/selfhost-generate-license';
export class SelfhostGenerateLicenseService extends Service {
constructor(private readonly store: SelfhostGenerateLicenseStore) {
super();
}
licenseKey$ = new LiveData<string | null>(null);
isLoading$ = new LiveData(false);
error$ = new LiveData<UserFriendlyError | null>(null);
generateLicenseKey = effect(
exhaustMap((sessionId: string) => {
return fromPromise(async () => {
return await this.store.generateKey(sessionId);
}).pipe(
backoffRetry({
when: isNetworkError,
count: Infinity,
}),
backoffRetry({
when: isBackendError,
}),
mergeMap(key => {
this.licenseKey$.next(key);
return EMPTY;
}),
catchError(err => {
this.error$.next(UserFriendlyError.fromAnyError(err));
console.error(err);
return EMPTY;
}),
onStart(() => {
this.isLoading$.next(true);
}),
onComplete(() => {
this.isLoading$.next(false);
})
);
})
);
}

View File

@@ -0,0 +1,72 @@
import type { License } from '@affine/graphql';
import {
backoffRetry,
catchErrorInto,
effect,
exhaustMapWithTrailing,
fromPromise,
LiveData,
onComplete,
onStart,
Service,
} from '@toeverything/infra';
import { EMPTY, mergeMap } from 'rxjs';
import type { WorkspaceService } from '../../workspace';
import { isBackendError, isNetworkError } from '../error';
import type { SelfhostLicenseStore } from '../stores/selfhost-license';
export class SelfhostLicenseService extends Service {
constructor(
private readonly store: SelfhostLicenseStore,
private readonly workspaceService: WorkspaceService
) {
super();
}
license$ = new LiveData<License | null>(null);
isRevalidating$ = new LiveData(false);
error$ = new LiveData<any | null>(null);
revalidate = effect(
exhaustMapWithTrailing(() => {
return fromPromise(async signal => {
const currentWorkspaceId = this.workspaceService.workspace.id;
if (!currentWorkspaceId) {
return undefined; // no subscription if no user
}
return await this.store.getLicense(currentWorkspaceId, signal);
}).pipe(
backoffRetry({
when: isNetworkError,
count: Infinity,
}),
backoffRetry({
when: isBackendError,
}),
mergeMap(data => {
if (data) {
this.license$.next(data);
}
return EMPTY;
}),
catchErrorInto(this.error$),
onStart(() => this.isRevalidating$.next(true)),
onComplete(() => this.isRevalidating$.next(false))
);
})
);
async activateLicense(workspaceId: string, licenseKey: string) {
return await this.store.activate(workspaceId, licenseKey);
}
async deactivateLicense(workspaceId: string) {
return await this.store.deactivate(workspaceId);
}
clear() {
this.license$.next(null);
this.error$.next(null);
}
}

View File

@@ -0,0 +1,24 @@
import { generateLicenseKeyMutation } from '@affine/graphql';
import { Store } from '@toeverything/infra';
import type { GraphQLService } from '../services/graphql';
export class SelfhostGenerateLicenseStore extends Store {
constructor(private readonly gqlService: GraphQLService) {
super();
}
async generateKey(sessionId: string, signal?: AbortSignal): Promise<string> {
const data = await this.gqlService.gql({
query: generateLicenseKeyMutation,
variables: {
sessionId: sessionId,
},
context: {
signal,
},
});
return data.generateLicenseKey;
}
}

View File

@@ -0,0 +1,66 @@
import {
activateLicenseMutation,
deactivateLicenseMutation,
getLicenseQuery,
} from '@affine/graphql';
import { Store } from '@toeverything/infra';
import type { WorkspaceServerService } from '../services/workspace-server';
export class SelfhostLicenseStore extends Store {
constructor(private readonly workspaceServerService: WorkspaceServerService) {
super();
}
async getLicense(workspaceId: string, signal?: AbortSignal) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: getLicenseQuery,
variables: {
workspaceId: workspaceId,
},
context: {
signal,
},
});
return data.workspace.license;
}
async activate(workspaceId: string, license: string, signal?: AbortSignal) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: activateLicenseMutation,
variables: {
workspaceId: workspaceId,
license: license,
},
context: {
signal,
},
});
return data.activateLicense;
}
async deactivate(workspaceId: string, signal?: AbortSignal) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: deactivateLicenseMutation,
variables: {
workspaceId: workspaceId,
},
context: {
signal,
},
});
return data.deactivateLicense;
}
}

View File

@@ -4,12 +4,13 @@ import {
catchErrorInto,
effect,
Entity,
exhaustMapWithTrailing,
fromPromise,
LiveData,
onComplete,
onStart,
} from '@toeverything/infra';
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
import { EMPTY, mergeMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../../cloud';
import type { WorkspaceService } from '../../workspace';
@@ -32,7 +33,7 @@ export class WorkspacePermission extends Entity {
}
revalidate = effect(
exhaustMap(() => {
exhaustMapWithTrailing(() => {
return fromPromise(async signal => {
if (this.workspaceService.workspace.flavour !== 'local') {
const info = await this.store.fetchWorkspaceInfo(