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:
parent
22887430e8
commit
7cd7ea1f9e
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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!(
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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?;
|
||||||
|
|
|
||||||
|
|
@ -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?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue