diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index 306fd49..562b827 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -467,6 +467,22 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.7" @@ -543,6 +559,22 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -595,6 +627,15 @@ dependencies = [ "ascii_utils", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.26" @@ -611,6 +652,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -626,6 +682,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + [[package]] name = "futures-macro" version = "0.3.28" @@ -656,8 +718,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", + "futures-io", "futures-macro", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -674,8 +738,10 @@ dependencies = [ "diesel", "env_logger", "lazy_static", + "lettre", "log", "mailchecker", + "rand", "redis", "serde", "serde_json", @@ -748,6 +814,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.9" @@ -797,6 +874,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -847,6 +933,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lettre" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d" +dependencies = [ + "base64", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "once_cell", + "quoted_printable", + "socket2", + "tokio", +] + [[package]] name = "libc" version = "0.2.144" @@ -906,6 +1015,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "memchr" version = "2.5.0" @@ -918,6 +1033,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -939,6 +1060,34 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -955,6 +1104,50 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "openssl" +version = "0.10.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df40a956736488b7b44fe79fe12d4f245bb5b3f5a1f6095e499760015be392" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -973,7 +1166,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] @@ -1065,6 +1258,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49" + [[package]] name = "rand" version = "0.8.5" @@ -1118,6 +1317,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.8.2" @@ -1164,12 +1372,44 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.17" @@ -1298,6 +1538,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -1494,6 +1747,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index f4b5504..561c8b3 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -17,4 +17,6 @@ serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" actix-remote-ip = "0.1.0" mailchecker = "5.0.9" -redis = "0.23.0" \ No newline at end of file +redis = "0.23.0" +lettre = "0.10.4" +rand = "0.8.5" \ No newline at end of file diff --git a/geneit_backend/src/app_config.rs b/geneit_backend/src/app_config.rs index e7bcf90..1d0c61d 100644 --- a/geneit_backend/src/app_config.rs +++ b/geneit_backend/src/app_config.rs @@ -55,6 +55,38 @@ pub struct AppConfig { /// Redis password #[clap(long, env, default_value = "secretredis")] redis_password: String, + + /// Mail sender + #[clap(long, env, default_value = "geneit@example.com")] + pub mail_sender: String, + + /// SMTP relay + #[clap(long, env, default_value = "localhost")] + pub smtp_relay: String, + + /// SMTP port + #[clap(long, env, default_value_t = 1025)] + pub smtp_port: u16, + + /// SMTP use TLS to connect to relay + #[clap(long, env)] + pub smtp_tls: bool, + + /// SMTP username + #[clap(long, env)] + pub smtp_username: Option, + + /// SMTP password + #[clap(long, env)] + pub smtp_password: Option, + + /// Password reset URL + #[clap( + long, + env, + default_value = "http://localhost:3000/reset_password#TOKEN" + )] + pub reset_password_url: String, } lazy_static::lazy_static! { @@ -88,4 +120,9 @@ impl AppConfig { }, } } + + /// Get password reset URL + pub fn get_password_reset_url(&self, token: &str) -> String { + self.reset_password_url.replace("TOKEN", token) + } } diff --git a/geneit_backend/src/controllers/auth_controller.rs b/geneit_backend/src/controllers/auth_controller.rs index 7927f3e..7e41ae1 100644 --- a/geneit_backend/src/controllers/auth_controller.rs +++ b/geneit_backend/src/controllers/auth_controller.rs @@ -39,9 +39,12 @@ pub async fn create_account(remote_ip: RemoteIP, req: web::Json, - reset_password_token: Option, - time_create: i64, - time_gen_reset_token: i64, - time_activate: i64, - active: bool, - admin: bool, + pub id: i32, + pub name: String, + pub email: String, + pub password: Option, + pub reset_password_token: Option, + pub time_create: i64, + pub time_gen_reset_token: i64, + pub time_activate: i64, + pub active: bool, + pub admin: bool, } impl User { diff --git a/geneit_backend/src/services/mail_service.rs b/geneit_backend/src/services/mail_service.rs new file mode 100644 index 0000000..d2491f9 --- /dev/null +++ b/geneit_backend/src/services/mail_service.rs @@ -0,0 +1,33 @@ +use crate::app_config::AppConfig; +use lettre::message::header::ContentType; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{Message, SmtpTransport, Transport}; +use std::fmt::Display; + +pub async fn send_mail(to: &str, subject: &str, body: D) -> anyhow::Result<()> { + let conf = AppConfig::get(); + + let email = Message::builder() + .from(conf.mail_sender.parse()?) + .to(to.parse()?) + .subject(subject) + .header(ContentType::TEXT_PLAIN) + .body(body.to_string())?; + + let mut mailer = match conf.smtp_tls { + true => SmtpTransport::relay(&conf.smtp_relay)?, + false => SmtpTransport::builder_dangerous(&conf.smtp_relay), + } + .port(conf.smtp_port); + + if let (Some(username), Some(password)) = (&conf.smtp_username, &conf.smtp_password) { + mailer = mailer.credentials(Credentials::new(username.to_string(), password.to_string())) + } + + let mailer = mailer.build(); + + mailer.send(&email)?; + log::debug!("A mail was sent to {} (subject = {})", to, subject); + + Ok(()) +} diff --git a/geneit_backend/src/services/mod.rs b/geneit_backend/src/services/mod.rs index db116d9..0e81e07 100644 --- a/geneit_backend/src/services/mod.rs +++ b/geneit_backend/src/services/mod.rs @@ -1,4 +1,5 @@ //! # Backend services +pub mod mail_service; pub mod rate_limiter_service; pub mod users_service; diff --git a/geneit_backend/src/services/users_service.rs b/geneit_backend/src/services/users_service.rs index ae96bda..76aa620 100644 --- a/geneit_backend/src/services/users_service.rs +++ b/geneit_backend/src/services/users_service.rs @@ -1,11 +1,19 @@ //! # Users service +use crate::app_config::AppConfig; use crate::connections::db_connection; -use crate::models::{NewUser, User}; +use crate::models::{NewUser, User, UserID}; use crate::schema::users; +use crate::services::mail_service; +use crate::utils::string_utils::rand_str; use crate::utils::time_utils::time; use diesel::prelude::*; +/// Get the information of the user +pub async fn get_by_id(id: UserID) -> anyhow::Result { + db_connection::execute(|conn| Ok(users::table.filter(users::dsl::id.eq(id.0)).first(conn)?)) +} + /// Create a new account pub async fn create_account(name: &str, email: &str) -> anyhow::Result { db_connection::execute(|conn| { @@ -32,3 +40,41 @@ pub async fn exists_email(email: &str) -> anyhow::Result { Ok(count != 0) }) } + +/// Request password reset +pub async fn request_reset_password(user: &mut User) -> anyhow::Result<()> { + // If required, regenerate reset token + if user.reset_password_token.is_none() || user.time_gen_reset_token as u64 + 3600 * 2 < time() { + user.reset_password_token = Some(rand_str(149)); + user.time_gen_reset_token = time() as i64; + + db_connection::execute(|conn| { + Ok( + diesel::update(users::dsl::users.filter(users::dsl::id.eq(user.id))) + .set(( + users::dsl::time_gen_reset_token.eq(user.time_gen_reset_token), + users::dsl::reset_password_token.eq(user.reset_password_token.clone()), + )) + .execute(conn)?, + ) + })?; + } + + // Send mail + mail_service::send_mail( + &user.email, + "Réinitialisation de votre mot de passe", + format!( + "Bonjour, \n\n\ + Vous pouvez réinitialiser le mot de passe de votre compte à l'adresse suivante : {} \n\n\ + Ce lien est valide durant 24 heures.\n\n\ + Cordialement,\n\n\ + L'équipe de GeneIT", + AppConfig::get() + .get_password_reset_url(user.reset_password_token.as_deref().unwrap_or("")) + ), + ) + .await?; + + Ok(()) +} diff --git a/geneit_backend/src/utils/mod.rs b/geneit_backend/src/utils/mod.rs index 0e2ae9a..1af8d13 100644 --- a/geneit_backend/src/utils/mod.rs +++ b/geneit_backend/src/utils/mod.rs @@ -1,3 +1,4 @@ //! # App utilities +pub mod string_utils; pub mod time_utils; diff --git a/geneit_backend/src/utils/string_utils.rs b/geneit_backend/src/utils/string_utils.rs new file mode 100644 index 0000000..237d8c2 --- /dev/null +++ b/geneit_backend/src/utils/string_utils.rs @@ -0,0 +1,11 @@ +use rand::distributions::Alphanumeric; +use rand::Rng; + +/// Generate a random string of a given size +pub fn rand_str(len: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .map(char::from) + .take(len) + .collect() +}