feat: invite to workspace email wait (#1057)

* feat: invite to workspace email wait

* feat: add option to skip email

* fix: docker compose ci mailer settings
This commit is contained in:
Zack 2024-12-11 09:43:27 +08:00 committed by GitHub
parent 22887430e8
commit 7cd7ea1f9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 109 additions and 33 deletions

View File

@ -204,6 +204,8 @@ pub async fn invite_user_to_workspace(
let invi = vec![WorkspaceMemberInvitation {
email: user_email.to_string(),
role: AFRole::Member,
skip_email_send: true,
..Default::default()
}];
let http_client = reqwest::Client::new();

View File

@ -134,6 +134,11 @@ services:
- APPFLOWY_AI_SERVER_HOST=${APPFLOWY_AI_SERVER_HOST}
- APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT}
- APPFLOWY_WEB_URL=${APPFLOWY_WEB_URL}
- APPFLOWY_MAILER_SMTP_HOST=${APPFLOWY_MAILER_SMTP_HOST}
- APPFLOWY_MAILER_SMTP_PORT=${APPFLOWY_MAILER_SMTP_PORT}
- APPFLOWY_MAILER_SMTP_USERNAME=${APPFLOWY_MAILER_SMTP_USERNAME}
- APPFLOWY_MAILER_SMTP_EMAIL=${APPFLOWY_MAILER_SMTP_EMAIL}
- APPFLOWY_MAILER_SMTP_PASSWORD=${APPFLOWY_MAILER_SMTP_PASSWORD}
build:
context: .
dockerfile: Dockerfile

View File

@ -368,7 +368,12 @@ impl TestClient {
.api_client
.invite_workspace_members(
workspace_id,
vec![WorkspaceMemberInvitation { email, role }],
vec![WorkspaceMemberInvitation {
email,
role,
skip_email_send: true,
..Default::default()
}],
)
.await?;

View File

@ -299,7 +299,7 @@ pub async fn insert_workspace_invitation(
workspace_id: &uuid::Uuid,
inviter_uuid: &Uuid,
invitee_email: &str,
invitee_role: AFRole,
invitee_role: &AFRole,
) -> Result<(), AppError> {
let role_id: i32 = invitee_role.into();
sqlx::query!(

View File

@ -43,6 +43,22 @@ pub struct CreateWorkspaceMember {
pub struct WorkspaceMemberInvitation {
pub email: String,
pub role: AFRole,
#[serde(default)]
pub skip_email_send: bool,
#[serde(default)]
pub wait_email_send: bool,
}
impl Default for WorkspaceMemberInvitation {
fn default() -> Self {
Self {
email: "".to_string(),
role: AFRole::Member,
skip_email_send: false,
wait_email_send: false,
}
}
}
#[derive(Deserialize)]

View File

@ -388,7 +388,7 @@ async fn post_workspace_invite_handler(
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Owner)
.await?;
let invited_members = payload.into_inner();
let invitations = payload.into_inner();
workspace::ops::invite_workspace_members(
&state.mailer,
&state.gotrue_admin,
@ -396,7 +396,7 @@ async fn post_workspace_invite_handler(
&state.gotrue_client,
&user_uuid,
&workspace_id,
invited_members,
invitations,
state.config.appflowy_web_url.as_deref(),
)
.await?;

View File

@ -27,6 +27,7 @@ use aws_sdk_s3::types::{
BucketInfo, BucketLocationConstraint, BucketType, CreateBucketConfiguration,
};
use collab::lock::Mutex;
use mailer::config::MailerSetting;
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use openssl::x509::X509;
use secrecy::{ExposeSecret, Secret};
@ -316,7 +317,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
.connect_lazy();
let grpc_history_client = Arc::new(Mutex::new(HistoryClient::new(channel)));
let mailer = get_mailer(config).await?;
let mailer = get_mailer(&config.mailer).await?;
info!("Application state initialized");
Ok(AppState {
@ -446,13 +447,14 @@ async fn create_bucket_if_not_exists(
}
}
async fn get_mailer(config: &Config) -> Result<AFCloudMailer, Error> {
async fn get_mailer(mailer: &MailerSetting) -> Result<AFCloudMailer, Error> {
info!("Connecting to mailer with setting: {:?}", mailer);
let mailer = Mailer::new(
config.mailer.smtp_username.clone(),
config.mailer.smtp_email.clone(),
config.mailer.smtp_password.clone(),
&config.mailer.smtp_host,
config.mailer.smtp_port,
mailer.smtp_username.clone(),
mailer.smtp_email.clone(),
mailer.smtp_password.clone(),
&mailer.smtp_host,
mailer.smtp_port,
)
.await?;

View File

@ -418,7 +418,7 @@ pub async fn invite_workspace_members(
workspace_id,
inviter,
invitation.email.as_str(),
invitation.role,
&invitation.role,
)
.await?;
invite_id
@ -457,26 +457,32 @@ pub async fn invite_workspace_members(
}
};
// send email can be slow, so send email in background
let cloned_mailer = mailer.clone();
tokio::spawn(async move {
if let Err(err) = cloned_mailer
.send_workspace_invite(
&invitation.email,
WorkspaceInviteMailerParam {
user_icon_url,
username: inviter_name,
workspace_name,
workspace_icon_url,
workspace_member_count,
accept_url,
},
)
.await
{
tracing::error!("Failed to send workspace invite email: {:?}", err);
};
});
if !invitation.skip_email_send {
let cloned_mailer = mailer.clone();
let email_sending = tokio::spawn(async move {
cloned_mailer
.send_workspace_invite(
&invitation.email,
WorkspaceInviteMailerParam {
user_icon_url,
username: inviter_name,
workspace_name,
workspace_icon_url,
workspace_member_count,
accept_url,
},
)
.await
});
if invitation.wait_email_send {
email_sending.await??;
}
} else {
tracing::info!(
"Skipping email send for workspace invite to {}",
invitation.email
);
}
}
txn

View File

@ -33,6 +33,15 @@ impl AFCloudMailer {
&subject,
)
.await
.map(|_| tracing::info!("Sent workspace invite email to {}", email))
.map_err(|err| {
tracing::error!(
"Failed to send workspace invite email to {}: {}",
email,
err
);
err
})
}
pub async fn send_workspace_access_request(

View File

@ -1,5 +1,6 @@
use anyhow::Context;
use app_error::ErrorCode;
use client_api_test::generate_unique_registered_user_client;
use client_api_test::{generate_unique_registered_user_client, TestClient};
use database_entity::dto::{AFRole, AFWorkspaceInvitationStatus};
use shared_entity::dto::workspace_dto::{QueryWorkspaceParam, WorkspaceMemberInvitation};
@ -30,6 +31,8 @@ async fn invite_workspace_crud() {
vec![WorkspaceMemberInvitation {
email: bob.email.clone(),
role: AFRole::Member,
skip_email_send: true,
..Default::default()
}],
)
.await
@ -149,3 +152,25 @@ async fn invite_workspace_crud() {
}
}
}
#[tokio::test]
async fn invite_wait_email_sending_success() {
let c1 = TestClient::new_user_without_ws_conn().await;
let c2 = TestClient::new_user_without_ws_conn().await;
let workspace_id = c1.workspace_id().await;
let _: () = c1
.api_client
.invite_workspace_members(
&workspace_id,
vec![WorkspaceMemberInvitation {
email: c2.user.email,
role: AFRole::Member,
skip_email_send: false,
wait_email_send: true,
}],
)
.await
.context("failed to send email to invite workspace members")
.unwrap();
}

View File

@ -101,6 +101,8 @@ async fn add_duplicate_workspace_members() {
vec![WorkspaceMemberInvitation {
email: c2.email().await,
role: AFRole::Member,
skip_email_send: true,
..Default::default()
}],
)
.await
@ -131,6 +133,8 @@ async fn add_not_exist_workspace_members() {
vec![WorkspaceMemberInvitation {
email: email.clone(),
role: AFRole::Member,
skip_email_send: true,
..Default::default()
}],
)
.await

View File

@ -67,6 +67,8 @@ async fn invite_user_to_workspace(
vec![WorkspaceMemberInvitation {
email: member_email.to_string(),
role: AFRole::Member,
skip_email_send: true,
..Default::default()
}],
)
.await