mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 08:50:50 +08:00
fix(server): should direct allocate seat if workspace is not team (#12469)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added the ability for workspace owners to approve members under review, with different approval processes for team and non-team workspaces. - **Bug Fixes** - Improved accuracy of workspace seat quota calculations for member management. - **Tests** - Enhanced test coverage and consistency for workspace member actions, including approval and revocation scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
acceptInviteByInviteIdMutation,
|
acceptInviteByInviteIdMutation,
|
||||||
|
approveWorkspaceTeamMemberMutation,
|
||||||
createInviteLinkMutation,
|
createInviteLinkMutation,
|
||||||
getInviteInfoQuery,
|
getInviteInfoQuery,
|
||||||
getMembersByWorkspaceIdQuery,
|
getMembersByWorkspaceIdQuery,
|
||||||
@@ -15,16 +16,11 @@ import { Models } from '../../../models';
|
|||||||
import { Mockers } from '../../mocks';
|
import { Mockers } from '../../mocks';
|
||||||
import { app, e2e } from '../test';
|
import { app, e2e } from '../test';
|
||||||
|
|
||||||
async function createTeamWorkspace(quantity = 10) {
|
async function createWorkspace() {
|
||||||
const owner = await app.create(Mockers.User);
|
const owner = await app.create(Mockers.User);
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
const workspace = await app.create(Mockers.Workspace, {
|
||||||
owner: { id: owner.id },
|
owner: { id: owner.id },
|
||||||
});
|
});
|
||||||
await app.create(Mockers.TeamWorkspace, {
|
|
||||||
id: workspace.id,
|
|
||||||
quantity,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
owner,
|
owner,
|
||||||
@@ -33,13 +29,10 @@ async function createTeamWorkspace(quantity = 10) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e2e('should invite a user', async t => {
|
e2e('should invite a user', async t => {
|
||||||
const u2 = await app.signup();
|
const { owner, workspace } = await createWorkspace();
|
||||||
const owner = await app.signup();
|
const u2 = await app.create(Mockers.User);
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
|
await app.login(owner);
|
||||||
const result = await app.gql({
|
const result = await app.gql({
|
||||||
query: inviteByEmailsMutation,
|
query: inviteByEmailsMutation,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -47,6 +40,7 @@ e2e('should invite a user', async t => {
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(result, 'failed to invite user');
|
t.truthy(result, 'failed to invite user');
|
||||||
// add invitation notification job
|
// add invitation notification job
|
||||||
const invitationNotification = await app.queue.waitFor(
|
const invitationNotification = await app.queue.waitFor(
|
||||||
@@ -68,7 +62,7 @@ e2e('should invite a user', async t => {
|
|||||||
t.is(getInviteInfo.status, WorkspaceMemberStatus.Pending);
|
t.is(getInviteInfo.status, WorkspaceMemberStatus.Pending);
|
||||||
|
|
||||||
// u2 accept invite
|
// u2 accept invite
|
||||||
await app.switchUser(u2);
|
await app.login(u2);
|
||||||
await app.gql({
|
await app.gql({
|
||||||
query: acceptInviteByInviteIdMutation,
|
query: acceptInviteByInviteIdMutation,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -88,18 +82,14 @@ e2e('should invite a user', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
e2e('should leave a workspace', async t => {
|
e2e('should leave a workspace', async t => {
|
||||||
const u2 = await app.signup();
|
const { owner, workspace } = await createWorkspace();
|
||||||
const owner = await app.signup();
|
const u2 = await app.create(Mockers.User);
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
await app.create(Mockers.WorkspaceUser, {
|
await app.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: u2.id,
|
userId: u2.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.switchUser(u2.id);
|
await app.login(u2);
|
||||||
const { leaveWorkspace } = await app.gql({
|
const { leaveWorkspace } = await app.gql({
|
||||||
query: leaveWorkspaceMutation,
|
query: leaveWorkspaceMutation,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -137,19 +127,42 @@ e2e('should revoke a user', async t => {
|
|||||||
t.true(revokeMember, 'failed to revoke user');
|
t.true(revokeMember, 'failed to revoke user');
|
||||||
});
|
});
|
||||||
|
|
||||||
e2e('should revoke a user on under review', async t => {
|
e2e('should approve a user on under review', async t => {
|
||||||
const user = await app.signup();
|
const { owner, workspace } = await createWorkspace();
|
||||||
const owner = await app.signup();
|
const user = await app.create(Mockers.User);
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
await app.create(Mockers.WorkspaceUser, {
|
await app.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
status: WorkspaceMemberStatus.UnderReview,
|
status: WorkspaceMemberStatus.UnderReview,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await app.login(owner);
|
||||||
|
const { approveMember } = await app.gql({
|
||||||
|
query: approveWorkspaceTeamMemberMutation,
|
||||||
|
variables: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.true(approveMember, 'failed to approve member');
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
(await app.get(Models).workspaceUser.get(workspace.id, user.id))?.status,
|
||||||
|
WorkspaceMemberStatus.Accepted
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should revoke a user on under review', async t => {
|
||||||
|
const { owner, workspace } = await createWorkspace();
|
||||||
|
const user = await app.create(Mockers.User);
|
||||||
|
await app.create(Mockers.WorkspaceUser, {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
userId: user.id,
|
||||||
|
status: WorkspaceMemberStatus.UnderReview,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.login(owner);
|
||||||
const { revokeMember } = await app.gql({
|
const { revokeMember } = await app.gql({
|
||||||
query: revokeMemberPermissionMutation,
|
query: revokeMemberPermissionMutation,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -174,13 +187,10 @@ e2e('should revoke a user on under review', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
e2e('should create user if not exist', async t => {
|
e2e('should create user if not exist', async t => {
|
||||||
const owner = await app.signup();
|
const { owner, workspace } = await createWorkspace();
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = faker.internet.email();
|
const email = faker.internet.email();
|
||||||
|
await app.login(owner);
|
||||||
await app.gql({
|
await app.gql({
|
||||||
query: inviteByEmailsMutation,
|
query: inviteByEmailsMutation,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -194,13 +204,10 @@ e2e('should create user if not exist', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
e2e('should support pagination for member', async t => {
|
e2e('should support pagination for member', async t => {
|
||||||
const u1 = await app.signup();
|
const { owner, workspace } = await createWorkspace();
|
||||||
const u2 = await app.signup();
|
const u1 = await app.create(Mockers.User);
|
||||||
const owner = await app.signup();
|
const u2 = await app.create(Mockers.User);
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
await app.create(Mockers.WorkspaceUser, {
|
await app.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: u1.id,
|
userId: u1.id,
|
||||||
@@ -210,6 +217,7 @@ e2e('should support pagination for member', async t => {
|
|||||||
userId: u2.id,
|
userId: u2.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await app.login(owner);
|
||||||
let result = await app.gql({
|
let result = await app.gql({
|
||||||
query: getMembersByWorkspaceIdQuery,
|
query: getMembersByWorkspaceIdQuery,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -245,14 +253,11 @@ e2e('should support pagination for member', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
e2e('should limit member count correctly', async t => {
|
e2e('should limit member count correctly', async t => {
|
||||||
const owner = await app.signup();
|
const { owner, workspace } = await createWorkspace();
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from({ length: 10 }).map(async () => {
|
Array.from({ length: 10 }).map(async () => {
|
||||||
const user = await app.signup();
|
const user = await app.create(Mockers.User);
|
||||||
await app.create(Mockers.WorkspaceUser, {
|
await app.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -260,7 +265,7 @@ e2e('should limit member count correctly', async t => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.switchUser(owner);
|
await app.login(owner);
|
||||||
const result = await app.gql({
|
const result = await app.gql({
|
||||||
query: getMembersByWorkspaceIdQuery,
|
query: getMembersByWorkspaceIdQuery,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -274,11 +279,7 @@ e2e('should limit member count correctly', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
e2e('should get invite link info with status', async t => {
|
e2e('should get invite link info with status', async t => {
|
||||||
const owner = await app.signup();
|
const { owner, workspace } = await createWorkspace();
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.login(owner);
|
await app.login(owner);
|
||||||
const { createInviteLink } = await app.gql({
|
const { createInviteLink } = await app.gql({
|
||||||
@@ -335,13 +336,9 @@ e2e('should get invite link info with status', async t => {
|
|||||||
e2e(
|
e2e(
|
||||||
'should accept invitation by link directly if status is pending',
|
'should accept invitation by link directly if status is pending',
|
||||||
async t => {
|
async t => {
|
||||||
const owner = await app.create(Mockers.User);
|
const { owner, workspace } = await createWorkspace();
|
||||||
const member = await app.create(Mockers.User);
|
const member = await app.create(Mockers.User);
|
||||||
|
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.login(owner);
|
await app.login(owner);
|
||||||
// create a pending invitation
|
// create a pending invitation
|
||||||
const invite = await app.gql({
|
const invite = await app.gql({
|
||||||
@@ -387,7 +384,7 @@ e2e(
|
|||||||
e2e(
|
e2e(
|
||||||
'should invite by link and send review request notification below quota limit',
|
'should invite by link and send review request notification below quota limit',
|
||||||
async t => {
|
async t => {
|
||||||
const { owner, workspace } = await createTeamWorkspace();
|
const { owner, workspace } = await createWorkspace();
|
||||||
|
|
||||||
await app.login(owner);
|
await app.login(owner);
|
||||||
const { createInviteLink } = await app.gql({
|
const { createInviteLink } = await app.gql({
|
||||||
@@ -422,7 +419,11 @@ e2e(
|
|||||||
e2e(
|
e2e(
|
||||||
'should invite by link and send review request notification over quota limit',
|
'should invite by link and send review request notification over quota limit',
|
||||||
async t => {
|
async t => {
|
||||||
const { owner, workspace } = await createTeamWorkspace(1);
|
const { owner, workspace } = await createWorkspace();
|
||||||
|
await app.create(Mockers.TeamWorkspace, {
|
||||||
|
id: workspace.id,
|
||||||
|
quantity: 3,
|
||||||
|
});
|
||||||
|
|
||||||
await app.login(owner);
|
await app.login(owner);
|
||||||
const { createInviteLink } = await app.gql({
|
const { createInviteLink } = await app.gql({
|
||||||
@@ -457,10 +458,7 @@ e2e(
|
|||||||
e2e(
|
e2e(
|
||||||
'should search members by name and email support case insensitive',
|
'should search members by name and email support case insensitive',
|
||||||
async t => {
|
async t => {
|
||||||
const owner = await app.create(Mockers.User);
|
const { owner, workspace } = await createWorkspace();
|
||||||
const workspace = await app.create(Mockers.Workspace, {
|
|
||||||
owner: { id: owner.id },
|
|
||||||
});
|
|
||||||
const user1 = await app.create(Mockers.User, {
|
const user1 = await app.create(Mockers.User, {
|
||||||
name: faker.internet.displayName({ firstName: 'Lucy' }),
|
name: faker.internet.displayName({ firstName: 'Lucy' }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -173,7 +173,8 @@ export class QuotaService {
|
|||||||
|
|
||||||
async getWorkspaceSeatQuota(workspaceId: string) {
|
async getWorkspaceSeatQuota(workspaceId: string) {
|
||||||
const quota = await this.getWorkspaceQuota(workspaceId);
|
const quota = await this.getWorkspaceQuota(workspaceId);
|
||||||
const memberCount = await this.models.workspaceUser.count(workspaceId);
|
const memberCount =
|
||||||
|
await this.models.workspaceUser.chargedCount(workspaceId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
memberCount,
|
memberCount,
|
||||||
|
|||||||
@@ -343,22 +343,37 @@ export class WorkspaceMemberResolver {
|
|||||||
.workspace(workspaceId)
|
.workspace(workspaceId)
|
||||||
.assert('Workspace.Users.Manage');
|
.assert('Workspace.Users.Manage');
|
||||||
|
|
||||||
|
const isTeam = await this.models.workspace.isTeamWorkspace(workspaceId);
|
||||||
const role = await this.models.workspaceUser.get(workspaceId, userId);
|
const role = await this.models.workspaceUser.get(workspaceId, userId);
|
||||||
|
|
||||||
if (role) {
|
if (role) {
|
||||||
if (role.status === WorkspaceMemberStatus.UnderReview) {
|
if (role.status === WorkspaceMemberStatus.UnderReview) {
|
||||||
await this.models.workspaceUser.setStatus(
|
if (isTeam) {
|
||||||
workspaceId,
|
await this.models.workspaceUser.setStatus(
|
||||||
userId,
|
workspaceId,
|
||||||
WorkspaceMemberStatus.AllocatingSeat,
|
userId,
|
||||||
{
|
WorkspaceMemberStatus.AllocatingSeat,
|
||||||
inviterId: me.id,
|
{
|
||||||
|
inviterId: me.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const quota = await this.quota.getWorkspaceSeatQuota(workspaceId);
|
||||||
|
if (quota.memberCount >= quota.memberLimit) {
|
||||||
|
throw new NoMoreSeat({ spaceId: workspaceId });
|
||||||
|
} else {
|
||||||
|
await this.models.workspaceUser.setStatus(
|
||||||
|
workspaceId,
|
||||||
|
userId,
|
||||||
|
WorkspaceMemberStatus.Accepted
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
this.event.emit('workspace.members.updated', {
|
this.event.emit('workspace.members.updated', {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.workspaceService.sendReviewApprovedNotification(
|
await this.workspaceService.sendReviewApprovedNotification(
|
||||||
role.id,
|
role.id,
|
||||||
me.id
|
me.id
|
||||||
|
|||||||
Reference in New Issue
Block a user