headless_lms_models/
user_email_codes.rs

1use crate::prelude::*;
2
3#[derive(sqlx::FromRow, Debug, Clone)]
4pub struct UserEmailCode {
5    pub id: Uuid,
6    pub user_id: Uuid,
7    pub code: String,
8    pub expires_at: DateTime<Utc>,
9    pub used_at: Option<DateTime<Utc>>,
10    pub created_at: DateTime<Utc>,
11    pub updated_at: DateTime<Utc>,
12    pub deleted_at: Option<DateTime<Utc>>,
13}
14
15pub async fn insert_user_email_code(
16    conn: &mut PgConnection,
17    user_id: Uuid,
18    code: String,
19) -> ModelResult<String> {
20    let mut tx = conn.begin().await?;
21
22    // Soft delete possible previous codes so that only one code is at use at a time
23    let _ = sqlx::query!(
24        r#"
25   UPDATE user_email_codes
26SET deleted_at = NOW()
27WHERE user_id = $1
28  AND deleted_at IS NULL
29    "#,
30        user_id
31    )
32    .execute(&mut *tx)
33    .await?;
34
35    // Attempt to insert new code; the unique index ensures no more than one active code per user
36    let record = sqlx::query!(
37        r#"
38      INSERT INTO user_email_codes (code, user_id)
39VALUES ($1, $2)
40RETURNING code
41        "#,
42        code,
43        user_id
44    )
45    .fetch_one(&mut *tx)
46    .await?;
47
48    tx.commit().await?;
49
50    Ok(record.code)
51}
52
53pub async fn get_unused_user_email_code_with_user_id(
54    conn: &mut PgConnection,
55    user_id: Uuid,
56) -> ModelResult<Option<UserEmailCode>> {
57    let now = Utc::now();
58    let record = sqlx::query_as!(
59        UserEmailCode,
60        r#"
61SELECT id,
62  code,
63  user_id,
64  created_at,
65  updated_at,
66  used_at,
67  deleted_at,
68  expires_at
69FROM user_email_codes
70WHERE user_id = $1
71  AND deleted_at IS NULL
72  AND used_at IS NULL
73  AND expires_at > $2
74        "#,
75        user_id,
76        now
77    )
78    .fetch_optional(conn)
79    .await?;
80
81    Ok(record)
82}
83
84pub async fn is_reset_user_email_code_valid(
85    conn: &mut PgConnection,
86    user_id: Uuid,
87    code: &String,
88) -> ModelResult<bool> {
89    let now = Utc::now();
90    let record = sqlx::query!(
91        r#"
92SELECT *
93FROM user_email_codes
94WHERE user_id = $1
95  AND code = $2
96  AND deleted_at IS NULL
97  AND used_at IS NULL
98  AND expires_at > $3
99       "#,
100        user_id,
101        code,
102        now
103    )
104    .fetch_optional(conn)
105    .await?;
106
107    Ok(record.is_some())
108}
109
110pub async fn mark_user_email_code_used(
111    conn: &mut PgConnection,
112    user_id: Uuid,
113    code: &String,
114) -> ModelResult<bool> {
115    let result = sqlx::query!(
116        r#"
117UPDATE user_email_codes
118SET used_at = NOW(),
119  deleted_at = NOW()
120WHERE user_id = $1
121  AND code = $2
122  AND deleted_at IS NULL
123        "#,
124        user_id,
125        code
126    )
127    .execute(conn)
128    .await?;
129
130    Ok(result.rows_affected() > 0)
131}