Send invitation mail, add accept invitation
This commit is contained in:
parent
da8fa8bebe
commit
4316f6888d
@ -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))]
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
alter table invitations drop COLUMN bind_token;
|
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE invitations ADD COLUMN bind_token UUID NOT NULL DEFAULT uuid_generate_v4();
|
@ -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;
|
||||||
|
|
||||||
|
61
jirs-server/src/mail/invite.rs
Normal file
61
jirs-server/src/mail/invite.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()) {
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user