backend/objects/
password_reset_token.rs

1use argon2::{
2    PasswordHasher,
3    password_hash::{SaltString, rand_core::OsRng},
4};
5use chrono::Utc;
6use diesel::{
7    ExpressionMethods, QueryDsl, update,
8};
9use diesel_async::RunQueryDsl;
10use lettre::message::MultiPart;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14use crate::{
15    Data,
16    error::Error,
17    schema::users,
18    utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier},
19};
20
21#[derive(Serialize, Deserialize)]
22pub struct PasswordResetToken {
23    user_uuid: Uuid,
24    pub token: String,
25    pub created_at: chrono::DateTime<Utc>,
26}
27
28impl PasswordResetToken {
29    pub async fn get(data: &Data, token: String) -> Result<PasswordResetToken, Error> {
30        let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(format!("{}", token)).await?)?;
31        let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?;
32
33        Ok(password_reset_token)
34    }
35
36    pub async fn get_with_identifier(
37        data: &Data,
38        identifier: String,
39    ) -> Result<PasswordResetToken, Error> {
40        let mut conn = data.pool.get().await?;
41
42        let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?;
43
44        let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?;
45
46        Ok(password_reset_token)
47    }
48
49    #[allow(clippy::new_ret_no_self)]
50    pub async fn new(data: &Data, identifier: String) -> Result<(), Error> {
51        let token = generate_refresh_token()?;
52
53        let mut conn = data.pool.get().await?;
54
55        let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?;
56
57        global_checks(data, user_uuid).await?;
58
59        use users::dsl as udsl;
60        let (username, email_address): (String, String) = udsl::users
61            .filter(udsl::uuid.eq(user_uuid))
62            .select((udsl::username, udsl::email))
63            .get_result(&mut conn)
64            .await?;
65
66        let password_reset_token = PasswordResetToken {
67            user_uuid,
68            token: token.clone(),
69            created_at: Utc::now(),
70        };
71
72        data.set_cache_key(format!("{}_password_reset", user_uuid), password_reset_token,  86400).await?;
73        data.set_cache_key(token.clone(), user_uuid, 86400).await?;
74
75        let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?;
76
77        reset_endpoint.set_query(Some(&format!("token={}", token)));
78
79        let email = data
80            .mail_client
81            .message_builder()
82            .to(email_address.parse()?)
83            .subject(format!("{} Password Reset", data.config.instance.name))
84            .multipart(MultiPart::alternative_plain_html(
85                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), 
86                format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset</h1></div><div class="content"><h2>Hello, {}!</h2><p>Someone requested a password reset for your Gorb account.</p><p>Click the button below within 24 hours to reset your password.</p><a href="{}" class="verify-button">RESET PASSWORD</a><p>If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, data.config.instance.name, username, reset_endpoint)
87            ))?;
88
89        data.mail_client.send_mail(email).await?;
90
91        Ok(())
92    }
93
94    pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> {
95        if !PASSWORD_REGEX.is_match(&password) {
96            return Err(Error::BadRequest(
97                "Please provide a valid password".to_string(),
98            ));
99        }
100
101        let salt = SaltString::generate(&mut OsRng);
102
103        let hashed_password = data
104            .argon2
105            .hash_password(password.as_bytes(), &salt)
106            .map_err(|e| Error::PasswordHashError(e.to_string()))?;
107
108        let mut conn = data.pool.get().await?;
109
110        use users::dsl;
111        update(users::table)
112            .filter(dsl::uuid.eq(self.user_uuid))
113            .set(dsl::password.eq(hashed_password.to_string()))
114            .execute(&mut conn)
115            .await?;
116
117        let (username, email_address): (String, String) = dsl::users
118            .filter(dsl::uuid.eq(self.user_uuid))
119            .select((dsl::username, dsl::email))
120            .get_result(&mut conn)
121            .await?;
122
123        let login_page = data.config.web.frontend_url.join("login")?;
124
125        let email = data
126            .mail_client
127            .message_builder()
128            .to(email_address.parse()?)
129            .subject(format!("Your {} Password has been Reset", data.config.instance.name))
130            .multipart(MultiPart::alternative_plain_html(
131                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 <strong>immediately</strong>.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), 
132                format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset Confirmation</h1></div><div class="content"><h2>Hello, {}!</h2><p>Your password has been successfully reset for your Gorb account.</p><p>If you did not initiate this change, please click the button below to reset your password <strong>immediately</strong>.</p><a href="{}" class="verify-button">RESET PASSWORD</a><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, data.config.instance.name, username, login_page)
133            ))?;
134
135        data.mail_client.send_mail(email).await?;
136
137        self.delete(&data).await
138    }
139
140    pub async fn delete(&self, data: &Data) -> Result<(), Error> {
141        data.del_cache_key(format!("{}_password_reset", &self.user_uuid)).await?;
142        data.del_cache_key(format!("{}", &self.token)).await?;
143
144        Ok(())
145    }
146}