shared/models/
user_recovery_code.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 UserRecoveryCode {
9    pub code: compact_str::CompactString,
10
11    pub created: chrono::NaiveDateTime,
12}
13
14impl BaseModel for UserRecoveryCode {
15    const NAME: &'static str = "user_recovery_code";
16
17    #[inline]
18    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
19        let prefix = prefix.unwrap_or_default();
20
21        BTreeMap::from([
22            (
23                "user_recovery_codes.code",
24                compact_str::format_compact!("{prefix}code"),
25            ),
26            (
27                "user_recovery_codes.created",
28                compact_str::format_compact!("{prefix}created"),
29            ),
30        ])
31    }
32
33    #[inline]
34    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
35        let prefix = prefix.unwrap_or_default();
36
37        Ok(Self {
38            code: row.try_get(compact_str::format_compact!("{prefix}code").as_str())?,
39            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
40        })
41    }
42}
43
44impl UserRecoveryCode {
45    pub async fn create_all(
46        database: &crate::database::Database,
47        user_uuid: uuid::Uuid,
48    ) -> Result<Vec<String>, crate::database::DatabaseError> {
49        let mut codes = Vec::new();
50        codes.reserve_exact(10);
51
52        let mut transaction = database.write().begin().await?;
53
54        for _ in 0..10 {
55            let code = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 10);
56
57            sqlx::query(
58                r#"
59                INSERT INTO user_recovery_codes (user_uuid, code, created)
60                VALUES ($1, $2, NOW())
61                "#,
62            )
63            .bind(user_uuid)
64            .bind(&code)
65            .execute(&mut *transaction)
66            .await?;
67
68            codes.push(code);
69        }
70
71        transaction.commit().await?;
72
73        Ok(codes)
74    }
75
76    pub async fn delete_by_user_uuid_code(
77        database: &crate::database::Database,
78        user_uuid: uuid::Uuid,
79        code: &str,
80    ) -> Result<Option<Self>, crate::database::DatabaseError> {
81        let row = sqlx::query(&format!(
82            r#"
83            DELETE FROM user_recovery_codes
84            WHERE user_recovery_codes.user_uuid = $1 AND user_recovery_codes.code = $2
85            RETURNING {}
86            "#,
87            Self::columns_sql(None)
88        ))
89        .bind(user_uuid)
90        .bind(code)
91        .fetch_optional(database.write())
92        .await?;
93
94        row.try_map(|row| Self::map(None, &row))
95    }
96
97    pub async fn delete_by_user_uuid(
98        database: &crate::database::Database,
99        user_uuid: uuid::Uuid,
100    ) -> Result<(), crate::database::DatabaseError> {
101        sqlx::query(
102            r#"
103            DELETE FROM user_recovery_codes
104            WHERE user_recovery_codes.user_uuid = $1
105            "#,
106        )
107        .bind(user_uuid)
108        .execute(database.write())
109        .await?;
110
111        Ok(())
112    }
113}