From ec7eb54bfca1f43a047cb65406402b9481ed9985 Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 18 May 2024 08:33:08 +0800 Subject: [PATCH] chore: Self host improvement (#561) * fix: remove multiple ways to specify database name * feat: resent email for already invited user * feat: mailer address from smtp username * feat: allow user defined smtp port --- ...5d59426119615a8d5d8a46101f6ccec00d617.json | 22 ----------- ...007f0a264f7d3db9de6326ef8082a2a898995.json | 28 +++++++++++++ deploy.env | 1 + libs/database/src/workspace.rs | 16 +++++--- src/application.rs | 3 +- src/biz/workspace/ops.rs | 39 +++++++++++-------- src/config/config.rs | 18 ++++----- src/mailer.rs | 15 +++++-- 8 files changed, 83 insertions(+), 59 deletions(-) delete mode 100644 .sqlx/query-374c2d7b26f923328edd09b0d515d59426119615a8d5d8a46101f6ccec00d617.json create mode 100644 .sqlx/query-594af4041e0778476a699536316007f0a264f7d3db9de6326ef8082a2a898995.json diff --git a/.sqlx/query-374c2d7b26f923328edd09b0d515d59426119615a8d5d8a46101f6ccec00d617.json b/.sqlx/query-374c2d7b26f923328edd09b0d515d59426119615a8d5d8a46101f6ccec00d617.json deleted file mode 100644 index 0c14c86a..00000000 --- a/.sqlx/query-374c2d7b26f923328edd09b0d515d59426119615a8d5d8a46101f6ccec00d617.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT invitee_email\n FROM public.af_workspace_invitation\n WHERE workspace_id = $1\n AND status = 0\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "invitee_email", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "374c2d7b26f923328edd09b0d515d59426119615a8d5d8a46101f6ccec00d617" -} diff --git a/.sqlx/query-594af4041e0778476a699536316007f0a264f7d3db9de6326ef8082a2a898995.json b/.sqlx/query-594af4041e0778476a699536316007f0a264f7d3db9de6326ef8082a2a898995.json new file mode 100644 index 00000000..fb19e523 --- /dev/null +++ b/.sqlx/query-594af4041e0778476a699536316007f0a264f7d3db9de6326ef8082a2a898995.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, invitee_email\n FROM public.af_workspace_invitation\n WHERE workspace_id = $1\n AND status = 0\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "invitee_email", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "594af4041e0778476a699536316007f0a264f7d3db9de6326ef8082a2a898995" +} diff --git a/deploy.env b/deploy.env index c687b447..53ce8cdb 100644 --- a/deploy.env +++ b/deploy.env @@ -83,6 +83,7 @@ APPFLOWY_S3_BUCKET=appflowy # AppFlowy Cloud Mailer APPFLOWY_MAILER_SMTP_HOST=smtp.gmail.com +APPFLOWY_MAILER_SMTP_PORT=465 APPFLOWY_MAILER_SMTP_USERNAME=email_sender@some_company.com APPFLOWY_MAILER_SMTP_PASSWORD=email_sender_password diff --git a/libs/database/src/workspace.rs b/libs/database/src/workspace.rs index a0a15864..36950c5f 100644 --- a/libs/database/src/workspace.rs +++ b/libs/database/src/workspace.rs @@ -4,7 +4,7 @@ use sqlx::{ types::{uuid, Uuid}, Executor, PgPool, Postgres, Transaction, }; -use std::{collections::HashSet, ops::DerefMut}; +use std::{collections::HashMap, ops::DerefMut}; use tracing::{event, instrument}; use crate::pg_row::AFWorkspaceMemberPermRow; @@ -711,10 +711,10 @@ pub async fn select_workspace_member_count_from_workspace_id( pub async fn select_workspace_pending_invitations( pool: &PgPool, workspace_id: &Uuid, -) -> Result, AppError> { - let invitee_emails = sqlx::query_scalar!( +) -> Result, AppError> { + let res = sqlx::query!( r#" - SELECT invitee_email + SELECT id, invitee_email FROM public.af_workspace_invitation WHERE workspace_id = $1 AND status = 0 @@ -723,7 +723,13 @@ pub async fn select_workspace_pending_invitations( ) .fetch_all(pool) .await?; - Ok(invitee_emails.into_iter().collect()) + + let inv_id_by_email = res + .into_iter() + .map(|row| (row.invitee_email, row.id)) + .collect::>(); + + Ok(inv_id_by_email) } #[inline] diff --git a/src/application.rs b/src/application.rs index fff692e5..0e77907c 100644 --- a/src/application.rs +++ b/src/application.rs @@ -252,6 +252,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result Result .acquire_timeout(Duration::from_secs(10)) .max_lifetime(Duration::from_secs(30 * 60)) .idle_timeout(Duration::from_secs(30)) - .connect_with(setting.with_db()) + .connect_with(setting.pg_connect_options()) .await .map_err(|e| anyhow::anyhow!("Failed to connect to postgres database: {}", e)) } diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 4032b111..4a85f99e 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -201,10 +201,6 @@ pub async fn invite_workspace_members( tracing::warn!("User already in workspace: {}", invitation.email); continue; } - if pending_invitations.contains(&invitation.email) { - tracing::warn!("User already invited: {}", invitation.email); - continue; - } let inviter_name = inviter_name.clone(); let workspace_name = workspace_name.clone(); @@ -216,7 +212,29 @@ pub async fn invite_workspace_members( let user_icon_url = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png" .to_string(); - let invite_id = uuid::Uuid::new_v4(); + + let invite_id = match pending_invitations.get(&invitation.email) { + None => { + // user is not invited yet + let invite_id = uuid::Uuid::new_v4(); + insert_workspace_invitation( + &mut txn, + &invite_id, + workspace_id, + inviter, + invitation.email.as_str(), + invitation.role, + ) + .await?; + invite_id + }, + Some(inv) => { + tracing::warn!("User already invited: {}", invitation.email); + *inv + }, + }; + + // Generate a link such that when clicked, the user is added to the workspace. let accept_url = gotrue_client .admin_generate_link( &admin_token, @@ -237,17 +255,6 @@ pub async fn invite_workspace_members( .await? .action_link; - // Generate a link such that when clicked, the user is added to the workspace. - insert_workspace_invitation( - &mut txn, - &invite_id, - workspace_id, - inviter, - invitation.email.as_str(), - invitation.role, - ) - .await?; - // send email can be slow, so send email in background let cloned_mailer = mailer.clone(); tokio::spawn(async move { diff --git a/src/config/config.rs b/src/config/config.rs index 124ffb5b..ce61ea6f 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -23,6 +23,7 @@ pub struct Config { #[derive(serde::Deserialize, Clone, Debug)] pub struct MailerSetting { pub smtp_host: String, + pub smtp_port: u16, pub smtp_username: String, pub smtp_password: Secret, } @@ -92,21 +93,20 @@ pub struct DatabaseSetting { /// connections are reserved for system applications. /// When we exceed the limit of the database connection, then it shows an error message. pub max_connections: u32, - pub database_name: String, } impl Display for DatabaseSetting { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( - f, - "DatabaseSetting {{ pg_conn_opts: {:?}, require_ssl: {}, max_connections: {}, database_name: {} }}", - self.pg_conn_opts, self.require_ssl, self.max_connections, self.database_name - ) + f, + "DatabaseSetting {{ pg_conn_opts: {:?}, require_ssl: {}, max_connections: {} }}", + self.pg_conn_opts, self.require_ssl, self.max_connections + ) } } impl DatabaseSetting { - pub fn without_db(&self) -> PgConnectOptions { + pub fn pg_connect_options(&self) -> PgConnectOptions { let ssl_mode = if self.require_ssl { PgSslMode::Require } else { @@ -115,10 +115,6 @@ impl DatabaseSetting { let options = self.pg_conn_opts.clone(); options.ssl_mode(ssl_mode) } - - pub fn with_db(&self) -> PgConnectOptions { - self.without_db().database(&self.database_name) - } } #[derive(Clone, Debug)] @@ -143,7 +139,6 @@ pub fn get_configuration() -> Result { max_connections: get_env_var("APPFLOWY_DATABASE_MAX_CONNECTIONS", "40") .parse() .context("fail to get APPFLOWY_DATABASE_MAX_CONNECTIONS")?, - database_name: get_env_var("APPFLOWY_DATABASE_NAME", "postgres"), }, gotrue: GoTrueSetting { base_url: get_env_var("APPFLOWY_GOTRUE_BASE_URL", "http://localhost:9999"), @@ -184,6 +179,7 @@ pub fn get_configuration() -> Result { }, mailer: MailerSetting { smtp_host: get_env_var("APPFLOWY_MAILER_SMTP_HOST", "smtp.gmail.com"), + smtp_port: get_env_var("APPFLOWY_MAILER_SMTP_PORT", "465").parse()?, smtp_username: get_env_var("APPFLOWY_MAILER_SMTP_USERNAME", "sender@example.com"), smtp_password: get_env_var("APPFLOWY_MAILER_SMTP_PASSWORD", "password").into(), }, diff --git a/src/mailer.rs b/src/mailer.rs index b42a8439..c3976e58 100644 --- a/src/mailer.rs +++ b/src/mailer.rs @@ -1,6 +1,7 @@ use lettre::message::header::ContentType; use lettre::message::Message; use lettre::transport::smtp::authentication::Credentials; +use lettre::Address; use lettre::AsyncSmtpTransport; use lettre::AsyncTransport; use std::sync::Arc; @@ -14,6 +15,7 @@ lazy_static::lazy_static! { #[derive(Clone)] pub struct Mailer { smtp_transport: AsyncSmtpTransport, + smtp_username: String, } impl Mailer { @@ -21,10 +23,12 @@ impl Mailer { smtp_username: String, smtp_password: String, smtp_host: &str, + smtp_port: u16, ) -> Result { - let creds = Credentials::new(smtp_username, smtp_password); + let creds = Credentials::new(smtp_username.clone(), smtp_password); let smtp_transport = AsyncSmtpTransport::::relay(smtp_host)? .credentials(creds) + .port(smtp_port) .build(); let workspace_invite_template = @@ -36,7 +40,10 @@ impl Mailer { .register_template_string("workspace_invite", workspace_invite_template) .unwrap(); - Ok(Self { smtp_transport }) + Ok(Self { + smtp_transport, + smtp_username, + }) } pub async fn send_workspace_invite( @@ -51,8 +58,8 @@ impl Mailer { let email = Message::builder() .from(lettre::message::Mailbox::new( - Some("AppFlowy Notify".to_string()), - lettre::Address::new("notify", "appflowy.io")?, + Some("AppFlowy Notification".to_string()), + self.smtp_username.parse::
()?, )) .to(lettre::message::Mailbox::new( Some(param.username.clone()),