feat: send notification when access request is approved (#854)
This commit is contained in:
parent
3b320b0619
commit
1664fe869e
|
|
@ -0,0 +1,131 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<style>
|
||||
td,th,div,p,a,h1,h2,h3,h4,h5,h6 {font-family: "Segoe UI", sans-serif; mso-line-height-rule: exactly;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<title>Your access request has been approved</title>
|
||||
<style>
|
||||
.hover-opacity-90:hover {
|
||||
opacity: 0.9 !important
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.sm-px-4 {
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important
|
||||
}
|
||||
.sm-py-12 {
|
||||
padding-top: 48px !important;
|
||||
padding-bottom: 48px !important
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; width: 100%; background-color: #faf5ff; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
|
||||
<div style="display: none">
|
||||
Workspace access request approved notification
|
||||
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
||||
</div>
|
||||
<div role="article" aria-roledescription="email" aria-label="Your access request has been approved" lang="en">
|
||||
<div class="sm-px-4 sm-py-12" style="background-color: #faf5ff; padding: 96px 48px; font-family: Helvetica, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; color: #000">
|
||||
<table align="center" cellpadding="0" cellspacing="0" role="none">
|
||||
<tr>
|
||||
<td style="width: 552px; max-width: 100%">
|
||||
<p style="width: 100%; white-space: normal; overflow-wrap: break-word; text-align: center; font-size: 24px">
|
||||
<span>Your request to access </span>
|
||||
<span style="font-size: 30px; font-weight: 700">{{ workspace_name }}</span>
|
||||
<span> has been approved </span>
|
||||
</p>
|
||||
<div role="separator" style="background-color: #cbd5e1; height: 1px; line-height: 1px; margin: 24px 20%">‍</div>
|
||||
<table align="center" cellpadding="0" cellspacing="0" role="none">
|
||||
<tr>
|
||||
<td style="width: 60px">
|
||||
<div style="margin-right: 8px; height: 60px; width: 60px; overflow: hidden; border-radius: 16px; background-color: #fff; border: 2px solid black">
|
||||
<img src="{{ workspace_icon_url }}" width="100%" height="100%" alt="{{ workspace_name }}" style="max-width: 100%; vertical-align: middle; line-height: 1; overflow: hidden; object-fit: cover">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="margin-bottom: 8px; font-weight: 700">{{ workspace_name }}</div>
|
||||
<div style="font-size: 14px; color: #64748b">
|
||||
{{ workspace_member_count }} members
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="text-align: center;">
|
||||
<a href="{{ launch_workspace_url }}" class="hover-opacity-90" style="margin-top: 32px; margin-bottom: 32px; display: inline-block; width: 60%; cursor: pointer; border-radius: 16px; padding: 16px 24px; color: #f8fafc; text-decoration: none; background-color: #9327ff; font-size: 20px; font-weight: 400; line-height: 20px">
|
||||
<!--[if mso]>
|
||||
<i style="mso-font-width: 150%; mso-text-raise: 30px" hidden> </i>
|
||||
<![endif]-->
|
||||
<span style="mso-text-raise: 16px">
|
||||
<div style="font-size: 24px; font-weight: 500">View workspace</div>
|
||||
</span>
|
||||
<!--[if mso]>
|
||||
<i hidden style="mso-font-width: 150%;"> ​</i>
|
||||
<![endif]-->
|
||||
</a>
|
||||
</div>
|
||||
<div style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: #64748b;
|
||||
">
|
||||
By clicking "View workspace" above, you confirm that you have read,
|
||||
understood, and agreed to AppFlowy's
|
||||
<a href="https://appflowy.io/terms/app" style="color: #64748b">Terms & Conditions</a>
|
||||
and
|
||||
<a href="https://appflowy.io/privacy/app" style="color: #64748b">Privacy Policy</a>.
|
||||
</div>
|
||||
<div role="separator" style="background-color: #cbd5e1; height: 1px; line-height: 1px; margin: 24px 20%;">‍</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #475569">
|
||||
<p style="margin: 0 0 16px; cursor: pointer; text-transform: uppercase">
|
||||
<a href="https://appflowy.io">
|
||||
<img src="https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy-Cloud/main/assets/mailer_templates/build_production/images/appflowy-logo.png" width="150px" style="max-width: 100%; vertical-align: middle; line-height: 1;" alt="">
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 14px; font-weight: 500; color: #000;">
|
||||
Bring projects, knowledge, and teams together with the power of AI.
|
||||
</p>
|
||||
<p style="cursor: default">
|
||||
<a href="https://twitter.com/appflowy" style="margin-right: 16px; color: #4338ca; text-decoration: none">
|
||||
<img src="https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy-Cloud/main/assets/mailer_templates/build_production/images/twitter.png" width="20" alt="Maizzle" style="max-width: 100%; vertical-align: middle; line-height: 1;">
|
||||
</a>
|
||||
<a href="https://www.reddit.com/r/AppFlowy" style="margin-right: 16px; color: #4338ca; text-decoration: none;">
|
||||
<img src="https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy-Cloud/main/assets/mailer_templates/build_production/images/reddit.png" width="20" alt="Maizzle" style="max-width: 100%; vertical-align: middle; line-height: 1;">
|
||||
</a>
|
||||
<a href="https://github.com/AppFlowy-IO/AppFlowy" style="margin-right: 16px; color: #4338ca; text-decoration: none;">
|
||||
<img src="https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy-Cloud/main/assets/mailer_templates/build_production/images/github.png" width="20" alt="Maizzle" style="max-width: 100%; vertical-align: middle; line-height: 1;">
|
||||
</a>
|
||||
<a href="https://discord.gg/9Q2xaN37tV" style="margin-right: 16px; color: #4338ca; text-decoration: none;">
|
||||
<img src="https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy-Cloud/main/assets/mailer_templates/build_production/images/discord.png" width="20" alt="Maizzle" style="max-width: 100%; vertical-align: middle; line-height: 1;">
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -30,6 +30,7 @@ module.exports = {
|
|||
userName: "John Doe",
|
||||
acceptUrl: "https://appflowy.io",
|
||||
approveUrl: "https://appflowy.io",
|
||||
launchWorkspaceUrl: "https://appflowy.io",
|
||||
workspaceName: "AppFlowy",
|
||||
workspaceMembersCount: "100",
|
||||
workspaceIconURL: "https://cdn-icons-png.flaticon.com/512/1078/1078013.png",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ module.exports = {
|
|||
userName: "{{ username }}",
|
||||
acceptUrl: "{{ accept_url }}",
|
||||
approveUrl: "{{ approve_url }}",
|
||||
launchWorkspaceUrl: "{{ launch_workspace_url }}",
|
||||
workspaceName: "{{ workspace_name }}",
|
||||
workspaceMembersCount: "{{ workspace_member_count }}",
|
||||
workspaceIconURL: "{{ workspace_icon_url }}",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
title: "Your access request has been approved"
|
||||
preheader: "Workspace access request approved notification"
|
||||
bodyClass: bg-purple-50
|
||||
---
|
||||
|
||||
<x-main>
|
||||
<div
|
||||
class="bg-purple-50 font-helvetica sm:px-4 px-12 sm:py-12 py-24 text-black"
|
||||
>
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td class="w-[552px] max-w-full">
|
||||
<p class="w-full text-center break-words whitespace-normal text-2xl">
|
||||
<span class="mx-2=1">Your request to access </span>
|
||||
<span class="text-3xl font-bold">{{ workspaceName }}</span>
|
||||
<span class="mx-2=1"> has been approved </span>
|
||||
</p>
|
||||
<x-divider space-x="20%" />
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td class="w-[60px]">
|
||||
<div
|
||||
style="border: 2px solid black"
|
||||
class="rounded-2xl mr-2 w-[60px] h-[60px] bg-white overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src="{{ workspaceIconURL }}"
|
||||
class="overflow-hidden object-cover"
|
||||
width="100%"
|
||||
height="100%"
|
||||
alt="{{ workspaceName }}"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-bold mb-2">{{ workspaceName }}</div>
|
||||
<div class="text-sm text-slate-500">
|
||||
{{ workspaceMembersCount }} members
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<x-button
|
||||
align="center"
|
||||
class="hover:opacity-90 cursor-pointer !text-xl !leading-[20px] !bg-[#9327ff] !font-normal w-[60%] my-8 rounded-2xl"
|
||||
href="{{ launchWorkspaceUrl }}"
|
||||
>
|
||||
<div class="font-medium text-[24px]">View workspace</div>
|
||||
</x-button>
|
||||
<div
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: #64748b;
|
||||
"
|
||||
>
|
||||
By clicking "View workspace" above, you confirm that you have read,
|
||||
understood, and agreed to AppFlowy's
|
||||
<a href="https://appflowy.io/terms/app" style="color: #64748b"
|
||||
>Terms & Conditions</a
|
||||
>
|
||||
and
|
||||
<a href="https://appflowy.io/privacy/app" style="color: #64748b"
|
||||
>Privacy Policy</a
|
||||
>.
|
||||
</div>
|
||||
<x-divider space-x="20%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center text-slate-600 text-xs px-6">
|
||||
<p class="m-0 mb-4 uppercase cursor-pointer">
|
||||
<a href="https://appflowy.io">
|
||||
<img
|
||||
src="{{ cdnBaseUrl }}images/appflowy-logo.png"
|
||||
width="150px"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
<p class="m-0 text-sm text-black font-medium">
|
||||
Bring projects, knowledge, and teams together with the power of AI.
|
||||
</p>
|
||||
|
||||
<p class="cursor-default">
|
||||
<a
|
||||
href="https://twitter.com/appflowy"
|
||||
class="text-indigo-700 [text-decoration:none] mr-4"
|
||||
>
|
||||
<img
|
||||
src="{{ cdnBaseUrl }}images/twitter.png"
|
||||
width="20"
|
||||
alt="Maizzle"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.reddit.com/r/AppFlowy"
|
||||
class="text-indigo-700 [text-decoration:none] mr-4"
|
||||
>
|
||||
<img
|
||||
src="{{ cdnBaseUrl }}images/reddit.png"
|
||||
width="20"
|
||||
alt="Maizzle"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/AppFlowy-IO/AppFlowy"
|
||||
class="text-indigo-700 [text-decoration:none] mr-4"
|
||||
>
|
||||
<img
|
||||
src="{{ cdnBaseUrl }}images/github.png"
|
||||
width="20"
|
||||
alt="Maizzle"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/9Q2xaN37tV"
|
||||
class="text-indigo-700 [text-decoration:none] mr-4"
|
||||
>
|
||||
<img
|
||||
src="{{ cdnBaseUrl }}images/discord.png"
|
||||
width="20"
|
||||
alt="Maizzle"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</x-main>
|
||||
|
|
@ -88,7 +88,22 @@ async fn post_approve_access_request_handler(
|
|||
let uid = state.user_cache.get_user_uid(&uuid).await?;
|
||||
let access_request_id = access_request_id.into_inner();
|
||||
let is_approved = approve_access_request_params.is_approved;
|
||||
approve_or_reject_access_request(&state.pg_pool, access_request_id, uid, *uuid, is_approved)
|
||||
.await?;
|
||||
let appflowy_web_url = state
|
||||
.config
|
||||
.appflowy_web_url
|
||||
.clone()
|
||||
.ok_or(AppError::Internal(anyhow!(
|
||||
"AppFlowy web url has not been set"
|
||||
)))?;
|
||||
approve_or_reject_access_request(
|
||||
&state.pg_pool,
|
||||
state.mailer.clone(),
|
||||
&appflowy_web_url,
|
||||
access_request_id,
|
||||
uid,
|
||||
*uuid,
|
||||
is_approved,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(AppResponse::Ok()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use crate::{
|
|||
folder_view::{to_dto_view_icon, to_view_layout},
|
||||
ops::get_latest_collab_folder,
|
||||
},
|
||||
mailer::{Mailer, WorkspaceAccessRequestMailerParam},
|
||||
mailer::{Mailer, WorkspaceAccessRequestApprovedMailerParam, WorkspaceAccessRequestMailerParam},
|
||||
};
|
||||
|
||||
pub async fn create_access_request(
|
||||
|
|
@ -58,7 +58,7 @@ pub async fn create_access_request(
|
|||
workspace_name: access_request.workspace.workspace_name,
|
||||
workspace_icon_url,
|
||||
workspace_member_count: access_request.workspace.member_count.unwrap_or(0),
|
||||
approve_url: approve_url.to_string(),
|
||||
approve_url,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
|
@ -110,6 +110,8 @@ pub async fn get_access_request(
|
|||
|
||||
pub async fn approve_or_reject_access_request(
|
||||
pg_pool: &PgPool,
|
||||
mailer: Mailer,
|
||||
appflowy_web_url: &str,
|
||||
request_id: Uuid,
|
||||
uid: i64,
|
||||
user_uuid: Uuid,
|
||||
|
|
@ -133,6 +135,35 @@ pub async fn approve_or_reject_access_request(
|
|||
role,
|
||||
)
|
||||
.await?;
|
||||
let cloned_mailer = mailer.clone();
|
||||
let launch_workspace_url = format!(
|
||||
"{}/app/{}",
|
||||
appflowy_web_url, &access_request.workspace.workspace_id
|
||||
);
|
||||
|
||||
// use default icon until we have workspace icon
|
||||
let workspace_icon_url =
|
||||
"https://miro.medium.com/v2/resize:fit:2400/1*mTPfm7CwU31-tLhtLNkyJw.png".to_string();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = cloned_mailer
|
||||
.send_workspace_access_request_approval_notification(
|
||||
&access_request.requester.name,
|
||||
&access_request.requester.email,
|
||||
WorkspaceAccessRequestApprovedMailerParam {
|
||||
workspace_name: access_request.workspace.workspace_name,
|
||||
workspace_icon_url,
|
||||
workspace_member_count: access_request.workspace.member_count.unwrap_or(0),
|
||||
launch_workspace_url,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to send access request approved notification email: {:?}",
|
||||
err
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
let status = if is_approved {
|
||||
AFAccessRequestStatusColumn::Approved
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ pub struct Mailer {
|
|||
|
||||
pub const WORKSPACE_INVITE_TEMPLATE_NAME: &str = "workspace_invite";
|
||||
pub const WORKSPACE_ACCESS_REQUEST_TEMPLATE_NAME: &str = "workspace_access_request";
|
||||
pub const WORKSPACE_ACCESS_REQUEST_APPROVED_NOTIFICATION_TEMPLATE_NAME: &str =
|
||||
"workspace_access_request_approved_notification";
|
||||
|
||||
impl Mailer {
|
||||
pub async fn new(
|
||||
|
|
@ -39,12 +41,19 @@ impl Mailer {
|
|||
include_str!("../assets/mailer_templates/build_production/workspace_invitation.html");
|
||||
let access_request_template =
|
||||
include_str!("../assets/mailer_templates/build_production/access_request.html");
|
||||
let access_request_approved_notification_template = include_str!(
|
||||
"../assets/mailer_templates/build_production/access_request_approved_notification.html"
|
||||
);
|
||||
let template_strings = HashMap::from([
|
||||
(WORKSPACE_INVITE_TEMPLATE_NAME, workspace_invite_template),
|
||||
(
|
||||
WORKSPACE_ACCESS_REQUEST_TEMPLATE_NAME,
|
||||
access_request_template,
|
||||
),
|
||||
(
|
||||
WORKSPACE_ACCESS_REQUEST_APPROVED_NOTIFICATION_TEMPLATE_NAME,
|
||||
access_request_approved_notification_template,
|
||||
),
|
||||
]);
|
||||
|
||||
for (template_name, template_string) in template_strings {
|
||||
|
|
@ -135,6 +144,24 @@ impl Mailer {
|
|||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_workspace_access_request_approval_notification(
|
||||
&self,
|
||||
recipient_name: &str,
|
||||
email: &str,
|
||||
param: WorkspaceAccessRequestApprovedMailerParam,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let subject = "Notification: Workspace access request approved";
|
||||
self
|
||||
.send_email_template(
|
||||
Some(recipient_name.to_string()),
|
||||
email,
|
||||
WORKSPACE_ACCESS_REQUEST_APPROVED_NOTIFICATION_TEMPLATE_NAME,
|
||||
param,
|
||||
subject,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
|
|
@ -156,3 +183,11 @@ pub struct WorkspaceAccessRequestMailerParam {
|
|||
pub workspace_member_count: i64,
|
||||
pub approve_url: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct WorkspaceAccessRequestApprovedMailerParam {
|
||||
pub workspace_name: String,
|
||||
pub workspace_icon_url: String,
|
||||
pub workspace_member_count: i64,
|
||||
pub launch_workspace_url: String,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue