From bd1e8be32811609fe6df452767ef2e4d542d4508 Mon Sep 17 00:00:00 2001 From: tomuta Date: Sun, 24 Nov 2019 22:28:49 -0700 Subject: [PATCH] Implement change-email, email-verification, account-recovery, and welcome notifications --- .env.template | 18 +- .../down.sql | 1 + .../up.sql | 5 + .../down.sql | 1 + .../up.sql | 5 + .../down.sql | 1 + .../up.sql | 5 + src/api/core/accounts.rs | 175 +++++++++++++++++- src/api/core/two_factor/email.rs | 20 +- src/api/identity.rs | 29 +++ src/auth.rs | 58 ++++++ src/config.rs | 11 ++ src/crypto.rs | 16 ++ src/db/models/device.rs | 3 +- src/db/models/user.rs | 12 +- src/db/schemas/mysql/schema.rs | 5 + src/db/schemas/postgresql/schema.rs | 7 +- src/db/schemas/sqlite/schema.rs | 5 + src/mail.rs | 81 +++++++- src/static/templates/email/change_email.hbs | 6 + .../templates/email/change_email.html.hbs | 129 +++++++++++++ src/static/templates/email/delete_account.hbs | 12 ++ .../templates/email/delete_account.html.hbs | 137 ++++++++++++++ src/static/templates/email/pw_hint_none.hbs | 8 +- .../templates/email/pw_hint_none.html.hbs | 5 + src/static/templates/email/pw_hint_some.hbs | 2 + .../templates/email/pw_hint_some.html.hbs | 5 + src/static/templates/email/verify_email.hbs | 12 ++ .../templates/email/verify_email.html.hbs | 137 ++++++++++++++ src/static/templates/email/welcome.hbs | 8 + src/static/templates/email/welcome.html.hbs | 129 +++++++++++++ .../templates/email/welcome_must_verify.hbs | 12 ++ .../email/welcome_must_verify.html.hbs | 137 ++++++++++++++ 33 files changed, 1164 insertions(+), 33 deletions(-) create mode 100644 migrations/mysql/2019-11-17-011009_add_email_verification/down.sql create mode 100644 migrations/mysql/2019-11-17-011009_add_email_verification/up.sql create mode 100644 migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql create mode 100644 migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql create mode 100644 migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql create mode 100644 migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql create mode 100644 src/static/templates/email/change_email.hbs create mode 100644 src/static/templates/email/change_email.html.hbs create mode 100644 src/static/templates/email/delete_account.hbs create mode 100644 src/static/templates/email/delete_account.html.hbs create mode 100644 src/static/templates/email/verify_email.hbs create mode 100644 src/static/templates/email/verify_email.html.hbs create mode 100644 src/static/templates/email/welcome.hbs create mode 100644 src/static/templates/email/welcome.html.hbs create mode 100644 src/static/templates/email/welcome_must_verify.hbs create mode 100644 src/static/templates/email/welcome_must_verify.html.hbs diff --git a/.env.template b/.env.template index 7c0200d..3ccdbdb 100644 --- a/.env.template +++ b/.env.template @@ -95,12 +95,22 @@ ## Controls if new users can register # SIGNUPS_ALLOWED=true +## Controls if new users need to verify their email address upon registration +## Note that setting this option to true prevents logins until the email address has been verified! +## The welcome email will include a verification link, and login attempts will periodically +## trigger another verification email to be sent. +# SIGNUPS_VERIFY=false + +## If SIGNUPS_VERIFY is set to true, this limits how many seconds after the last time +## an email verification link has been sent another verification email will be sent +# SIGNUPS_VERIFY_RESEND_TIME=3600 + +## If SIGNUPS_VERIFY is set to true, this limits how many times an email verification +## email will be re-sent upon an attempted login. +# SIGNUPS_VERIFY_RESEND_LIMIT=6 + ## Controls if new users from a list of comma-separated domains can register ## even if SIGNUPS_ALLOWED is set to false -## -## WARNING: There is currently no validation that prevents anyone from -## signing up with any made-up email address from one of these -## whitelisted domains! # SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org ## Token for the admin interface, preferably use a long random string diff --git a/migrations/mysql/2019-11-17-011009_add_email_verification/down.sql b/migrations/mysql/2019-11-17-011009_add_email_verification/down.sql new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/migrations/mysql/2019-11-17-011009_add_email_verification/down.sql @@ -0,0 +1 @@ + diff --git a/migrations/mysql/2019-11-17-011009_add_email_verification/up.sql b/migrations/mysql/2019-11-17-011009_add_email_verification/up.sql new file mode 100644 index 0000000..0195776 --- /dev/null +++ b/migrations/mysql/2019-11-17-011009_add_email_verification/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL; +ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL; +ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL; +ALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL; diff --git a/migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql b/migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql @@ -0,0 +1 @@ + diff --git a/migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql b/migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql new file mode 100644 index 0000000..1a1c55d --- /dev/null +++ b/migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users ADD COLUMN verified_at TIMESTAMP DEFAULT NULL; +ALTER TABLE users ADD COLUMN last_verifying_at TIMESTAMP DEFAULT NULL; +ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL; +ALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL; diff --git a/migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql b/migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql @@ -0,0 +1 @@ + diff --git a/migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql b/migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql new file mode 100644 index 0000000..aa3b675 --- /dev/null +++ b/migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL; +ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL; +ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN email_new TEXT DEFAULT NULL; +ALTER TABLE users ADD COLUMN email_new_token TEXT DEFAULT NULL; diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index b91e724..489854d 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1,11 +1,13 @@ use rocket_contrib::json::Json; +use chrono::Utc; use crate::db::models::*; use crate::db::DbConn; use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType}; -use crate::auth::{decode_invite, Headers}; +use crate::auth::{decode_invite, decode_delete, decode_verify_email, Headers}; use crate::mail; +use crate::crypto; use crate::CONFIG; @@ -25,6 +27,10 @@ pub fn routes() -> Vec { post_sstamp, post_email_token, post_email, + post_verify_email, + post_verify_email_token, + post_delete_recover, + post_delete_recover_token, delete_account, post_delete_account, revision_date, @@ -126,6 +132,20 @@ fn register(data: JsonUpcase, conn: DbConn) -> EmptyResult { user.public_key = Some(keys.PublicKey); } + if CONFIG.mail_enabled() { + if CONFIG.signups_verify() { + if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid) { + error!("Error sending welcome email: {:#?}", e); + } + + user.last_verifying_at = Some(user.created_at); + } else { + if let Err(e) = mail::send_welcome(&user.email) { + error!("Error sending welcome email: {:#?}", e); + } + } + } + user.save(&conn) } @@ -341,8 +361,9 @@ struct EmailTokenData { #[post("/accounts/email-token", data = "")] fn post_email_token(data: JsonUpcase, headers: Headers, conn: DbConn) -> EmptyResult { let data: EmailTokenData = data.into_inner().data; + let mut user = headers.user; - if !headers.user.check_valid_password(&data.MasterPasswordHash) { + if !user.check_valid_password(&data.MasterPasswordHash) { err!("Invalid password") } @@ -350,7 +371,21 @@ fn post_email_token(data: JsonUpcase, headers: Headers, conn: Db err!("Email already in use"); } - Ok(()) + if !CONFIG.signups_allowed() && !CONFIG.can_signup_user(&data.NewEmail) { + err!("Email cannot be changed to this address"); + } + + let token = crypto::generate_token(6)?; + + if CONFIG.mail_enabled() { + if let Err(e) = mail::send_change_email(&data.NewEmail, &token) { + error!("Error sending change-email email: {:#?}", e); + } + } + + user.email_new = Some(data.NewEmail); + user.email_new_token = Some(token); + user.save(&conn) } #[derive(Deserialize)] @@ -361,8 +396,7 @@ struct ChangeEmailData { Key: String, NewMasterPasswordHash: String, - #[serde(rename = "Token")] - _Token: NumberOrString, + Token: NumberOrString, } #[post("/accounts/email", data = "")] @@ -378,7 +412,32 @@ fn post_email(data: JsonUpcase, headers: Headers, conn: DbConn) err!("Email already in use"); } + match user.email_new { + Some(ref val) => { + if *val != data.NewEmail.to_string() { + err!("Email change mismatch"); + } + }, + None => err!("No email change pending"), + } + + if CONFIG.mail_enabled() { + // Only check the token if we sent out an email... + match user.email_new_token { + Some(ref val) => + if *val != data.Token.into_string() { + err!("Token mismatch"); + } + None => err!("No email change pending"), + } + user.verified_at = Some(Utc::now().naive_utc()); + } else { + user.verified_at = None; + } + user.email = data.NewEmail; + user.email_new = None; + user.email_new_token = None; user.set_password(&data.NewMasterPasswordHash); user.akey = data.Key; @@ -386,6 +445,112 @@ fn post_email(data: JsonUpcase, headers: Headers, conn: DbConn) user.save(&conn) } +#[post("/accounts/verify-email")] +fn post_verify_email(headers: Headers, _conn: DbConn) -> EmptyResult { + let user = headers.user; + + if !CONFIG.mail_enabled() { + err!("Cannot verify email address"); + } + + if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) { + error!("Error sending delete account email: {:#?}", e); + } + + Ok(()) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct VerifyEmailTokenData { + UserId: String, + Token: String, +} + +#[post("/accounts/verify-email-token", data = "")] +fn post_verify_email_token(data: JsonUpcase, conn: DbConn) -> EmptyResult { + let data: VerifyEmailTokenData = data.into_inner().data; + + let mut user = match User::find_by_uuid(&data.UserId, &conn) { + Some(user) => user, + None => err!("User doesn't exist"), + }; + + let claims = match decode_verify_email(&data.Token) { + Ok(claims) => claims, + Err(_) => err!("Invalid claim"), + }; + + if claims.sub != user.uuid { + err!("Invalid claim"); + } + + user.verified_at = Some(Utc::now().naive_utc()); + user.last_verifying_at = None; + user.login_verify_count = 0; + if let Err(e) = user.save(&conn) { + error!("Error saving email verification: {:#?}", e); + } + + Ok(()) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct DeleteRecoverData { + Email: String, +} + +#[post("/accounts/delete-recover", data="")] +fn post_delete_recover(data: JsonUpcase, conn: DbConn) -> EmptyResult { + let data: DeleteRecoverData = data.into_inner().data; + + let user = User::find_by_mail(&data.Email, &conn); + + if CONFIG.mail_enabled() { + if let Some(user) = user { + if let Err(e) = mail::send_delete_account(&user.email, &user.uuid) { + error!("Error sending delete account email: {:#?}", e); + } + } + Ok(()) + } else { + // We don't support sending emails, but we shouldn't allow anybody + // to delete accounts without at least logging in... And if the user + // cannot remember their password then they will need to contact + // the administrator to delete it... + err!("Please contact the administrator to delete your account"); + } +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct DeleteRecoverTokenData { + UserId: String, + Token: String, +} + +#[post("/accounts/delete-recover-token", data="")] +fn post_delete_recover_token(data: JsonUpcase, conn: DbConn) -> EmptyResult { + let data: DeleteRecoverTokenData = data.into_inner().data; + + let user = match User::find_by_uuid(&data.UserId, &conn) { + Some(user) => user, + None => err!("User doesn't exist"), + }; + + let claims = match decode_delete(&data.Token) { + Ok(claims) => claims, + Err(_) => err!("Invalid claim"), + }; + + if claims.sub != user.uuid { + err!("Invalid claim"); + } + + user.delete(&conn) +} + #[post("/accounts/delete", data = "")] fn post_delete_account(data: JsonUpcase, headers: Headers, conn: DbConn) -> EmptyResult { delete_account(data, headers, conn) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 654d239..0a78ca2 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -66,7 +66,7 @@ pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult { let type_ = TwoFactorType::Email as i32; let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, &conn)?; - let generated_token = generate_token(CONFIG.email_token_size())?; + let generated_token = crypto::generate_token(CONFIG.email_token_size())?; let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?; twofactor_data.set_token(generated_token); @@ -109,22 +109,6 @@ struct SendEmailData { MasterPasswordHash: String, } - -fn generate_token(token_size: u32) -> Result { - if token_size > 19 { - err!("Generating token failed") - } - - // 8 bytes to create an u64 for up to 19 token digits - let bytes = crypto::get_random(vec![0; 8]); - let mut bytes_array = [0u8; 8]; - bytes_array.copy_from_slice(&bytes); - - let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size); - let token = format!("{:0size$}", number, size = token_size as usize); - Ok(token) -} - /// Send a verification email to the specified email address to check whether it exists/belongs to user. #[post("/two-factor/send-email", data = "")] fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) -> EmptyResult { @@ -145,7 +129,7 @@ fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) - tf.delete(&conn)?; } - let generated_token = generate_token(CONFIG.email_token_size())?; + let generated_token = crypto::generate_token(CONFIG.email_token_size())?; let twofactor_data = EmailTokenData::new(data.Email, generated_token); // Uses EmailVerificationChallenge as type to show that it's not verified yet. diff --git a/src/api/identity.rs b/src/api/identity.rs index 7a2ef13..ad475e7 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -3,6 +3,7 @@ use rocket::request::{Form, FormItems, FromForm}; use rocket::Route; use rocket_contrib::json::Json; use serde_json::Value; +use chrono::Utc; use crate::api::core::two_factor::email::EmailTokenData; use crate::api::core::two_factor::{duo, email, yubikey}; @@ -96,6 +97,34 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult ) } + if !user.verified_at.is_some() && CONFIG.mail_enabled() && CONFIG.signups_verify() { + let now = Utc::now().naive_utc(); + if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 { + let resend_limit = CONFIG.signups_verify_resend_limit() as i32; + if resend_limit == 0 || user.login_verify_count < resend_limit { + // We want to send another email verification if we require signups to verify + // their email address, and we haven't sent them a reminder in a while... + let mut user = user; + user.last_verifying_at = Some(now); + user.login_verify_count = user.login_verify_count + 1; + + if let Err(e) = user.save(&conn) { + error!("Error updating user: {:#?}", e); + } + + if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) { + error!("Error auto-sending email verification email: {:#?}", e); + } + } + } + + // We still want the login to fail until they actually verified the email address + err!( + "Please verify your email before trying again.", + format!("IP: {}. Username: {}.", ip.ip, username) + ) + } + let (mut device, new_device) = get_device(&data, &conn, &user); let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?; diff --git a/src/auth.rs b/src/auth.rs index 8a55ba5..cd19e97 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -18,6 +18,8 @@ lazy_static! { static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM); pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain()); pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain()); + pub static ref JWT_DELETE_ISSUER: String = format!("{}|delete", CONFIG.domain()); + pub static ref JWT_VERIFYEMAIL_ISSUER: String = format!("{}|verifyemail", CONFIG.domain()); pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain()); static ref PRIVATE_RSA_KEY: Vec = match read_file(&CONFIG.private_rsa_key()) { Ok(key) => key, @@ -62,6 +64,14 @@ pub fn decode_invite(token: &str) -> Result { decode_jwt(token, JWT_INVITE_ISSUER.to_string()) } +pub fn decode_delete(token: &str) -> Result { + decode_jwt(token, JWT_DELETE_ISSUER.to_string()) +} + +pub fn decode_verify_email(token: &str) -> Result { + decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string()) +} + pub fn decode_admin(token: &str) -> Result { decode_jwt(token, JWT_ADMIN_ISSUER.to_string()) } @@ -134,6 +144,54 @@ pub fn generate_invite_claims( } } +#[derive(Debug, Serialize, Deserialize)] +pub struct DeleteJWTClaims { + // Not before + pub nbf: i64, + // Expiration time + pub exp: i64, + // Issuer + pub iss: String, + // Subject + pub sub: String, +} + +pub fn generate_delete_claims( + uuid: String, +) -> DeleteJWTClaims { + let time_now = Utc::now().naive_utc(); + DeleteJWTClaims { + nbf: time_now.timestamp(), + exp: (time_now + Duration::days(5)).timestamp(), + iss: JWT_DELETE_ISSUER.to_string(), + sub: uuid, + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VerifyEmailJWTClaims { + // Not before + pub nbf: i64, + // Expiration time + pub exp: i64, + // Issuer + pub iss: String, + // Subject + pub sub: String, +} + +pub fn generate_verify_email_claims( + uuid: String, +) -> DeleteJWTClaims { + let time_now = Utc::now().naive_utc(); + DeleteJWTClaims { + nbf: time_now.timestamp(), + exp: (time_now + Duration::days(5)).timestamp(), + iss: JWT_VERIFYEMAIL_ISSUER.to_string(), + sub: uuid, + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct AdminJWTClaims { // Not before diff --git a/src/config.rs b/src/config.rs index f75b9e6..4cccbb1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -243,6 +243,12 @@ make_config! { disable_icon_download: bool, true, def, false; /// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited signups_allowed: bool, true, def, true; + /// Require email verification on signups. This will prevent logins from succeeding until the address has been verified + signups_verify: bool, true, def, false; + /// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds) + signups_verify_resend_time: u64, true, def, 3_600; + /// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit) + signups_verify_resend_limit: u32, true, def, 6; /// Allow signups only from this list of comma-separated domains signups_domains_whitelist: String, true, def, "".to_string(); /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled @@ -595,6 +601,8 @@ fn load_templates(path: &str) -> Handlebars { } // First register default templates here + reg!("email/change_email", ".html"); + reg!("email/delete_account", ".html"); reg!("email/invite_accepted", ".html"); reg!("email/invite_confirmed", ".html"); reg!("email/new_device_logged_in", ".html"); @@ -602,6 +610,9 @@ fn load_templates(path: &str) -> Handlebars { reg!("email/pw_hint_some", ".html"); reg!("email/send_org_invite", ".html"); reg!("email/twofactor_email", ".html"); + reg!("email/verify_email", ".html"); + reg!("email/welcome", ".html"); + reg!("email/welcome_must_verify", ".html"); reg!("admin/base"); reg!("admin/login"); diff --git a/src/crypto.rs b/src/crypto.rs index d8cf446..dd3ed31 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -4,6 +4,7 @@ use ring::{digest, hmac, pbkdf2}; use std::num::NonZeroU32; +use crate::error::Error; static DIGEST_ALG: &digest::Algorithm = &digest::SHA256; const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN; @@ -52,6 +53,21 @@ pub fn get_random(mut array: Vec) -> Vec { array } +pub fn generate_token(token_size: u32) -> Result { + if token_size > 19 { + err!("Generating token failed") + } + + // 8 bytes to create an u64 for up to 19 token digits + let bytes = get_random(vec![0; 8]); + let mut bytes_array = [0u8; 8]; + bytes_array.copy_from_slice(&bytes); + + let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size); + let token = format!("{:0size$}", number, size = token_size as usize); + Ok(token) +} + // // Constant time compare // diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 9f8506e..4fa91fa 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,6 +1,7 @@ use chrono::{NaiveDateTime, Utc}; use super::User; +use crate::CONFIG; #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "devices"] @@ -87,7 +88,7 @@ impl Device { premium: true, name: user.name.to_string(), email: user.email.to_string(), - email_verified: true, + email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(), orgowner, orgadmin, diff --git a/src/db/models/user.rs b/src/db/models/user.rs index f35739e..9646ae5 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -11,8 +11,13 @@ pub struct User { pub uuid: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub verified_at: Option, + pub last_verifying_at: Option, + pub login_verify_count: i32, pub email: String, + pub email_new: Option, + pub email_new_token: Option, pub name: String, pub password_hash: Vec, @@ -56,9 +61,14 @@ impl User { uuid: crate::util::get_uuid(), created_at: now, updated_at: now, + verified_at: None, + last_verifying_at: None, + login_verify_count: 0, name: email.clone(), email, akey: String::new(), + email_new: None, + email_new_token: None, password_hash: Vec::new(), salt: crypto::get_random_64(), @@ -135,7 +145,7 @@ impl User { "Id": self.uuid, "Name": self.name, "Email": self.email, - "EmailVerified": true, + "EmailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(), "Premium": true, "MasterPasswordHint": self.password_hint, "Culture": "en-US", diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index 2e70511..36165b6 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -101,7 +101,12 @@ table! { uuid -> Varchar, created_at -> Datetime, updated_at -> Datetime, + verified_at -> Nullable, + last_verifying_at -> Nullable, + login_verify_count -> Integer, email -> Varchar, + email_new -> Nullable, + email_new_token -> Nullable, name -> Text, password_hash -> Blob, salt -> Blob, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 0112c59..a683212 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -101,7 +101,12 @@ table! { uuid -> Text, created_at -> Timestamp, updated_at -> Timestamp, + verified_at -> Nullable, + last_verifying_at -> Nullable, + login_verify_count -> Integer, email -> Text, + email_new -> Nullable, + email_new_token -> Nullable, name -> Text, password_hash -> Binary, salt -> Binary, @@ -170,4 +175,4 @@ allow_tables_to_appear_in_same_query!( users, users_collections, users_organizations, -); \ No newline at end of file +); diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 43abe5c..a683212 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -101,7 +101,12 @@ table! { uuid -> Text, created_at -> Timestamp, updated_at -> Timestamp, + verified_at -> Nullable, + last_verifying_at -> Nullable, + login_verify_count -> Integer, email -> Text, + email_new -> Nullable, + email_new_token -> Nullable, name -> Text, password_hash -> Binary, salt -> Binary, diff --git a/src/mail.rs b/src/mail.rs index 80a4f46..711ac5b 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -8,7 +8,7 @@ use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use quoted_printable::encode_to_str; use crate::api::EmptyResult; -use crate::auth::{encode_jwt, generate_invite_claims}; +use crate::auth::{encode_jwt, generate_invite_claims, generate_delete_claims, generate_verify_email_claims}; use crate::error::Error; use crate::CONFIG; use chrono::NaiveDateTime; @@ -95,6 +95,73 @@ pub fn send_password_hint(address: &str, hint: Option) -> EmptyResult { send_email(&address, &subject, &body_html, &body_text) } +pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { + let claims = generate_delete_claims( + uuid.to_string(), + ); + let delete_token = encode_jwt(&claims); + + let (subject, body_html, body_text) = get_text( + "email/delete_account", + json!({ + "url": CONFIG.domain(), + "user_id": uuid, + "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), + "token": delete_token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + +pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult { + let claims = generate_verify_email_claims( + uuid.to_string(), + ); + let verify_email_token = encode_jwt(&claims); + + let (subject, body_html, body_text) = get_text( + "email/verify_email", + json!({ + "url": CONFIG.domain(), + "user_id": uuid, + "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), + "token": verify_email_token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + +pub fn send_welcome(address: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/welcome", + json!({ + "url": CONFIG.domain(), + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + +pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult { + let claims = generate_verify_email_claims( + uuid.to_string(), + ); + let verify_email_token = encode_jwt(&claims); + + let (subject, body_html, body_text) = get_text( + "email/welcome_must_verify", + json!({ + "url": CONFIG.domain(), + "user_id": uuid, + "token": verify_email_token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + pub fn send_invite( address: &str, uuid: &str, @@ -183,6 +250,18 @@ pub fn send_token(address: &str, token: &str) -> EmptyResult { send_email(&address, &subject, &body_html, &body_text) } +pub fn send_change_email(address: &str, token: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/change_email", + json!({ + "url": CONFIG.domain(), + "token": token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult { let html = PartBuilder::new() .body(encode_to_str(body_html)) diff --git a/src/static/templates/email/change_email.hbs b/src/static/templates/email/change_email.hbs new file mode 100644 index 0000000..3000882 --- /dev/null +++ b/src/static/templates/email/change_email.hbs @@ -0,0 +1,6 @@ +Your Email Change + + +

