feat(core): optimize team workspace member management (#9737)

close AF-2106 AF-2077 AF-2089

feat(core): handle need more seat status

feat(core): prevent invite members when team plan is canceled
This commit is contained in:
JimmFly
2025-02-07 10:08:00 +00:00
parent 85d916f1eb
commit d5a626d9c3
18 changed files with 562 additions and 154 deletions
@@ -12,6 +12,7 @@ export { AccountLoggedIn } from './events/account-logged-in';
export { AccountLoggedOut } from './events/account-logged-out';
export { ServerInitialized } from './events/server-initialized';
export { ValidatorProvider } from './provider/validator';
export { AcceptInviteService } from './services/accept-invite';
export { AuthService } from './services/auth';
export { CaptchaService } from './services/captcha';
export { DefaultServerService } from './services/default-server';
@@ -52,6 +53,7 @@ import { WorkspaceInvoices } from './entities/workspace-invoices';
import { WorkspaceSubscription } from './entities/workspace-subscription';
import { ValidatorProvider } from './provider/validator';
import { ServerScope } from './scopes/server';
import { AcceptInviteService } from './services/accept-invite';
import { AuthService } from './services/auth';
import { CaptchaService } from './services/captcha';
import { CloudDocMetaService } from './services/cloud-doc-meta';
@@ -71,8 +73,10 @@ import { UserQuotaService } from './services/user-quota';
import { WorkspaceInvoicesService } from './services/workspace-invoices';
import { WorkspaceServerService } from './services/workspace-server';
import { WorkspaceSubscriptionService } from './services/workspace-subscription';
import { AcceptInviteStore } from './stores/accept-invite';
import { AuthStore } from './stores/auth';
import { CloudDocMetaStore } from './stores/cloud-doc-meta';
import { InviteInfoStore } from './stores/invite-info';
import { InvoicesStore } from './stores/invoices';
import { SelfhostGenerateLicenseStore } from './stores/selfhost-generate-license';
import { SelfhostLicenseStore } from './stores/selfhost-license';
@@ -136,7 +140,10 @@ export function configureCloudModule(framework: Framework) {
.store(InvoicesStore, [GraphQLService])
.entity(Invoices, [InvoicesStore])
.service(SelfhostGenerateLicenseService, [SelfhostGenerateLicenseStore])
.store(SelfhostGenerateLicenseStore, [GraphQLService]);
.store(SelfhostGenerateLicenseStore, [GraphQLService])
.store(InviteInfoStore, [GraphQLService])
.service(AcceptInviteService, [AcceptInviteStore, InviteInfoStore])
.store(AcceptInviteStore, [GraphQLService]);
framework
.scope(WorkspaceScope)
@@ -0,0 +1,78 @@
import type { GetInviteInfoQuery } from '@affine/graphql';
import {
backoffRetry,
catchErrorInto,
effect,
fromPromise,
LiveData,
onComplete,
onStart,
Service,
} from '@toeverything/infra';
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../error';
import type { AcceptInviteStore } from '../stores/accept-invite';
import type { InviteInfoStore } from '../stores/invite-info';
export type InviteInfo = GetInviteInfoQuery['getInviteInfo'];
export class AcceptInviteService extends Service {
constructor(
private readonly store: AcceptInviteStore,
private readonly inviteInfoStore: InviteInfoStore
) {
super();
}
inviteInfo$ = new LiveData<InviteInfo | undefined>(undefined);
accepted$ = new LiveData<boolean>(false);
loading$ = new LiveData(false);
error$ = new LiveData<any>(null);
readonly revalidate = effect(
exhaustMap(({ inviteId }: { inviteId: string }) => {
if (!inviteId) {
return EMPTY;
}
return fromPromise(async () => {
return await this.inviteInfoStore.getInviteInfo(inviteId);
}).pipe(
mergeMap(res => {
this.inviteInfo$.setValue(res);
return fromPromise(async () => {
return await this.store.acceptInvite(
res.workspace.id,
inviteId,
true
);
});
}),
mergeMap(res => {
this.accepted$.next(res);
return EMPTY;
}),
backoffRetry({
when: isNetworkError,
count: Infinity,
}),
backoffRetry({
when: isBackendError,
count: 3,
}),
catchErrorInto(this.error$),
onStart(() => {
this.loading$.setValue(true);
this.inviteInfo$.setValue(undefined);
this.accepted$.setValue(false);
}),
onComplete(() => {
this.loading$.setValue(false);
})
);
})
);
override dispose(): void {
this.revalidate.unsubscribe();
}
}
@@ -0,0 +1,30 @@
import { acceptInviteByInviteIdMutation } from '@affine/graphql';
import { Store } from '@toeverything/infra';
import type { GraphQLService } from '../services/graphql';
export class AcceptInviteStore extends Store {
constructor(private readonly gqlService: GraphQLService) {
super();
}
async acceptInvite(
workspaceId: string,
inviteId: string,
sendAcceptMail?: boolean,
signal?: AbortSignal
) {
const data = await this.gqlService.gql({
query: acceptInviteByInviteIdMutation,
variables: {
workspaceId,
inviteId,
sendAcceptMail,
},
context: { signal },
});
return data.acceptInviteById;
}
}
@@ -0,0 +1,25 @@
import { getInviteInfoQuery } from '@affine/graphql';
import { Store } from '@toeverything/infra';
import type { GraphQLService } from '../services/graphql';
export class InviteInfoStore extends Store {
constructor(private readonly gqlService: GraphQLService) {
super();
}
async getInviteInfo(inviteId?: string, signal?: AbortSignal) {
if (!inviteId) {
throw new Error('No inviteId');
}
const data = await this.gqlService.gql({
query: getInviteInfoQuery,
variables: {
inviteId,
},
context: { signal },
});
return data.getInviteInfo;
}
}