shared/models/
user_password_reset.rs

1use 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}