Skip to main content

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