shared/models/
user_password_reset.rs1use crate::prelude::*;
2use rand::distr::SampleString;
3use serde::{Deserialize, Serialize};
4use sqlx::{Row, postgres::PgRow};
5use std::collections::BTreeMap;
6
7#[derive(Serialize, Deserialize)]
8pub struct UserPasswordReset {
9 pub uuid: uuid::Uuid,
10 pub user: super::user::User,
11
12 pub token: String,
13
14 pub created: chrono::NaiveDateTime,
15}
16
17impl BaseModel for UserPasswordReset {
18 const NAME: &'static str = "user_password_reset";
19
20 #[inline]
21 fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
22 let prefix = prefix.unwrap_or_default();
23
24 let mut columns = BTreeMap::from([
25 (
26 "user_password_resets.uuid",
27 compact_str::format_compact!("{prefix}uuid"),
28 ),
29 (
30 "user_password_resets.token",
31 compact_str::format_compact!("{prefix}token"),
32 ),
33 (
34 "user_password_resets.created",
35 compact_str::format_compact!("{prefix}created"),
36 ),
37 ]);
38
39 columns.extend(super::user::User::columns(Some("user_")));
40
41 columns
42 }
43
44 #[inline]
45 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
46 let prefix = prefix.unwrap_or_default();
47
48 Ok(Self {
49 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
50 user: super::user::User::map(Some("user_"), row)?,
51 token: row.try_get(compact_str::format_compact!("{prefix}token").as_str())?,
52 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
53 })
54 }
55}
56
57impl UserPasswordReset {
58 pub async fn create(
59 database: &crate::database::Database,
60 user_uuid: uuid::Uuid,
61 ) -> Result<String, anyhow::Error> {
62 let existing = sqlx::query(
63 r#"
64 SELECT COUNT(*)
65 FROM user_password_resets
66 WHERE user_password_resets.user_uuid = $1 AND user_password_resets.created > NOW() - INTERVAL '20 minutes'
67 "#,
68 )
69 .bind(user_uuid)
70 .fetch_optional(database.read())
71 .await?;
72
73 if let Some(row) = existing
74 && row.get::<i64, _>(0) > 0
75 {
76 return Err(anyhow::anyhow!(
77 "a password reset was already requested recently"
78 ));
79 }
80
81 let token = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 96);
82
83 sqlx::query(
84 r#"
85 INSERT INTO user_password_resets (user_uuid, token, created)
86 VALUES ($1, crypt($2, gen_salt('bf')), NOW())
87 "#,
88 )
89 .bind(user_uuid)
90 .bind(&token)
91 .execute(database.write())
92 .await?;
93
94 Ok(token)
95 }
96
97 pub async fn delete_by_token(
98 database: &crate::database::Database,
99 token: &str,
100 ) -> Result<Option<Self>, crate::database::DatabaseError> {
101 let row = sqlx::query(&format!(
102 r#"
103 SELECT {}, {} FROM user_password_resets
104 JOIN users ON users.uuid = user_password_resets.user_uuid
105 LEFT JOIN roles ON roles.uuid = users.role_uuid
106 WHERE
107 user_password_resets.token = crypt($1, user_password_resets.token)
108 AND user_password_resets.created > NOW() - INTERVAL '20 minutes'
109 "#,
110 Self::columns_sql(None),
111 super::user::User::columns_sql(Some("user_"))
112 ))
113 .bind(token)
114 .fetch_optional(database.read())
115 .await?;
116
117 let row = match row {
118 Some(row) => row,
119 None => return Ok(None),
120 };
121
122 sqlx::query(
123 r#"
124 DELETE FROM user_password_resets
125 WHERE user_password_resets.uuid = $1
126 "#,
127 )
128 .bind(row.get::<uuid::Uuid, _>("uuid"))
129 .execute(database.write())
130 .await?;
131
132 Ok(Some(Self::map(None, &row)?))
133 }
134}