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 created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub bind_token: Uuid,
|
||||
}
|
||||
|
||||
#[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 diesel::pg::Pg;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use jirs_data::{IssueAssignee, Project, User};
|
||||
use jirs_data::{IssueAssignee, Project, User, UserId};
|
||||
|
||||
use crate::db::{DbExecutor, DbPooledConn};
|
||||
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 {
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod invite;
|
||||
pub mod welcome;
|
||||
|
||||
pub type MailTransport = lettre::SmtpTransport;
|
||||
|
@ -103,6 +103,12 @@ table! {
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
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)]
|
||||
pub struct Configuration {
|
||||
pub concurrency: usize,
|
||||
@ -56,6 +62,25 @@ impl Configuration {
|
||||
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 {
|
||||
let contents: String = read_to_string(Self::config_file()).unwrap_or_default();
|
||||
match toml::from_str(contents.as_str()) {
|
||||
|
@ -28,21 +28,35 @@ pub struct CreateInvitation {
|
||||
|
||||
impl WsHandler<CreateInvitation> for WebSocketActor {
|
||||
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,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
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,
|
||||
project_id,
|
||||
email,
|
||||
name,
|
||||
})) {
|
||||
Ok(Ok(_invitation)) => Some(WsMsg::InvitationSendSuccess),
|
||||
_ => Some(WsMsg::InvitationSendFailure),
|
||||
Ok(Ok(invitation)) => invitation,
|
||||
_ => 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;
|
||||
|
||||
impl WsHandler<LoadInvitedUsers> for WebSockerActor {
|
||||
fn handle_msg(&mut self, msg: LoadInvitedUsers, _ctx: &mut _) -> WsResult {
|
||||
impl WsHandler<LoadInvitedUsers> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> WsResult {
|
||||
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