headless_lms_models/
email_verification_tokens.rs1use 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}