To finalize changing your email address enter the following code in web vault: {{token}}

+

If you did not try to change an email address, you can safely ignore this email.

+ diff --git a/src/static/templates/email/change_email.html.hbs b/src/static/templates/email/change_email.html.hbs new file mode 100644 index 0000000..afca96d --- /dev/null +++ b/src/static/templates/email/change_email.html.hbs @@ -0,0 +1,129 @@ +Your Email Change + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + +
+ To finalize changing your email address enter the following code in web vault: {{token}} +
+ If you did not try to change an email address, you can safely ignore this email. +
+
+ + + + + +
+
+ + diff --git a/src/static/templates/email/delete_account.hbs b/src/static/templates/email/delete_account.hbs new file mode 100644 index 0000000..2242534 --- /dev/null +++ b/src/static/templates/email/delete_account.hbs @@ -0,0 +1,12 @@ +Delete Your Account + + +

+click the link below to delete your account. +
+
+ +Delete Your Account +

+

If you did not request this email to delete your account, you can safely ignore this email.

+ diff --git a/src/static/templates/email/delete_account.html.hbs b/src/static/templates/email/delete_account.html.hbs new file mode 100644 index 0000000..70a92cd --- /dev/null +++ b/src/static/templates/email/delete_account.html.hbs @@ -0,0 +1,137 @@ +Delete Your Account + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + +
+ click the link below to delete your account. +
+ + Delete Your Account + +
+ If you did not request this email to delete your account, you can safely ignore this email. +
+
+ + + + + +
+
+ + diff --git a/src/static/templates/email/pw_hint_none.hbs b/src/static/templates/email/pw_hint_none.hbs index c73e3b3..67a7a09 100644 --- a/src/static/templates/email/pw_hint_none.hbs +++ b/src/static/templates/email/pw_hint_none.hbs @@ -1,3 +1,7 @@ -Sorry, you have no password hint... +Your master password hint -Sorry, you have not specified any password hint... +You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. + +If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account so that you can register again and start over. All data associated with your account will be deleted. + +If you did not request your master password hint you can safely ignore this email. diff --git a/src/static/templates/email/pw_hint_none.html.hbs b/src/static/templates/email/pw_hint_none.html.hbs index 12729bf..bf3162c 100644 --- a/src/static/templates/email/pw_hint_none.html.hbs +++ b/src/static/templates/email/pw_hint_none.html.hbs @@ -99,6 +99,11 @@ Sorry, you have no password hint... You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint.
+ + + If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account so that you can register again and start over. All data associated with your account will be deleted. + + If you did not request your master password hint you can safely ignore this email. diff --git a/src/static/templates/email/pw_hint_some.hbs b/src/static/templates/email/pw_hint_some.hbs index f16dc2b..07c3c0a 100644 --- a/src/static/templates/email/pw_hint_some.hbs +++ b/src/static/templates/email/pw_hint_some.hbs @@ -5,4 +5,6 @@ You (or someone) recently requested your master password hint. Your hint is: "{{hint}}" Log in: Web Vault +If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account so that you can register again and start over. All data associated with your account will be deleted. + If you did not request your master password hint you can safely ignore this email. diff --git a/src/static/templates/email/pw_hint_some.html.hbs b/src/static/templates/email/pw_hint_some.html.hbs index 0b17bdd..f1e987a 100644 --- a/src/static/templates/email/pw_hint_some.html.hbs +++ b/src/static/templates/email/pw_hint_some.html.hbs @@ -105,6 +105,11 @@ Your master password hint Log in: Web Vault + + + If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account so that you can register again and start over. All data associated with your account will be deleted. + + If you did not request your master password hint you can safely ignore this email. diff --git a/src/static/templates/email/verify_email.hbs b/src/static/templates/email/verify_email.hbs new file mode 100644 index 0000000..06cf2c8 --- /dev/null +++ b/src/static/templates/email/verify_email.hbs @@ -0,0 +1,12 @@ +Verify Your Email + + +

