Skip to content

Commit

Permalink
chore(server): cleanup team impl (#9171)
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo committed Dec 16, 2024
1 parent de2dab3 commit 83618e3
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 150 deletions.
12 changes: 5 additions & 7 deletions packages/backend/server/src/base/event/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import type { Snapshot, User, Workspace } from '@prisma/client';
import { Flatten, Payload } from './types';

export interface WorkspaceEvents {
team: {
seatAvailable: Payload<{ inviteId: string; email: string }[]>;
reviewRequest: Payload<{ inviteIds: string[] }>;
declineRequest: Payload<{
workspaceId: Workspace['id'];
inviteeId: User['id'];
}>;
members: {
reviewRequested: Payload<{ inviteId: string }>;
requestDeclined: Payload<{ inviteId: string }>;
requestApproved: Payload<{ inviteId: string }>;
updated: Payload<{ workspaceId: Workspace['id']; count: number }>;
};
deleted: Payload<Workspace['id']>;
blob: {
Expand Down
170 changes: 75 additions & 95 deletions packages/backend/server/src/core/permission/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,11 @@ import { groupBy } from 'lodash-es';
import {
DocAccessDenied,
EventEmitter,
PrismaTransaction,
SpaceAccessDenied,
SpaceOwnerNotFound,
} from '../../base';
import { FeatureKind } from '../features/types';
import { QuotaType } from '../quota/types';
import { Permission, PublicPageMode } from './types';

const NeedUpdateStatus = new Set<WorkspaceMemberStatus>([
WorkspaceMemberStatus.NeedMoreSeat,
WorkspaceMemberStatus.NeedMoreSeatAndReview,
]);

@Injectable()
export class PermissionService {
constructor(
Expand Down Expand Up @@ -377,21 +369,6 @@ export class PermissionService {
.then(p => p.id);
}

private async isTeamWorkspace(tx: PrismaTransaction, workspaceId: string) {
return await tx.workspaceFeature
.count({
where: {
workspaceId,
activated: true,
feature: {
feature: QuotaType.TeamPlanV1,
type: FeatureKind.Feature,
},
},
})
.then(count => count > 0);
}

async acceptWorkspaceInvitation(
invitationId: string,
workspaceId: string,
Expand All @@ -410,91 +387,94 @@ export class PermissionService {
}

async refreshSeatStatus(workspaceId: string, memberLimit: number) {
const [pending, underReview] = await this.prisma.$transaction(async tx => {
const usedCount = await this.prisma.workspaceUserPermission.count({
where: { workspaceId, status: WorkspaceMemberStatus.Accepted },
});

const availableCount = memberLimit - usedCount;

if (availableCount <= 0) {
return;
}

await this.prisma.$transaction(async tx => {
const members = await tx.workspaceUserPermission.findMany({
where: { workspaceId },
select: { userId: true, status: true, updatedAt: true },
select: { id: true, status: true },
where: {
workspaceId,
status: {
in: [
WorkspaceMemberStatus.NeedMoreSeat,
WorkspaceMemberStatus.NeedMoreSeatAndReview,
],
},
},
orderBy: { createdAt: 'asc' },
});
const memberCount = members.filter(
m => m.status === WorkspaceMemberStatus.Accepted
).length;
const needChange = members
.filter(m => NeedUpdateStatus.has(m.status))
.toSorted((a, b) => Number(a.updatedAt) - Number(b.updatedAt))
.slice(0, memberLimit - memberCount);

const needChange = members.slice(0, availableCount);
const { NeedMoreSeat, NeedMoreSeatAndReview } = groupBy(
needChange,
m => m.status
);
const inviteByMail = NeedMoreSeat?.map(m => m.userId) ?? [];
await tx.workspaceUserPermission.updateMany({
where: { workspaceId, userId: { in: inviteByMail } },
data: { status: WorkspaceMemberStatus.Pending },
});
const inviteByLink = NeedMoreSeatAndReview?.map(m => m.userId) ?? [];
await tx.workspaceUserPermission.updateMany({
where: { workspaceId, userId: { in: inviteByLink } },
data: { status: WorkspaceMemberStatus.UnderReview },
});

const pending = await tx.workspaceUserPermission
.findMany({
where: {
workspaceId,
userId: { in: inviteByLink },
status: WorkspaceMemberStatus.Pending,
},
select: { id: true, user: { select: { email: true } } },
})
.then(r => r.map(m => ({ inviteId: m.id, email: m.user.email })));
const underReview = await tx.workspaceUserPermission
.findMany({
where: {
workspaceId,
userId: { in: inviteByLink },
status: WorkspaceMemberStatus.UnderReview,
},
select: { id: true },
})
.then(r => ({ inviteIds: r.map(m => m.id) }));
return [pending, underReview] as const;
const toPendings = NeedMoreSeat ?? [];
if (toPendings.length > 0) {
await tx.workspaceUserPermission.updateMany({
where: { id: { in: toPendings.map(m => m.id) } },
data: { status: WorkspaceMemberStatus.Pending },
});
}

const toUnderReviewUserIds = NeedMoreSeatAndReview ?? [];
if (toUnderReviewUserIds.length > 0) {
await tx.workspaceUserPermission.updateMany({
where: { id: { in: toUnderReviewUserIds.map(m => m.id) } },
data: { status: WorkspaceMemberStatus.UnderReview },
});
}

return [toPendings, toUnderReviewUserIds] as const;
});
this.event.emit('workspace.team.seatAvailable', pending);
this.event.emit('workspace.team.reviewRequest', underReview);
}

async revokeWorkspace(workspaceId: string, user: string) {
return await this.prisma.$transaction(async tx => {
const result = await tx.workspaceUserPermission.deleteMany({
where: {
workspaceId,
userId: user,
// We shouldn't revoke owner permission
// should auto deleted by workspace/user delete cascading
type: { not: Permission.Owner },
},
});
const permission = await this.prisma.workspaceUserPermission.findUnique({
where: { workspaceId_userId: { workspaceId, userId: user } },
});

const success = result.count > 0;

if (success) {
const isTeam = await this.isTeamWorkspace(tx, workspaceId);
if (isTeam) {
const count = await tx.workspaceUserPermission.count({
where: { workspaceId },
});
this.event.emit('workspace.members.updated', {
workspaceId,
count,
});
this.event.emit('workspace.team.declineRequest', {
workspaceId,
inviteeId: user,
});
}
}
return success;
// We shouldn't revoke owner permission
// should auto deleted by workspace/user delete cascading
if (!permission || permission.type === Permission.Owner) {
return false;
}

await this.prisma.workspaceUserPermission.deleteMany({
where: {
workspaceId,
userId: user,
},
});

const count = await this.prisma.workspaceUserPermission.count({
where: { workspaceId },
});

this.event.emit('workspace.members.updated', {
workspaceId,
count,
});

if (
permission.status === 'UnderReview' ||
permission.status === 'NeedMoreSeatAndReview'
) {
this.event.emit('workspace.members.requestDeclined', {
inviteId: permission.id,
});
}

return true;
}
/// End regin: workspace permission

Expand Down
69 changes: 42 additions & 27 deletions packages/backend/server/src/core/workspaces/resolvers/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,6 @@ export class WorkspaceService {
};
}

async sendInviteMail(inviteId: string, email: string) {
const { workspaceId } = await this.getInviteInfo(inviteId);
const workspace = await this.getWorkspaceInfo(workspaceId);
const owner = await this.permission.getWorkspaceOwner(workspaceId);

await this.mailer.sendInviteEmail(email, inviteId, {
workspace,
user: {
avatar: owner.avatarUrl || '',
name: owner.name || '',
},
});
}

async sendAcceptedEmail(inviteId: string) {
const { workspaceId, inviterUserId, inviteeUserId } =
await this.getInviteInfo(inviteId);
Expand All @@ -117,7 +103,7 @@ export class WorkspaceService {
return true;
}

async sendReviewRequestMail(inviteId: string) {
async sendReviewRequestedMail(inviteId: string) {
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId);
if (!inviteeUserId) {
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`);
Expand Down Expand Up @@ -145,24 +131,50 @@ export class WorkspaceService {
}
}

async sendInviteMail(inviteId: string) {
const target = await this.getInviteeEmailTarget(inviteId);

if (!target) {
return;
}

const owner = await this.permission.getWorkspaceOwner(target.workspace.id);

await this.mailer.sendInviteEmail(target.email, inviteId, {
workspace: target.workspace,
user: {
avatar: owner.avatarUrl || '',
name: owner.name || '',
},
});
}

async sendReviewApproveEmail(inviteId: string) {
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId);
if (!inviteeUserId) {
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`);
const target = await this.getInviteeEmailTarget(inviteId);

if (!target) {
return;
}
const workspace = await this.getWorkspaceInfo(workspaceId);
const invitee = await this.user.findUserById(inviteeUserId);
if (!invitee) {
this.logger.error(
`Invitee user not found for inviteId: ${inviteId}, userId: ${inviteeUserId}`
);

await this.mailer.sendReviewApproveEmail(target.email, target.workspace);
}

async sendReviewDeclinedEmail(inviteId: string) {
const target = await this.getInviteeEmailTarget(inviteId);

if (!target) {
return;
}
await this.mailer.sendReviewApproveEmail(invitee.email, workspace);

await this.mailer.sendReviewDeclinedEmail(target.email, target.workspace);
}

async sendReviewDeclinedEmail(workspaceId: string, inviteeUserId: string) {
private async getInviteeEmailTarget(inviteId: string) {
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId);
if (!inviteeUserId) {
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`);
return;
}
const workspace = await this.getWorkspaceInfo(workspaceId);
const invitee = await this.user.findUserById(inviteeUserId);
if (!invitee) {
Expand All @@ -172,6 +184,9 @@ export class WorkspaceService {
return;
}

await this.mailer.sendReviewDeclinedEmail(invitee.email, workspace);
return {
email: invitee.email,
workspace,
};
}
}
36 changes: 21 additions & 15 deletions packages/backend/server/src/core/workspaces/resolvers/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class TeamWorkspaceResolver {
// after user click the invite link, we can check again and reject if charge failed
if (sendInviteMail) {
try {
await this.workspaceService.sendInviteMail(ret.inviteId, email);
await this.workspaceService.sendInviteMail(ret.inviteId);
ret.sentSuccess = true;
} catch (e) {
this.logger.warn(
Expand Down Expand Up @@ -253,8 +253,9 @@ export class TeamWorkspaceResolver {
);

if (result) {
// send approve mail
await this.workspaceService.sendReviewApproveEmail(result);
this.event.emit('workspace.members.requestApproved', {
inviteId: result,
});
}
return result;
}
Expand Down Expand Up @@ -314,22 +315,27 @@ export class TeamWorkspaceResolver {
}
}

@OnEvent('workspace.team.reviewRequest')
async onReviewRequest({
inviteIds,
}: EventPayload<'workspace.team.reviewRequest'>) {
@OnEvent('workspace.members.reviewRequested')
async onReviewRequested({
inviteId,
}: EventPayload<'workspace.members.reviewRequested'>) {
// send review request mail to owner and admin
for (const inviteId of inviteIds) {
await this.workspaceService.sendReviewRequestMail(inviteId);
}
await this.workspaceService.sendReviewRequestedMail(inviteId);
}

@OnEvent('workspace.team.declineRequest')
@OnEvent('workspace.members.requestDeclined')
async onDeclineRequest({
workspaceId,
inviteeId,
}: EventPayload<'workspace.team.declineRequest'>) {
inviteId,
}: EventPayload<'workspace.members.requestDeclined'>) {
// send decline mail
await this.workspaceService.sendReviewDeclinedEmail(workspaceId, inviteeId);
await this.workspaceService.sendReviewDeclinedEmail(inviteId);
}

@OnEvent('workspace.members.requestApproved')
async onApproveRequest({
inviteId,
}: EventPayload<'workspace.members.requestApproved'>) {
// send approve mail
await this.workspaceService.sendReviewApproveEmail(inviteId);
}
}
Loading

0 comments on commit 83618e3

Please sign in to comment.