allowthem_core/
token_cleanup.rs1use chrono::Utc;
2
3use crate::db::Db;
4use crate::error::AuthError;
5
6impl Db {
7 pub async fn cleanup_expired_tokens(&self) -> Result<u64, AuthError> {
8 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
9
10 let r1 = sqlx::query(
11 "DELETE FROM allowthem_password_reset_tokens \
12 WHERE expires_at <= ? OR used_at IS NOT NULL",
13 )
14 .bind(&now)
15 .execute(self.pool())
16 .await
17 .map_err(AuthError::Database)?;
18
19 let r2 = sqlx::query(
20 "DELETE FROM allowthem_email_verification_tokens \
21 WHERE expires_at <= ? OR used_at IS NOT NULL",
22 )
23 .bind(&now)
24 .execute(self.pool())
25 .await
26 .map_err(AuthError::Database)?;
27
28 Ok(r1.rows_affected() + r2.rows_affected())
29 }
30}
31
32#[cfg(test)]
33mod tests {
34 use chrono::{Duration, Utc};
35
36 use crate::db::Db;
37 use crate::types::{Email, ResetTokenId, UserId, VerificationTokenId};
38
39 async fn test_db() -> Db {
40 Db::connect("sqlite::memory:").await.expect("in-memory db")
41 }
42
43 async fn make_user(db: &Db) -> UserId {
44 let email = Email::new("cleanup@example.com".to_string()).unwrap();
45 let user = db
46 .create_user(email, "test-password", None, None)
47 .await
48 .expect("create user");
49 user.id
50 }
51
52 async fn insert_reset_token(db: &Db, user_id: UserId, expires_at: &str, used: bool) {
53 let id = ResetTokenId::new();
54 let used_at = if used {
55 Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
56 } else {
57 None
58 };
59 sqlx::query(
60 "INSERT INTO allowthem_password_reset_tokens \
61 (id, user_id, token_hash, expires_at, used_at) VALUES (?, ?, ?, ?, ?)",
62 )
63 .bind(id)
64 .bind(user_id)
65 .bind(format!("reset-hash-{}", uuid::Uuid::now_v7()))
66 .bind(expires_at)
67 .bind(used_at.as_deref())
68 .execute(db.pool())
69 .await
70 .expect("insert reset token");
71 }
72
73 async fn insert_verification_token(db: &Db, user_id: UserId, expires_at: &str, used: bool) {
74 let id = VerificationTokenId::new();
75 let used_at = if used {
76 Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
77 } else {
78 None
79 };
80 sqlx::query(
81 "INSERT INTO allowthem_email_verification_tokens \
82 (id, user_id, token_hash, expires_at, used_at) VALUES (?, ?, ?, ?, ?)",
83 )
84 .bind(id)
85 .bind(user_id)
86 .bind(format!("verify-hash-{}", uuid::Uuid::now_v7()))
87 .bind(expires_at)
88 .bind(used_at.as_deref())
89 .execute(db.pool())
90 .await
91 .expect("insert verification token");
92 }
93
94 async fn count_reset_tokens(db: &Db) -> i64 {
95 let (count,): (i64,) =
96 sqlx::query_as("SELECT COUNT(*) FROM allowthem_password_reset_tokens")
97 .fetch_one(db.pool())
98 .await
99 .expect("count");
100 count
101 }
102
103 async fn count_verification_tokens(db: &Db) -> i64 {
104 let (count,): (i64,) =
105 sqlx::query_as("SELECT COUNT(*) FROM allowthem_email_verification_tokens")
106 .fetch_one(db.pool())
107 .await
108 .expect("count");
109 count
110 }
111
112 #[tokio::test]
113 async fn cleanup_removes_expired_tokens() {
114 let db = test_db().await;
115 let user_id = make_user(&db).await;
116 let past = (Utc::now() - Duration::hours(1))
117 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
118 .to_string();
119 let future = (Utc::now() + Duration::hours(1))
120 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
121 .to_string();
122
123 insert_reset_token(&db, user_id, &past, false).await;
124 insert_reset_token(&db, user_id, &future, false).await;
125 insert_verification_token(&db, user_id, &past, false).await;
126 insert_verification_token(&db, user_id, &future, false).await;
127
128 let removed = db.cleanup_expired_tokens().await.expect("cleanup");
129 assert_eq!(
130 removed, 2,
131 "should remove 1 expired reset + 1 expired verification"
132 );
133 assert_eq!(count_reset_tokens(&db).await, 1);
134 assert_eq!(count_verification_tokens(&db).await, 1);
135 }
136
137 #[tokio::test]
138 async fn cleanup_removes_used_tokens() {
139 let db = test_db().await;
140 let user_id = make_user(&db).await;
141 let future = (Utc::now() + Duration::hours(1))
142 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
143 .to_string();
144
145 insert_reset_token(&db, user_id, &future, true).await;
146 insert_verification_token(&db, user_id, &future, true).await;
147
148 let removed = db.cleanup_expired_tokens().await.expect("cleanup");
149 assert_eq!(removed, 2, "should remove used tokens even if not expired");
150 }
151
152 #[tokio::test]
153 async fn cleanup_preserves_active_tokens() {
154 let db = test_db().await;
155 let user_id = make_user(&db).await;
156 let future = (Utc::now() + Duration::hours(1))
157 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
158 .to_string();
159
160 insert_reset_token(&db, user_id, &future, false).await;
161 insert_verification_token(&db, user_id, &future, false).await;
162
163 let removed = db.cleanup_expired_tokens().await.expect("cleanup");
164 assert_eq!(removed, 0, "should not remove active tokens");
165 assert_eq!(count_reset_tokens(&db).await, 1);
166 assert_eq!(count_verification_tokens(&db).await, 1);
167 }
168}