Skip to main content

headless_lms_models/
email_verification_tokens.rs

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