+Verify this email address for your account by clicking the link below. +
+
+ +Verify Email Address Now +

+

If you did not request to verify your account, you can safely ignore this email.

+ diff --git a/src/static/templates/email/verify_email.html.hbs b/src/static/templates/email/verify_email.html.hbs new file mode 100644 index 0000000..c950c7f --- /dev/null +++ b/src/static/templates/email/verify_email.html.hbs @@ -0,0 +1,137 @@ +Verify Your Email + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + +
+ Verify this email address for your account by clicking the link below. +
+ + Verify Email Address Now + +
+ If you did not request to verify your account, you can safely ignore this email. +
+
+ + + + + +
+
+ + diff --git a/src/static/templates/email/welcome.hbs b/src/static/templates/email/welcome.hbs new file mode 100644 index 0000000..be4f530 --- /dev/null +++ b/src/static/templates/email/welcome.hbs @@ -0,0 +1,8 @@ +Welcome + + +

+Thank you for creating an account at {{url}}. You may now log in with your new account. +

+

If you did not request to create an account, you can safely ignore this email.

+ diff --git a/src/static/templates/email/welcome.html.hbs b/src/static/templates/email/welcome.html.hbs new file mode 100644 index 0000000..5f782f3 --- /dev/null +++ b/src/static/templates/email/welcome.html.hbs @@ -0,0 +1,129 @@ +Welcome + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + +
+ Thank you for creating an account at {{url}}. You may now log in with your new account. +
+ If you did not request to create an account, you can safely ignore this email. +
+
+ + + + + +
+
+ + diff --git a/src/static/templates/email/welcome_must_verify.hbs b/src/static/templates/email/welcome_must_verify.hbs new file mode 100644 index 0000000..2a7f86e --- /dev/null +++ b/src/static/templates/email/welcome_must_verify.hbs @@ -0,0 +1,12 @@ +Welcome + + +

+Thank you for creating an account at {{url}}. Before you can login with your new account, you must verify this email address by clicking the link below. +
+
+ +Verify Email Address Now +

+

If you did not request to create an account, you can safely ignore this email.

+ diff --git a/src/static/templates/email/welcome_must_verify.html.hbs b/src/static/templates/email/welcome_must_verify.html.hbs new file mode 100644 index 0000000..d6de973 --- /dev/null +++ b/src/static/templates/email/welcome_must_verify.html.hbs @@ -0,0 +1,137 @@ +Welcome + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + +
+ Thank you for creating an account at {{url}}. Before you can login with your new account, you must verify this email address by clicking the link below. +
+ + Verify Email Address Now + +
+ If you did not request to create an account, you can safely ignore this email. +
+
+ + + + + +
+
+ +