Send invitation mail, add accept invitation

This commit is contained in:
Adrian Wozniak 2020-04-22 09:26:53 +02:00
parent da8fa8bebe
commit 4316f6888d
10 changed files with 160 additions and 8 deletions

View File

@ -478,6 +478,7 @@ pub struct Invitation {
pub invited_by_id: i32, pub invited_by_id: i32,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub bind_token: Uuid,
} }
#[cfg_attr(feature = "backend", derive(Queryable))] #[cfg_attr(feature = "backend", derive(Queryable))]

View File

@ -0,0 +1 @@
alter table invitations drop COLUMN bind_token;

View File

@ -0,0 +1 @@
ALTER TABLE invitations ADD COLUMN bind_token UUID NOT NULL DEFAULT uuid_generate_v4();

View File

@ -1,8 +1,9 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use jirs_data::{IssueAssignee, Project, User}; use jirs_data::{IssueAssignee, Project, User, UserId};
use crate::db::{DbExecutor, DbPooledConn}; use crate::db::{DbExecutor, DbPooledConn};
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
@ -162,6 +163,40 @@ impl Handler<Register> for DbExecutor {
} }
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct LoadInvitedUsers {
pub user_id: UserId,
}
impl Message for LoadInvitedUsers {
type Result = Result<Vec<User>, ServiceErrors>;
}
impl Handler<LoadInvitedUsers> for DbExecutor {
type Result = Result<Vec<User>, ServiceErrors>;
fn handle(&mut self, msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl as idsl;
use crate::schema::users::dsl as udsl;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = udsl::users
.inner_join(idsl::invitations.on(idsl::email.eq(udsl::email)))
.filter(idsl::invited_by_id.eq(msg.user_id))
.select(udsl::users::all_columns());
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
let res: Vec<User> = query
.load(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
Ok(res)
}
}
fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 { fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 {
use crate::schema::users::dsl; use crate::schema::users::dsl;

View File

@ -0,0 +1,61 @@
use actix::{Handler, Message};
use lettre;
use lettre_email;
use uuid::Uuid;
use crate::mail::MailExecutor;
#[derive(Debug)]
pub struct Invite {
pub bind_token: Uuid,
pub email: String,
pub inviter_name: String,
}
impl Message for Invite {
type Result = Result<(), String>;
}
impl Handler<Invite> for MailExecutor {
type Result = Result<(), String>;
fn handle(&mut self, msg: Invite, _ctx: &mut Self::Context) -> Self::Result {
use lettre::Transport;
let transport = &mut self.transport;
let from = self.config.from.as_str();
let addr = crate::web::Configuration::read().full_addr();
let html = format!(
r#"
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<h1>You have been invited to project by {inviter_name}!</h1>
<p>
</p>
<p>
Please click this link: <a href="{addr}/invite?token={bind_token}"></a>
</p>
</body>
</html>
"#,
bind_token = msg.bind_token,
inviter_name = msg.inviter_name,
addr = addr,
);
let email = lettre_email::Email::builder()
.from(from.clone())
.to(msg.email.as_str())
.html(html.as_str())
.subject("Invitation to JIRS project")
.build()
.map_err(|_| "Email is not valid".to_string())?;
transport
.send(email.into())
.and_then(|_| Ok(()))
.map_err(|e| format!("Mailer: {}", e))
}
}

View File

@ -4,6 +4,7 @@ use actix::{Actor, SyncContext};
use lettre; use lettre;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub mod invite;
pub mod welcome; pub mod welcome;
pub type MailTransport = lettre::SmtpTransport; pub type MailTransport = lettre::SmtpTransport;

View File

@ -103,6 +103,12 @@ table! {
/// ///
/// (Automatically generated by Diesel.) /// (Automatically generated by Diesel.)
updated_at -> Timestamp, updated_at -> Timestamp,
/// The `bind_token` column of the `invitations` table.
///
/// Its SQL type is `Uuid`.
///
/// (Automatically generated by Diesel.)
bind_token -> Uuid,
} }
} }

View File

@ -32,6 +32,12 @@ pub async fn user_from_request(
} }
} }
#[derive(Debug)]
pub enum Protocol {
Http,
Https,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Configuration { pub struct Configuration {
pub concurrency: usize, pub concurrency: usize,
@ -56,6 +62,25 @@ impl Configuration {
format!("{}:{}", self.bind, self.port) format!("{}:{}", self.bind, self.port)
} }
pub fn full_addr(&self) -> String {
match self.protocol() {
Protocol::Http => format!("http://{}", self.addr()),
Protocol::Https => format!("https://{}", self.addr()),
}
}
pub fn protocol(&self) -> Protocol {
if self.bind.as_str() == "0.0.0.0"
|| self.bind.as_str().starts_with("127.")
|| self.bind.as_str() == "localhost"
|| self.bind.as_str().ends_with(".lvh.me")
{
Protocol::Http
} else {
Protocol::Https
}
}
pub fn read() -> Self { pub fn read() -> Self {
let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); let contents: String = read_to_string(Self::config_file()).unwrap_or_default();
match toml::from_str(contents.as_str()) { match toml::from_str(contents.as_str()) {

View File

@ -28,21 +28,35 @@ pub struct CreateInvitation {
impl WsHandler<CreateInvitation> for WebSocketActor { impl WsHandler<CreateInvitation> for WebSocketActor {
fn handle_msg(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> WsResult {
let (user_id, project_id) = match self.current_user.as_ref().map(|u| (u.id, u.project_id)) { let (user_id, inviter_name, project_id) = match self
.current_user
.as_ref()
.map(|u| (u.id, u.name.clone(), u.project_id))
{
Some(id) => id, Some(id) => id,
_ => return Ok(None), _ => return Ok(None),
}; };
let CreateInvitation { email, name } = msg; let CreateInvitation { email, name } = msg;
let res = match block_on(self.db.send(invitations::CreateInvitation { let invitation = match block_on(self.db.send(invitations::CreateInvitation {
user_id, user_id,
project_id, project_id,
email, email,
name, name,
})) { })) {
Ok(Ok(_invitation)) => Some(WsMsg::InvitationSendSuccess), Ok(Ok(invitation)) => invitation,
_ => Some(WsMsg::InvitationSendFailure), _ => return Ok(Some(WsMsg::InvitationSendFailure)),
}; };
Ok(res) match block_on(self.mail.send(crate::mail::invite::Invite {
bind_token: invitation.bind_token.clone(),
email: invitation.email.clone(),
inviter_name,
})) {
Ok(Ok(_)) => (),
_ => return Ok(Some(WsMsg::InvitationSendFailure)),
}
Ok(Some(WsMsg::InvitationSendSuccess))
} }
} }

View File

@ -51,8 +51,15 @@ impl WsHandler<Register> for WebSocketActor {
pub struct LoadInvitedUsers; pub struct LoadInvitedUsers;
impl WsHandler<LoadInvitedUsers> for WebSockerActor { impl WsHandler<LoadInvitedUsers> for WebSocketActor {
fn handle_msg(&mut self, msg: LoadInvitedUsers, _ctx: &mut _) -> WsResult { fn handle_msg(&mut self, _msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
let users = match block_on(self.db.send(crate::db::users::LoadInvitedUsers { user_id })) {
Ok(Ok(users)) => users,
_ => return Ok(None),
};
Ok(Some(WsMsg::InvitedUsersLoaded(users)))
} }
} }