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