headless_lms_models/
email_verification_tokens.rs

1use crate::prelude::*;
2use rand::Rng;
3use rand::distr::{Alphanumeric, SampleString};
4
5#[derive(sqlx::FromRow, Debug, Clone)]
6pub struct EmailVerificationToken {
7    pub id: Uuid,
8    pub email_verification_token: String,
9    pub user_id: Uuid,
10    pub code: String,
11    pub code_sent: bool,
12    pub expires_at: DateTime<Utc>,
13    pub used_at: Option<DateTime<Utc>>,
14    pub created_at: DateTime<Utc>,
15    pub updated_at: DateTime<Utc>,
16    pub deleted_at: Option<DateTime<Utc>>,
17}
18
19pub fn is_valid(token: &EmailVerificationToken) -> bool {
20    let now = Utc::now();
21    token.expires_at > now && token.used_at.is_none() && token.deleted_at.is_none()
22}
23
24pub async fn maybe_cleanup_expired(conn: &mut PgConnection) -> ModelResult<()> {
25    let random_num = rand::rng().random_range(1..=10);
26    if random_num == 1 {
27        info!("Cleaning up expired email verification tokens");
28        let _ = sqlx::query!(
29            r#"
30DELETE FROM email_verification_tokens
31WHERE expires_at < NOW()
32  AND deleted_at IS NULL
33            "#,
34        )
35        .execute(conn)
36        .await?;
37    }
38    Ok(())
39}
40
41pub async fn create_email_verification_token(
42    conn: &mut PgConnection,
43    user_id: Uuid,
44    code: String,
45) -> ModelResult<String> {
46    maybe_cleanup_expired(conn).await?;
47
48    let email_verification_token = Alphanumeric.sample_string(&mut rand::rng(), 128);
49
50    let result = sqlx::query!(
51        r#"
52INSERT INTO email_verification_tokens (
53    email_verification_token,
54    user_id,
55    code,
56    expires_at
57)
58VALUES ($1, $2, $3, NOW() + INTERVAL '15 minutes')
59RETURNING email_verification_token
60        "#,
61        email_verification_token,
62        user_id,
63        code
64    )
65    .fetch_one(conn)
66    .await?;
67
68    Ok(result.email_verification_token)
69}
70
71pub async fn get_by_email_verification_token(
72    conn: &mut PgConnection,
73    token: &str,
74) -> ModelResult<Option<EmailVerificationToken>> {
75    maybe_cleanup_expired(conn).await?;
76
77    let record = sqlx::query_as!(
78        EmailVerificationToken,
79        r#"
80SELECT id,
81    email_verification_token,
82    user_id,
83    code,
84    code_sent,
85    expires_at,
86    used_at,
87    created_at,
88    updated_at,
89    deleted_at
90FROM email_verification_tokens
91WHERE email_verification_token = $1
92  AND expires_at > NOW()
93  AND deleted_at IS NULL
94  AND used_at IS NULL
95        "#,
96        token
97    )
98    .fetch_optional(conn)
99    .await?;
100
101    Ok(record)
102}
103
104pub async fn verify_code(
105    conn: &mut PgConnection,
106    email_verification_token: &str,
107    code: &str,
108) -> ModelResult<bool> {
109    let token = get_by_email_verification_token(conn, email_verification_token).await?;
110
111    match token {
112        Some(t) => Ok(t.code == code),
113        None => Ok(false),
114    }
115}
116
117pub async fn mark_as_used(
118    conn: &mut PgConnection,
119    email_verification_token: &str,
120) -> ModelResult<()> {
121    sqlx::query!(
122        r#"
123UPDATE email_verification_tokens
124SET used_at = NOW()
125WHERE email_verification_token = $1
126  AND deleted_at IS NULL
127  AND used_at IS NULL
128  AND expires_at > NOW()
129        "#,
130        email_verification_token
131    )
132    .execute(conn)
133    .await?;
134
135    Ok(())
136}
137
138pub async fn mark_code_sent(
139    conn: &mut PgConnection,
140    email_verification_token: &str,
141) -> ModelResult<()> {
142    sqlx::query!(
143        r#"
144UPDATE email_verification_tokens
145SET code_sent = TRUE
146WHERE email_verification_token = $1
147  AND deleted_at IS NULL
148        "#,
149        email_verification_token
150    )
151    .execute(conn)
152    .await?;
153
154    Ok(())
155}