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
This commit is contained in:
Zack 2024-05-18 08:33:08 +08:00 committed by GitHub
parent 2736fa60a7
commit ec7eb54bfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 83 additions and 59 deletions

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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

View File

@ -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<HashSet<String>, AppError> {
let invitee_emails = sqlx::query_scalar!(
) -> Result<HashMap<String, Uuid>, 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::<HashMap<String, Uuid>>();
Ok(inv_id_by_email)
}
#[inline]

View File

@ -252,6 +252,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
config.mailer.smtp_username.clone(),
config.mailer.smtp_password.expose_secret().clone(),
&config.mailer.smtp_host,
config.mailer.smtp_port,
)
.await?;
let realtime_shared_state = RealtimeSharedState::new(redis_conn_manager.clone());
@ -405,7 +406,7 @@ async fn get_connection_pool(setting: &DatabaseSetting) -> Result<PgPool, Error>
.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))
}

View File

@ -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 {

View File

@ -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<String>,
}
@ -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<Config, anyhow::Error> {
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<Config, anyhow::Error> {
},
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(),
},

View File

@ -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<lettre::Tokio1Executor>,
smtp_username: String,
}
impl Mailer {
@ -21,10 +23,12 @@ impl Mailer {
smtp_username: String,
smtp_password: String,
smtp_host: &str,
smtp_port: u16,
) -> Result<Self, anyhow::Error> {
let creds = Credentials::new(smtp_username, smtp_password);
let creds = Credentials::new(smtp_username.clone(), smtp_password);
let smtp_transport = AsyncSmtpTransport::<lettre::Tokio1Executor>::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::<Address>()?,
))
.to(lettre::message::Mailbox::new(
Some(param.username.clone()),