use argon2::{ PasswordHasher, password_hash::{SaltString, rand_core::OsRng}, }; use chrono::Utc; use diesel::{ ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, dsl::now, insert_into, update, }; use diesel_async::RunQueryDsl; use lettre::message::MultiPart; use uuid::Uuid; use crate::{ Conn, Data, error::Error, schema::{password_reset_tokens, users}, utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier}, }; #[derive(Selectable, Queryable)] #[diesel(table_name = password_reset_tokens)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct PasswordResetToken { user_uuid: Uuid, pub token: String, pub created_at: chrono::DateTime, } impl PasswordResetToken { pub async fn get(conn: &mut Conn, token: String) -> Result { use password_reset_tokens::dsl; let password_reset_token = dsl::password_reset_tokens .filter(dsl::token.eq(token)) .select(PasswordResetToken::as_select()) .get_result(conn) .await?; Ok(password_reset_token) } pub async fn get_with_identifier( conn: &mut Conn, identifier: String, ) -> Result { let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; use password_reset_tokens::dsl; let password_reset_token = dsl::password_reset_tokens .filter(dsl::user_uuid.eq(user_uuid)) .select(PasswordResetToken::as_select()) .get_result(conn) .await?; Ok(password_reset_token) } #[allow(clippy::new_ret_no_self)] pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { let token = generate_refresh_token()?; let mut conn = data.pool.get().await?; let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; global_checks(data, user_uuid).await?; use users::dsl as udsl; let (username, email_address): (String, String) = udsl::users .filter(udsl::uuid.eq(user_uuid)) .select((udsl::username, udsl::email)) .get_result(&mut conn) .await?; use password_reset_tokens::dsl; insert_into(password_reset_tokens::table) .values(( dsl::user_uuid.eq(user_uuid), dsl::token.eq(&token), dsl::created_at.eq(now), )) .execute(&mut conn) .await?; let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; reset_endpoint.set_query(Some(&format!("token={}", token))); let email = data .mail_client .message_builder() .to(email_address.parse()?) .subject(format!("{} Password Reset", data.config.instance.name)) .multipart(MultiPart::alternative_plain_html( format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) ))?; data.mail_client.send_mail(email).await?; Ok(()) } pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { if !PASSWORD_REGEX.is_match(&password) { return Err(Error::BadRequest( "Please provide a valid password".to_string(), )); } let salt = SaltString::generate(&mut OsRng); let hashed_password = data .argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| Error::PasswordHashError(e.to_string()))?; let mut conn = data.pool.get().await?; use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.user_uuid)) .set(dsl::password.eq(hashed_password.to_string())) .execute(&mut conn) .await?; let (username, email_address): (String, String) = dsl::users .filter(dsl::uuid.eq(self.user_uuid)) .select((dsl::username, dsl::email)) .get_result(&mut conn) .await?; let login_page = data.config.web.frontend_url.join("login")?; let email = data .mail_client .message_builder() .to(email_address.parse()?) .subject(format!("Your {} Password has been Reset", data.config.instance.name)) .multipart(MultiPart::alternative_plain_html( format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) ))?; data.mail_client.send_mail(email).await?; self.delete(&mut conn).await } pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { use password_reset_tokens::dsl; delete(password_reset_tokens::table) .filter(dsl::user_uuid.eq(self.user_uuid)) .filter(dsl::token.eq(&self.token)) .execute(conn) .await?; Ok(()) } }