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}