From a65775d85b5522e29994be579c5a468b39486ebe Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 5 Jan 2022 03:15:51 +0900 Subject: [PATCH] Implement EmailSignup --- plume-models/src/email_signups.rs | 141 ++++++++++++++++++++++++++++++ plume-models/src/lib.rs | 2 + plume-models/src/users.rs | 16 ++++ 3 files changed, 159 insertions(+) create mode 100644 plume-models/src/email_signups.rs diff --git a/plume-models/src/email_signups.rs b/plume-models/src/email_signups.rs new file mode 100644 index 00000000..7d005060 --- /dev/null +++ b/plume-models/src/email_signups.rs @@ -0,0 +1,141 @@ +use crate::{ + db_conn::DbConn, + schema::email_signups, + users::{NewUser, Role, User}, + Error, Result, +}; +use chrono::{offset::Utc, Duration, NaiveDateTime}; +use diesel::{ + Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl, +}; +use plume_common::utils::random_hex; +use std::ops::Deref; + +const TOKEN_VALIDITY_HOURS: i64 = 2; + +pub struct Token(String); + +impl From for Token { + fn from(string: String) -> Self { + Token(string) + } +} + +impl From for String { + fn from(token: Token) -> Self { + token.0 + } +} + +impl Deref for Token { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Token { + fn generate() -> Self { + Self(random_hex()) + } +} + +#[derive(Identifiable, Queryable)] +pub struct EmailSignup { + pub id: i32, + pub email: String, + pub token: String, + pub expiration_date: NaiveDateTime, +} + +#[derive(Insertable)] +#[table_name = "email_signups"] +pub struct NewEmailSignup<'a> { + pub email: &'a str, + pub token: &'a str, + pub expiration_date: NaiveDateTime, +} + +impl EmailSignup { + pub fn start(conn: &DbConn, email: &str) -> Result { + conn.transaction(|| { + Self::ensure_user_not_exist_by_email(conn, email)?; + let _rows = Self::delete_existings_by_email(conn, email)?; + let token = Token::generate(); + let expiration_date = Utc::now() + .naive_utc() + .checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS)) + .expect("could not calculate expiration date"); + let new_signup = NewEmailSignup { + email, + token: &token, + expiration_date, + }; + let _rows = diesel::insert_into(email_signups::table) + .values(new_signup) + .execute(&**conn)?; + + Ok(token) + }) + } + + pub fn confirm(conn: &DbConn, token: &Token, email: &str) -> Result { + Self::ensure_user_not_exist_by_email(conn, email)?; + let signup: Self = email_signups::table + .filter(email_signups::token.eq(token.as_str())) + .first(&**conn) + .map_err(Error::from)?; + if signup.expired() { + Self::delete_existings_by_email(conn, email)?; + return Err(Error::Expired); + } + Ok(signup) + } + + pub fn complete( + &self, + conn: &DbConn, + username: String, + display_name: String, + summary: &str, + password: String, + ) -> Result { + Self::ensure_user_not_exist_by_email(conn, &self.email)?; + let user = NewUser::new_local( + conn, + username, + display_name, + Role::Normal, + summary, + self.email.clone(), + Some(password), + )?; + self.delete(conn)?; + Ok(user) + } + + fn delete(&self, conn: &DbConn) -> Result<()> { + let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?; + Ok(()) + } + + fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> { + if User::email_used(conn, email)? { + let _rows = Self::delete_existings_by_email(conn, email)?; + return Err(Error::UserAlreadyExists); + } + Ok(()) + } + + fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result { + let existing_signups = email_signups::table.filter(email_signups::email.eq(email)); + diesel::delete(existing_signups) + .execute(&**conn) + .map_err(Error::from) + } + + fn expired(&self) -> bool { + self.expiration_date < Utc::now().naive_utc() + } +} diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index 3913e63e..fd93a601 100644 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -67,6 +67,7 @@ pub enum Error { Url, Webfinger, Expired, + UserAlreadyExists, } impl From for Error { @@ -376,6 +377,7 @@ pub mod blogs; pub mod comment_seers; pub mod comments; pub mod db_conn; +pub mod email_signups; pub mod follows; pub mod headers; pub mod inbox; diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index 8c42701b..8244075e 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -202,6 +202,22 @@ impl User { } } + /** + * TODO: Should create user record with normalized(lowercased) email + */ + pub fn email_used(conn: &DbConn, email: &str) -> Result { + use diesel::dsl::{exists, select}; + + select(exists( + users::table + .filter(users::instance_id.eq(Instance::get_local()?.id)) + .filter(users::email.eq(email)) + .or_filter(users::email.eq(email.to_ascii_lowercase())), + )) + .get_result(&**conn) + .map_err(Error::from) + } + fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result { let link = resolve(acct.to_owned(), true)? .links