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 { let invi = vec![WorkspaceMemberInvitation {
email: user_email.to_string(), email: user_email.to_string(),
role: AFRole::Member, role: AFRole::Member,
skip_email_send: true,
..Default::default()
}]; }];
let http_client = reqwest::Client::new(); let http_client = reqwest::Client::new();

View File

@ -134,6 +134,11 @@ services:
- APPFLOWY_AI_SERVER_HOST=${APPFLOWY_AI_SERVER_HOST} - APPFLOWY_AI_SERVER_HOST=${APPFLOWY_AI_SERVER_HOST}
- APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT} - APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT}
- APPFLOWY_WEB_URL=${APPFLOWY_WEB_URL} - 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: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile

View File

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

View File

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

View File

@ -43,6 +43,22 @@ pub struct CreateWorkspaceMember {
pub struct WorkspaceMemberInvitation { pub struct WorkspaceMemberInvitation {
pub email: String, pub email: String,
pub role: AFRole, 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)] #[derive(Deserialize)]

View File

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

View File

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

View File

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

View File

@ -33,6 +33,15 @@ impl AFCloudMailer {
&subject, &subject,
) )
.await .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( pub async fn send_workspace_access_request(

View File

@ -1,5 +1,6 @@
use anyhow::Context;
use app_error::ErrorCode; 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 database_entity::dto::{AFRole, AFWorkspaceInvitationStatus};
use shared_entity::dto::workspace_dto::{QueryWorkspaceParam, WorkspaceMemberInvitation}; use shared_entity::dto::workspace_dto::{QueryWorkspaceParam, WorkspaceMemberInvitation};
@ -30,6 +31,8 @@ async fn invite_workspace_crud() {
vec![WorkspaceMemberInvitation { vec![WorkspaceMemberInvitation {
email: bob.email.clone(), email: bob.email.clone(),
role: AFRole::Member, role: AFRole::Member,
skip_email_send: true,
..Default::default()
}], }],
) )
.await .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 { vec![WorkspaceMemberInvitation {
email: c2.email().await, email: c2.email().await,
role: AFRole::Member, role: AFRole::Member,
skip_email_send: true,
..Default::default()
}], }],
) )
.await .await
@ -131,6 +133,8 @@ async fn add_not_exist_workspace_members() {
vec![WorkspaceMemberInvitation { vec![WorkspaceMemberInvitation {
email: email.clone(), email: email.clone(),
role: AFRole::Member, role: AFRole::Member,
skip_email_send: true,
..Default::default()
}], }],
) )
.await .await

View File

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