From 7cd7ea1f9e870779005fa28c3c3de8d72ba779f2 Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 11 Dec 2024 09:43:27 +0800 Subject: [PATCH] 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 --- admin_frontend/src/ext/api.rs | 2 + docker-compose-ci.yml | 5 +++ libs/client-api-test/src/test_client.rs | 7 ++- libs/database/src/workspace.rs | 2 +- libs/shared-entity/src/dto/workspace_dto.rs | 16 +++++++ src/api/workspace.rs | 4 +- src/application.rs | 16 ++++--- src/biz/workspace/ops.rs | 48 ++++++++++++--------- src/mailer.rs | 9 ++++ tests/workspace/invitation_crud.rs | 27 +++++++++++- tests/workspace/member_crud.rs | 4 ++ tests/workspace/workspace_settings.rs | 2 + 12 files changed, 109 insertions(+), 33 deletions(-) diff --git a/admin_frontend/src/ext/api.rs b/admin_frontend/src/ext/api.rs index b82428fb..ddb80af2 100644 --- a/admin_frontend/src/ext/api.rs +++ b/admin_frontend/src/ext/api.rs @@ -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(); diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index d19ee722..8937fa8c 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -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 diff --git a/libs/client-api-test/src/test_client.rs b/libs/client-api-test/src/test_client.rs index dcc0b737..9f991ac3 100644 --- a/libs/client-api-test/src/test_client.rs +++ b/libs/client-api-test/src/test_client.rs @@ -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?; diff --git a/libs/database/src/workspace.rs b/libs/database/src/workspace.rs index 7d30c7be..42013bed 100644 --- a/libs/database/src/workspace.rs +++ b/libs/database/src/workspace.rs @@ -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!( diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index 84ca4708..9bbb6651 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -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)] diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 7c966bea..d5ba72e1 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -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?; diff --git a/src/application.rs b/src/application.rs index be9a2e62..10bf0103 100644 --- a/src/application.rs +++ b/src/application.rs @@ -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 Result { +async fn get_mailer(mailer: &MailerSetting) -> Result { + 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?; diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 7451dc3d..c6cbf742 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -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 diff --git a/src/mailer.rs b/src/mailer.rs index 0024ef83..08b1cad9 100644 --- a/src/mailer.rs +++ b/src/mailer.rs @@ -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( diff --git a/tests/workspace/invitation_crud.rs b/tests/workspace/invitation_crud.rs index 9964e689..15d19488 100644 --- a/tests/workspace/invitation_crud.rs +++ b/tests/workspace/invitation_crud.rs @@ -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(); +} diff --git a/tests/workspace/member_crud.rs b/tests/workspace/member_crud.rs index 536f429a..f9d5591b 100644 --- a/tests/workspace/member_crud.rs +++ b/tests/workspace/member_crud.rs @@ -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 diff --git a/tests/workspace/workspace_settings.rs b/tests/workspace/workspace_settings.rs index d97a5834..fdefbfc5 100644 --- a/tests/workspace/workspace_settings.rs +++ b/tests/workspace/workspace_settings.rs @@ -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