diff --git a/src/methods/account_recover.rs b/src/methods/account_recover.rs index 650c9d5..3c50db5 100644 --- a/src/methods/account_recover.rs +++ b/src/methods/account_recover.rs @@ -34,7 +34,7 @@ pub mod password { if let Some(mail) = &email { if mail.trim().ends_with("@skynet.ie") { // all responses from this are a success - return Ok(json!({"result": "success"}).into()); + return Ok(json!({"result": "error", "error": "Skynet email not permitted."}).into()); } } @@ -51,6 +51,7 @@ pub mod password { // user does not have a different email address set if user_details.mail.trim().ends_with("@skynet.ie") { + // not returning an error here as there is no need to let the person requesting what email the user has return Ok(json!({"result": "success"}).into()); } diff --git a/src/methods/account_update.rs b/src/methods/account_update.rs index 0bb85dc..1e9a144 100644 --- a/src/methods/account_update.rs +++ b/src/methods/account_update.rs @@ -36,7 +36,7 @@ pub async fn submit(mut req: Request) -> tide::Result { // check that any mail is not using @skynet.ie if field == "mail" && value.trim().ends_with("@skynet.ie") { - return Ok(json!({"result": "error", "error": "skynet email not valid contact address"}).into()); + return Ok(json!({"result": "error", "error": "Skynet email not valid contact address"}).into()); } let config = &req.state().config; diff --git a/src/methods/mod.rs b/src/methods/mod.rs index f48ff03..8273b7b 100644 --- a/src/methods/mod.rs +++ b/src/methods/mod.rs @@ -1,4 +1,3 @@ pub mod account_new; pub mod account_recover; pub mod account_update; -pub mod password_reset; diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs deleted file mode 100644 index 4332b61..0000000 --- a/src/methods/password_reset.rs +++ /dev/null @@ -1,302 +0,0 @@ -use crate::{get_now_iso, random_string, uid_to_dn, Accounts, AccountsReset, Config, State}; -use chrono::{Duration, SecondsFormat, Utc}; -use ldap3::{exop::PasswordModify, LdapConn}; -use lettre::{ - message::{header, MultiPart, SinglePart}, - transport::smtp::{authentication::Credentials, response::Response, Error}, - Message, SmtpTransport, Transport, -}; -use maud::html; -use sqlx::{Pool, Sqlite}; -use tide::{ - prelude::{json, Deserialize}, - Request, -}; - -#[derive(Debug, Deserialize)] -pub struct PassReset { - user: Option, - email: Option, -} - -/// Handles password resets -/// All responses are success, never want to leak info -pub async fn post_password_reset(mut req: Request) -> tide::Result { - let PassReset { - user, - email, - } = req.body_json().await?; - - // check that any mail is not using @skynet.ie - if let Some(mail) = &email { - if mail.trim().ends_with("@skynet.ie") { - // all responses from this are a success - return Ok(json!({"result": "success"}).into()); - } - } - - let config = &req.state().config; - let db = &req.state().db; - - // considering the local db is updated hourly (or less) use that instead of teh ldap for lookups - let user_details = match db_get_user(db, &user, &email).await { - None => { - return Ok(json!({"result": "success"}).into()); - } - Some(x) => x, - }; - - // user does not have a different email address set - if user_details.mail.trim().ends_with("@skynet.ie") { - return Ok(json!({"result": "success"}).into()); - } - - // check if a recent password reset request happened lately - db_pending_clear_expired(db).await?; - - if db_get_user_reset(db, &user_details.user).await.is_some() { - // reset already requested within timeframe - return Ok(json!({"result": "success"}).into()); - } - - // send mail - let auth = random_string(50); - - if send_mail(config, &user_details, &auth).is_ok() { - // save to db - - save_to_db(db, &user_details, &auth).await?; - } - - Ok(json!({"result": "success"}).into()) -} - -#[derive(Debug, Deserialize)] -pub struct PassResetAuth { - auth: String, - pass: String, -} - -pub async fn post_password_auth(mut req: Request) -> tide::Result { - let PassResetAuth { - auth, - pass, - } = req.body_json().await?; - - let config = &req.state().config; - let db = &req.state().db; - - if db_pending_clear_expired(db).await.is_err() { - return Ok(json!({"result": "error"}).into()); - } - - // check if auth exists - let details = match db_get_user_reset_auth(db, &auth).await { - None => { - return Ok(json!({"result": "error"}).into()); - } - Some(x) => x, - }; - - if ldap_reset_pw(config, &details, &pass).await.is_err() { - return Ok(json!({"result": "error", "error": "ldap error"}).into()); - }; - - Ok(json!({"result": "success", "success": "Password set"}).into()) -} - -async fn db_get_user(pool: &Pool, user_in: &Option, mail_in: &Option) -> Option { - let user = match user_in { - None => "", - Some(x) => x, - }; - let mail = match mail_in { - None => "", - Some(x) => x, - }; - - if let Ok(res) = sqlx::query_as::<_, Accounts>( - r#" - SELECT * - FROM accounts - WHERE user == ? OR mail ==? - "#, - ) - .bind(user) - .bind(mail) - .fetch_all(pool) - .await - { - if !res.is_empty() { - return Some(res[0].to_owned()); - } - } - - None -} - -async fn db_pending_clear_expired(pool: &Pool) -> Result, sqlx::Error> { - sqlx::query_as::<_, AccountsReset>( - r#" - DELETE - FROM accounts_reset - WHERE date_expiry < ? - "#, - ) - .bind(get_now_iso(false)) - .fetch_all(pool) - .await -} - -async fn db_get_user_reset(pool: &Pool, user: &str) -> Option { - if let Ok(res) = sqlx::query_as::<_, AccountsReset>( - r#" - SELECT * - FROM accounts_reset - WHERE user == ? - "#, - ) - .bind(user) - .fetch_all(pool) - .await - { - if !res.is_empty() { - return Some(res[0].to_owned()); - } - } - - None -} - -async fn db_get_user_reset_auth(pool: &Pool, auth: &str) -> Option { - if let Ok(res) = sqlx::query_as::<_, AccountsReset>( - r#" - SELECT * - FROM accounts_reset - WHERE auth == ? - "#, - ) - .bind(auth) - .fetch_all(pool) - .await - { - if !res.is_empty() { - return Some(res[0].to_owned()); - } - } - - None -} - -async fn ldap_reset_pw(config: &Config, details: &AccountsReset, pass: &str) -> Result<(), ldap3::LdapError> { - let mut ldap = LdapConn::new(&config.ldap_host)?; - ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw)?.success()?; - - let dn = uid_to_dn(&details.user); - - // if so then set password - let tmp = PasswordModify { - // none as we are staying on the same connection. - user_id: Some(&dn), - old_pass: None, - new_pass: Some(pass), - }; - - ldap.extended(tmp)?.success()?; - ldap.unbind()?; - - Ok(()) -} - -fn send_mail(config: &Config, record: &Accounts, auth: &str) -> Result { - let recipient = &record.user; - let mail = &record.mail; - let url_base = "https://account.skynet.ie"; - let link_new = format!("{url_base}/recovery_pass?auth={auth}"); - let discord = "https://discord.skynet.ie"; - let sender = format!("UL Computer Society <{}>", &config.mail_user); - - // Create the html we want to send. - let html = html! { - head { - title { "Hello from Skynet!" } - style type="text/css" { - "h2, h4 { font-family: Arial, Helvetica, sans-serif; }" - } - } - div style="display: flex; flex-direction: column; align-items: center;" { - h2 { "Hello from Skynet!" } - // Substitute in the name of our recipient. - p { "Hi " (recipient) "," } - p { - "Here is your password reset link:" - br; - a href=(link_new) { (link_new) } - } - p { - "If did not request this please ignore." - } - p { - "UL Computer Society" - br; - "Skynet Team" - br; - a href=(discord) { (discord) } - } - } - }; - - let body_text = format!( - r#" - Hi {recipient} - - Here is your password reset link: - {link_new} - - If did not request this please ignore. - - UL Computer Society - Skynet Team - {discord} - "# - ); - - // Build the message. - let email = Message::builder() - .from(sender.parse().unwrap()) - .to(mail.parse().unwrap()) - .subject("Skynet: Password Reset") - .multipart( - // This is composed of two parts. - // also helps not trip spam settings (uneven number of url's - MultiPart::alternative() - .singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text)) - .singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())), - ) - .expect("failed to build email"); - - let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone()); - - // Open a remote connection to gmail using STARTTLS - let mailer = SmtpTransport::starttls_relay(&config.mail_smtp).unwrap().credentials(creds).build(); - - // Send the email - mailer.send(&email) -} - -async fn save_to_db(db: &Pool, record: &Accounts, auth: &str) -> Result, sqlx::Error> { - // lets start off a 4 hour timeout on password resets - let offset = Utc::now() + Duration::hours(4); - - sqlx::query_as::<_, AccountsReset>( - " - INSERT OR REPLACE INTO accounts_reset (user, auth_code, date_expiry) - VALUES (?1, ?2, ?3) - ", - ) - .bind(record.user.to_owned()) - .bind(auth.to_owned()) - .bind(offset.to_rfc3339_opts(SecondsFormat::Millis, true)) - .fetch_optional(db) - .await -}