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:
parent
2736fa60a7
commit
ec7eb54bfc
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -83,6 +83,7 @@ APPFLOWY_S3_BUCKET=appflowy
|
||||||
|
|
||||||
# AppFlowy Cloud Mailer
|
# AppFlowy Cloud Mailer
|
||||||
APPFLOWY_MAILER_SMTP_HOST=smtp.gmail.com
|
APPFLOWY_MAILER_SMTP_HOST=smtp.gmail.com
|
||||||
|
APPFLOWY_MAILER_SMTP_PORT=465
|
||||||
APPFLOWY_MAILER_SMTP_USERNAME=email_sender@some_company.com
|
APPFLOWY_MAILER_SMTP_USERNAME=email_sender@some_company.com
|
||||||
APPFLOWY_MAILER_SMTP_PASSWORD=email_sender_password
|
APPFLOWY_MAILER_SMTP_PASSWORD=email_sender_password
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use sqlx::{
|
||||||
types::{uuid, Uuid},
|
types::{uuid, Uuid},
|
||||||
Executor, PgPool, Postgres, Transaction,
|
Executor, PgPool, Postgres, Transaction,
|
||||||
};
|
};
|
||||||
use std::{collections::HashSet, ops::DerefMut};
|
use std::{collections::HashMap, ops::DerefMut};
|
||||||
use tracing::{event, instrument};
|
use tracing::{event, instrument};
|
||||||
|
|
||||||
use crate::pg_row::AFWorkspaceMemberPermRow;
|
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(
|
pub async fn select_workspace_pending_invitations(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
workspace_id: &Uuid,
|
workspace_id: &Uuid,
|
||||||
) -> Result<HashSet<String>, AppError> {
|
) -> Result<HashMap<String, Uuid>, AppError> {
|
||||||
let invitee_emails = sqlx::query_scalar!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT invitee_email
|
SELECT id, invitee_email
|
||||||
FROM public.af_workspace_invitation
|
FROM public.af_workspace_invitation
|
||||||
WHERE workspace_id = $1
|
WHERE workspace_id = $1
|
||||||
AND status = 0
|
AND status = 0
|
||||||
|
|
@ -723,7 +723,13 @@ pub async fn select_workspace_pending_invitations(
|
||||||
)
|
)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.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]
|
#[inline]
|
||||||
|
|
|
||||||
|
|
@ -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_username.clone(),
|
||||||
config.mailer.smtp_password.expose_secret().clone(),
|
config.mailer.smtp_password.expose_secret().clone(),
|
||||||
&config.mailer.smtp_host,
|
&config.mailer.smtp_host,
|
||||||
|
config.mailer.smtp_port,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let realtime_shared_state = RealtimeSharedState::new(redis_conn_manager.clone());
|
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))
|
.acquire_timeout(Duration::from_secs(10))
|
||||||
.max_lifetime(Duration::from_secs(30 * 60))
|
.max_lifetime(Duration::from_secs(30 * 60))
|
||||||
.idle_timeout(Duration::from_secs(30))
|
.idle_timeout(Duration::from_secs(30))
|
||||||
.connect_with(setting.with_db())
|
.connect_with(setting.pg_connect_options())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to connect to postgres database: {}", e))
|
.map_err(|e| anyhow::anyhow!("Failed to connect to postgres database: {}", e))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,10 +201,6 @@ pub async fn invite_workspace_members(
|
||||||
tracing::warn!("User already in workspace: {}", invitation.email);
|
tracing::warn!("User already in workspace: {}", invitation.email);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if pending_invitations.contains(&invitation.email) {
|
|
||||||
tracing::warn!("User already invited: {}", invitation.email);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let inviter_name = inviter_name.clone();
|
let inviter_name = inviter_name.clone();
|
||||||
let workspace_name = workspace_name.clone();
|
let workspace_name = workspace_name.clone();
|
||||||
|
|
@ -216,7 +212,29 @@ pub async fn invite_workspace_members(
|
||||||
let user_icon_url =
|
let user_icon_url =
|
||||||
"https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png"
|
"https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png"
|
||||||
.to_string();
|
.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
|
let accept_url = gotrue_client
|
||||||
.admin_generate_link(
|
.admin_generate_link(
|
||||||
&admin_token,
|
&admin_token,
|
||||||
|
|
@ -237,17 +255,6 @@ pub async fn invite_workspace_members(
|
||||||
.await?
|
.await?
|
||||||
.action_link;
|
.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
|
// send email can be slow, so send email in background
|
||||||
let cloned_mailer = mailer.clone();
|
let cloned_mailer = mailer.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ pub struct Config {
|
||||||
#[derive(serde::Deserialize, Clone, Debug)]
|
#[derive(serde::Deserialize, Clone, Debug)]
|
||||||
pub struct MailerSetting {
|
pub struct MailerSetting {
|
||||||
pub smtp_host: String,
|
pub smtp_host: String,
|
||||||
|
pub smtp_port: u16,
|
||||||
pub smtp_username: String,
|
pub smtp_username: String,
|
||||||
pub smtp_password: Secret<String>,
|
pub smtp_password: Secret<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -92,21 +93,20 @@ pub struct DatabaseSetting {
|
||||||
/// connections are reserved for system applications.
|
/// connections are reserved for system applications.
|
||||||
/// When we exceed the limit of the database connection, then it shows an error message.
|
/// When we exceed the limit of the database connection, then it shows an error message.
|
||||||
pub max_connections: u32,
|
pub max_connections: u32,
|
||||||
pub database_name: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for DatabaseSetting {
|
impl Display for DatabaseSetting {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"DatabaseSetting {{ pg_conn_opts: {:?}, require_ssl: {}, max_connections: {}, database_name: {} }}",
|
"DatabaseSetting {{ pg_conn_opts: {:?}, require_ssl: {}, max_connections: {} }}",
|
||||||
self.pg_conn_opts, self.require_ssl, self.max_connections, self.database_name
|
self.pg_conn_opts, self.require_ssl, self.max_connections
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DatabaseSetting {
|
impl DatabaseSetting {
|
||||||
pub fn without_db(&self) -> PgConnectOptions {
|
pub fn pg_connect_options(&self) -> PgConnectOptions {
|
||||||
let ssl_mode = if self.require_ssl {
|
let ssl_mode = if self.require_ssl {
|
||||||
PgSslMode::Require
|
PgSslMode::Require
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -115,10 +115,6 @@ impl DatabaseSetting {
|
||||||
let options = self.pg_conn_opts.clone();
|
let options = self.pg_conn_opts.clone();
|
||||||
options.ssl_mode(ssl_mode)
|
options.ssl_mode(ssl_mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_db(&self) -> PgConnectOptions {
|
|
||||||
self.without_db().database(&self.database_name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[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")
|
max_connections: get_env_var("APPFLOWY_DATABASE_MAX_CONNECTIONS", "40")
|
||||||
.parse()
|
.parse()
|
||||||
.context("fail to get APPFLOWY_DATABASE_MAX_CONNECTIONS")?,
|
.context("fail to get APPFLOWY_DATABASE_MAX_CONNECTIONS")?,
|
||||||
database_name: get_env_var("APPFLOWY_DATABASE_NAME", "postgres"),
|
|
||||||
},
|
},
|
||||||
gotrue: GoTrueSetting {
|
gotrue: GoTrueSetting {
|
||||||
base_url: get_env_var("APPFLOWY_GOTRUE_BASE_URL", "http://localhost:9999"),
|
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 {
|
mailer: MailerSetting {
|
||||||
smtp_host: get_env_var("APPFLOWY_MAILER_SMTP_HOST", "smtp.gmail.com"),
|
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_username: get_env_var("APPFLOWY_MAILER_SMTP_USERNAME", "sender@example.com"),
|
||||||
smtp_password: get_env_var("APPFLOWY_MAILER_SMTP_PASSWORD", "password").into(),
|
smtp_password: get_env_var("APPFLOWY_MAILER_SMTP_PASSWORD", "password").into(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use lettre::message::header::ContentType;
|
use lettre::message::header::ContentType;
|
||||||
use lettre::message::Message;
|
use lettre::message::Message;
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
use lettre::Address;
|
||||||
use lettre::AsyncSmtpTransport;
|
use lettre::AsyncSmtpTransport;
|
||||||
use lettre::AsyncTransport;
|
use lettre::AsyncTransport;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -14,6 +15,7 @@ lazy_static::lazy_static! {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Mailer {
|
pub struct Mailer {
|
||||||
smtp_transport: AsyncSmtpTransport<lettre::Tokio1Executor>,
|
smtp_transport: AsyncSmtpTransport<lettre::Tokio1Executor>,
|
||||||
|
smtp_username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mailer {
|
impl Mailer {
|
||||||
|
|
@ -21,10 +23,12 @@ impl Mailer {
|
||||||
smtp_username: String,
|
smtp_username: String,
|
||||||
smtp_password: String,
|
smtp_password: String,
|
||||||
smtp_host: &str,
|
smtp_host: &str,
|
||||||
|
smtp_port: u16,
|
||||||
) -> Result<Self, anyhow::Error> {
|
) -> 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)?
|
let smtp_transport = AsyncSmtpTransport::<lettre::Tokio1Executor>::relay(smtp_host)?
|
||||||
.credentials(creds)
|
.credentials(creds)
|
||||||
|
.port(smtp_port)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let workspace_invite_template =
|
let workspace_invite_template =
|
||||||
|
|
@ -36,7 +40,10 @@ impl Mailer {
|
||||||
.register_template_string("workspace_invite", workspace_invite_template)
|
.register_template_string("workspace_invite", workspace_invite_template)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Ok(Self { smtp_transport })
|
Ok(Self {
|
||||||
|
smtp_transport,
|
||||||
|
smtp_username,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_workspace_invite(
|
pub async fn send_workspace_invite(
|
||||||
|
|
@ -51,8 +58,8 @@ impl Mailer {
|
||||||
|
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from(lettre::message::Mailbox::new(
|
.from(lettre::message::Mailbox::new(
|
||||||
Some("AppFlowy Notify".to_string()),
|
Some("AppFlowy Notification".to_string()),
|
||||||
lettre::Address::new("notify", "appflowy.io")?,
|
self.smtp_username.parse::<Address>()?,
|
||||||
))
|
))
|
||||||
.to(lettre::message::Mailbox::new(
|
.to(lettre::message::Mailbox::new(
|
||||||
Some(param.username.clone()),
|
Some(param.username.clone()),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue