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_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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
Loading…
Reference in New Issue