-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(server): team mail sender (#9104)
fix AF-1914
- Loading branch information
1 parent
350696c
commit 69e5997
Showing
19 changed files
with
416 additions
and
208 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
export * from './blob'; | ||
export * from './history'; | ||
export * from './page'; | ||
export * from './service'; | ||
export * from './team'; | ||
export * from './workspace'; |
177 changes: 177 additions & 0 deletions
177
packages/backend/server/src/core/workspaces/resolvers/service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { Injectable, Logger } from '@nestjs/common'; | ||
import { PrismaClient } from '@prisma/client'; | ||
import { getStreamAsBuffer } from 'get-stream'; | ||
|
||
import { Cache, MailService } from '../../../fundamentals'; | ||
import { DocContentService } from '../../doc-renderer'; | ||
import { PermissionService } from '../../permission'; | ||
import { WorkspaceBlobStorage } from '../../storage'; | ||
import { UserService } from '../../user'; | ||
|
||
export const defaultWorkspaceAvatar = | ||
'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQtSURBVHgBfVa9jhxFEK6q7rkf+4T2AgdIIC0ZoXkBuNQJtngBuIzs1hIRye1FhL438D0CRgKRGUeE6wwkhHYlkE2AtGdkbN/MdJe/qu7Z27PWnnG5Znq7v/rqd47pHddkNh/918tR1/FBamXc9zxOPVFKfJ4yP86qD1LD3/986/3F2zB40+LXv83HrHq/6+gAoNS1kF4odUz2nhJRTkI5E6mD6Bk1crLJkLy5cHc+P4ohzxLng8RKLqKUq6hkUtBSe8Zvdmfir7TT2a0fnkzeaeCbv/44ztSfZskjP2ygVRM0mbYTpgHMMMS8CsIIj/c+//Hp8UYD3z758whQUwdeEwPjAZQLqJhI0VxB2MVco+kXP/0zuZKD6dP5uM397ELzqEtMba/UJ4t7iXeq8U94z52Q+js09qjlIXMxAEsRDJpI59dVPzlDTooHko7BdlR2FcYmAtbGMmAt2mFI4yDQkIjtEQkxUAMKAPD9SiOK4b578N0S7Nt+fqFKbTbmRD1YGXurEmdtnjjz4kFuIV0gtWewV62hMHBY2gpEOw3Rnmztx9jnO72xzTV/YkzgNmgkiypeYJdCLjonqyAAg7VCshVpjTbD08HbxrySdhKxcDvoJTA5gLvpeXVQ+K340WKea9UkNeZVqGSba/IbF6athj+LUeRmRCyiAVnlAKhJJQfmugGZ28ZWna24RGzwNUNUqpWGf6HkajvAgNA4NsSjHgcb9obx+k5c3DUttcwd3NcHxpVurXQ2d4MZACGw9TwEHsdtbEwytL1xywAGcxavjoH1quLVywuGi+aBhFWexRilFSwK0QzgdUdkkVMeKw4wijrgxjzz2CefCRZn+21ViOWW4Ym9nNnyFLMbMS8ivNhGP8RdlgUojBkuBLDpEPi+5LpWiDURgFkKOIIckJTgN/sZ84KtKkKpDnsOZiTQ47jD4ZGwHghbw6AXIL3lo5Zg6Tp2AwIAyYJ8BRzGfmfPl6kI7HOLUdN2LIg+4IfL5SiFdvkK4blI6h50qda7jQI0CUMLdEhFIkqtQciMvXsgpaZ1pWtVUfrIa+TX5/8+RBcftAhTa91r8ycXA5ZxBqhAh2zgVagUAddxMkxfF/JxfvbpB+8d2jhBtsPhtuqsE0HJlhxYeHKdkCU8xUCos8dmkDdnGaOlJ1yy9dM52J2spqldvz9fTgB4z+aQd2kqjUY2KU2s4dTT7ezD0AqDAbvZiKF/VO9+fGPv9IoBu+b/P5ti6djDY+JlSg4ug1jc6fJbMAx9/3b4CNGTD/evT698D9avv188m4gKvko8MiMeJC3jmOvU9MSuHXZohAVpOrmxd+10HW/jR3/58uU45TRFt35ZR2XpY61DzW+tH3z/7xdM8sP93d3Fm1gbDawbEtU7CMtt/JVxEw01Kh7RAmoBE4+u7eycYv38bRivAZbdHBtPrwOHAAAAAElFTkSuQmCC'; | ||
|
||
export type InviteInfo = { | ||
workspaceId: string; | ||
inviterUserId?: string; | ||
inviteeUserId?: string; | ||
}; | ||
|
||
@Injectable() | ||
export class WorkspaceService { | ||
private readonly logger = new Logger(WorkspaceService.name); | ||
|
||
constructor( | ||
private readonly blobStorage: WorkspaceBlobStorage, | ||
private readonly cache: Cache, | ||
private readonly doc: DocContentService, | ||
private readonly mailer: MailService, | ||
private readonly permission: PermissionService, | ||
private readonly prisma: PrismaClient, | ||
private readonly user: UserService | ||
) {} | ||
|
||
async getInviteInfo(inviteId: string): Promise<InviteInfo> { | ||
// invite link | ||
const invite = await this.cache.get<InviteInfo>( | ||
`workspace:inviteLinkId:${inviteId}` | ||
); | ||
if (typeof invite?.workspaceId === 'string') { | ||
return invite; | ||
} | ||
|
||
return await this.prisma.workspaceUserPermission | ||
.findUniqueOrThrow({ | ||
where: { | ||
id: inviteId, | ||
}, | ||
select: { | ||
workspaceId: true, | ||
userId: true, | ||
}, | ||
}) | ||
.then(r => ({ | ||
workspaceId: r.workspaceId, | ||
inviteeUserId: r.userId, | ||
})); | ||
} | ||
|
||
async getWorkspaceInfo(workspaceId: string) { | ||
const workspaceContent = await this.doc.getWorkspaceContent(workspaceId); | ||
|
||
let avatar = defaultWorkspaceAvatar; | ||
if (workspaceContent?.avatarKey) { | ||
const avatarBlob = await this.blobStorage.get( | ||
workspaceId, | ||
workspaceContent.avatarKey | ||
); | ||
|
||
if (avatarBlob.body) { | ||
avatar = (await getStreamAsBuffer(avatarBlob.body)).toString('base64'); | ||
} | ||
} | ||
|
||
return { | ||
avatar, | ||
id: workspaceId, | ||
name: workspaceContent?.name ?? '', | ||
}; | ||
} | ||
|
||
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); | ||
const workspace = await this.getWorkspaceInfo(workspaceId); | ||
const invitee = inviteeUserId | ||
? await this.user.findUserById(inviteeUserId) | ||
: null; | ||
const inviter = inviterUserId | ||
? await this.user.findUserById(inviterUserId) | ||
: await this.permission.getWorkspaceOwner(workspaceId); | ||
|
||
if (!inviter || !invitee) { | ||
this.logger.error( | ||
`Inviter or invitee user not found for inviteId: ${inviteId}` | ||
); | ||
return false; | ||
} | ||
|
||
await this.mailer.sendAcceptedEmail(inviter.email, { | ||
inviteeName: invitee.name, | ||
workspaceName: workspace.name, | ||
}); | ||
return true; | ||
} | ||
|
||
async sendReviewRequestMail(inviteId: string) { | ||
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId); | ||
if (!inviteeUserId) { | ||
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`); | ||
return; | ||
} | ||
|
||
const invitee = await this.user.findUserById(inviteeUserId); | ||
if (!invitee) { | ||
this.logger.error( | ||
`Invitee user not found for inviteId: ${inviteId}, userId: ${inviteeUserId}` | ||
); | ||
return; | ||
} | ||
|
||
const workspace = await this.getWorkspaceInfo(workspaceId); | ||
const owner = await this.permission.getWorkspaceOwner(workspaceId); | ||
const admin = await this.permission.getWorkspaceAdmin(workspaceId); | ||
|
||
for (const user of [owner, ...admin]) { | ||
await this.mailer.sendReviewRequestMail( | ||
user.email, | ||
invitee.email, | ||
workspace | ||
); | ||
} | ||
} | ||
|
||
async sendReviewApproveEmail(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) { | ||
this.logger.error( | ||
`Invitee user not found for inviteId: ${inviteId}, userId: ${inviteeUserId}` | ||
); | ||
return; | ||
} | ||
await this.mailer.sendReviewApproveEmail(invitee.email, workspace); | ||
} | ||
|
||
async sendReviewDeclinedEmail(workspaceId: string, inviteeUserId: string) { | ||
const workspace = await this.getWorkspaceInfo(workspaceId); | ||
const invitee = await this.user.findUserById(inviteeUserId); | ||
if (!invitee) { | ||
this.logger.error( | ||
`Invitee user not found in workspace: ${workspaceId}, userId: ${inviteeUserId}` | ||
); | ||
return; | ||
} | ||
|
||
await this.mailer.sendReviewDeclinedEmail(invitee.email, workspace); | ||
} | ||
} |
Oops, something went wrong.