backend/objects/
password_reset_token.rs

1use argon2::{
2    PasswordHasher,
3    password_hash::{SaltString, rand_core::OsRng},
4};
5use chrono::Utc;
6use diesel::{ExpressionMethods, QueryDsl, update};
7use diesel_async::RunQueryDsl;
8use lettre::message::MultiPart;
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12use crate::{
13    AppState, Conn,
14    error::Error,
15    schema::users,
16    utils::{CacheFns, PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier},
17};
18
19#[derive(Serialize, Deserialize)]
20pub struct PasswordResetToken {
21    user_uuid: Uuid,
22    pub token: String,
23    pub created_at: chrono::DateTime<Utc>,
24}
25
26impl PasswordResetToken {
27    pub async fn get(
28        cache_pool: &redis::Client,
29        token: String,
30    ) -> Result<PasswordResetToken, Error> {
31        let user_uuid: Uuid = cache_pool.get_cache_key(token.to_string()).await?;
32        let password_reset_token = cache_pool
33            .get_cache_key(format!("{user_uuid}_password_reset"))
34            .await?;
35
36        Ok(password_reset_token)
37    }
38
39    pub async fn get_with_identifier(
40        conn: &mut Conn,
41        cache_pool: &redis::Client,
42        identifier: String,
43    ) -> Result<PasswordResetToken, Error> {
44        let user_uuid = user_uuid_from_identifier(conn, &identifier).await?;
45
46        let password_reset_token = cache_pool
47            .get_cache_key(format!("{user_uuid}_password_reset"))
48            .await?;
49
50        Ok(password_reset_token)
51    }
52
53    #[allow(clippy::new_ret_no_self)]
54    pub async fn new(
55        conn: &mut Conn,
56        app_state: &AppState,
57        identifier: String,
58    ) -> Result<(), Error> {
59        let token = generate_token::<32>()?;
60
61        let user_uuid = user_uuid_from_identifier(conn, &identifier).await?;
62
63        global_checks(conn, &app_state.config, user_uuid).await?;
64
65        use users::dsl as udsl;
66        let (username, email_address): (String, String) = udsl::users
67            .filter(udsl::uuid.eq(user_uuid))
68            .select((udsl::username, udsl::email))
69            .get_result(conn)
70            .await?;
71
72        let password_reset_token = PasswordResetToken {
73            user_uuid,
74            token: token.clone(),
75            created_at: Utc::now(),
76        };
77
78        app_state
79            .cache_pool
80            .set_cache_key(
81                format!("{user_uuid}_password_reset"),
82                password_reset_token,
83                86400,
84            )
85            .await?;
86        app_state
87            .cache_pool
88            .set_cache_key(token.clone(), user_uuid, 86400)
89            .await?;
90
91        let mut reset_endpoint = app_state.config.web.frontend_url.join("reset-password")?;
92
93        reset_endpoint.set_query(Some(&format!("token={token}")));
94
95        let email = app_state
96            .mail_client
97            .message_builder()
98            .to(email_address.parse()?)
99            .subject(format!("{} Password Reset", app_state.config.instance.name))
100            .multipart(MultiPart::alternative_plain_html(
101                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.", app_state.config.instance.name, username, reset_endpoint), 
102                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>"#, app_state.config.instance.name, username, reset_endpoint)
103            ))?;
104
105        app_state.mail_client.send_mail(email).await?;
106
107        Ok(())
108    }
109
110    pub async fn set_password(
111        &self,
112        conn: &mut Conn,
113        app_state: &AppState,
114        password: String,
115    ) -> Result<(), Error> {
116        if !PASSWORD_REGEX.is_match(&password) {
117            return Err(Error::BadRequest(
118                "Please provide a valid password".to_string(),
119            ));
120        }
121
122        let salt = SaltString::generate(&mut OsRng);
123
124        let hashed_password = app_state
125            .argon2
126            .hash_password(password.as_bytes(), &salt)
127            .map_err(|e| Error::PasswordHashError(e.to_string()))?;
128
129        use users::dsl;
130        update(users::table)
131            .filter(dsl::uuid.eq(self.user_uuid))
132            .set(dsl::password.eq(hashed_password.to_string()))
133            .execute(conn)
134            .await?;
135
136        let (username, email_address): (String, String) = dsl::users
137            .filter(dsl::uuid.eq(self.user_uuid))
138            .select((dsl::username, dsl::email))
139            .get_result(conn)
140            .await?;
141
142        let login_page = app_state.config.web.frontend_url.join("login")?;
143
144        let email = app_state
145            .mail_client
146            .message_builder()
147            .to(email_address.parse()?)
148            .subject(format!("Your {} Password has been Reset", app_state.config.instance.name))
149            .multipart(MultiPart::alternative_plain_html(
150                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.", app_state.config.instance.name, username, login_page), 
151                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>"#, app_state.config.instance.name, username, login_page)
152            ))?;
153
154        app_state.mail_client.send_mail(email).await?;
155
156        self.delete(&app_state.cache_pool).await
157    }
158
159    pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> {
160        cache_pool
161            .del_cache_key(format!("{}_password_reset", &self.user_uuid))
162            .await?;
163        cache_pool.del_cache_key(self.token.to_string()).await?;
164
165        Ok(())
166    }
167}