1use chrono::{DateTime, Utc};
2use sha2::{Digest, Sha256};
3
4use crate::db::Db;
5use crate::error::AuthError;
6use crate::sessions::generate_token;
7use crate::types::{ApiTokenId, ApiTokenInfo, TokenHash, UserId};
8
9fn hash_api_token(raw: &str) -> TokenHash {
15 let digest = Sha256::digest(raw.as_bytes());
16 TokenHash::new_unchecked(format!("{digest:x}"))
17}
18
19impl Db {
20 pub async fn create_api_token(
26 &self,
27 user_id: UserId,
28 name: &str,
29 expires_at: Option<DateTime<Utc>>,
30 metadata: Option<&str>,
31 ) -> Result<(String, ApiTokenInfo), AuthError> {
32 let id = ApiTokenId::new();
33 let raw_session_token = generate_token();
34 let raw = raw_session_token.as_str().to_string();
35 let token_hash = hash_api_token(&raw);
36 let expires_str = expires_at.map(|t| t.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string());
37
38 let info = sqlx::query_as::<_, ApiTokenInfo>(
39 "INSERT INTO allowthem_api_tokens (id, user_id, name, token_hash, expires_at, metadata)
40 VALUES (?, ?, ?, ?, ?, ?)
41 RETURNING id, user_id, name, metadata, expires_at, created_at",
42 )
43 .bind(id)
44 .bind(user_id)
45 .bind(name)
46 .bind(token_hash)
47 .bind(expires_str)
48 .bind(metadata)
49 .fetch_one(self.pool())
50 .await
51 .map_err(AuthError::Database)?;
52
53 Ok((raw, info))
54 }
55
56 pub async fn validate_api_token(
61 &self,
62 raw_token: &str,
63 ) -> Result<Option<(UserId, ApiTokenInfo)>, AuthError> {
64 let hash = hash_api_token(raw_token);
65 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
66 let info = sqlx::query_as::<_, ApiTokenInfo>(
67 "SELECT id, user_id, name, metadata, expires_at, created_at
68 FROM allowthem_api_tokens
69 WHERE token_hash = ? AND (expires_at IS NULL OR expires_at > ?)",
70 )
71 .bind(hash)
72 .bind(now)
73 .fetch_optional(self.pool())
74 .await
75 .map_err(AuthError::Database)?;
76
77 Ok(info.map(|i| (i.user_id, i)))
78 }
79
80 pub async fn list_api_tokens(&self, user_id: UserId) -> Result<Vec<ApiTokenInfo>, AuthError> {
82 sqlx::query_as::<_, ApiTokenInfo>(
83 "SELECT id, user_id, name, metadata, expires_at, created_at
84 FROM allowthem_api_tokens
85 WHERE user_id = ?
86 ORDER BY created_at DESC",
87 )
88 .bind(user_id)
89 .fetch_all(self.pool())
90 .await
91 .map_err(AuthError::Database)
92 }
93
94 pub async fn delete_api_token(&self, id: ApiTokenId) -> Result<bool, AuthError> {
98 let result = sqlx::query("DELETE FROM allowthem_api_tokens WHERE id = ?")
99 .bind(id)
100 .execute(self.pool())
101 .await
102 .map_err(AuthError::Database)?;
103 Ok(result.rows_affected() > 0)
104 }
105
106 pub async fn delete_user_api_tokens(&self, user_id: UserId) -> Result<u64, AuthError> {
110 let result = sqlx::query("DELETE FROM allowthem_api_tokens WHERE user_id = ?")
111 .bind(user_id)
112 .execute(self.pool())
113 .await
114 .map_err(AuthError::Database)?;
115 Ok(result.rows_affected())
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use chrono::{Duration, Utc};
122
123 use crate::db::Db;
124 use crate::types::{Email, UserId};
125
126 async fn test_db() -> Db {
127 Db::connect("sqlite::memory:")
128 .await
129 .expect("in-memory test db")
130 }
131
132 async fn create_test_user(db: &Db) -> UserId {
133 let email = Email::new(format!("user_{}@example.com", uuid::Uuid::now_v7())).unwrap();
134 let user = db
135 .create_user(email, "password123", None, None)
136 .await
137 .unwrap();
138 user.id
139 }
140
141 #[tokio::test]
142 async fn test_create_and_validate_api_token() {
143 let db = test_db().await;
144 let user_id = create_test_user(&db).await;
145
146 let (raw, info) = db
147 .create_api_token(user_id, "my-token", None, None)
148 .await
149 .unwrap();
150
151 assert_eq!(info.user_id, user_id);
152 assert_eq!(info.name, "my-token");
153 assert!(info.expires_at.is_none());
154 assert!(info.metadata.is_none());
155
156 let result = db.validate_api_token(&raw).await.unwrap();
157 let (uid, _token_info) = result.expect("token must be valid");
158 assert_eq!(uid, user_id);
159 }
160
161 #[tokio::test]
162 async fn test_expired_api_token_rejected() {
163 let db = test_db().await;
164 let user_id = create_test_user(&db).await;
165
166 let past = Utc::now() - Duration::hours(1);
167 let (raw, _) = db
168 .create_api_token(user_id, "expired-token", Some(past), None)
169 .await
170 .unwrap();
171
172 let result = db.validate_api_token(&raw).await.unwrap();
173 assert!(result.is_none(), "expired token must be rejected");
174 }
175
176 #[tokio::test]
177 async fn test_deleted_api_token_rejected() {
178 let db = test_db().await;
179 let user_id = create_test_user(&db).await;
180
181 let (raw, info) = db
182 .create_api_token(user_id, "delete-me", None, None)
183 .await
184 .unwrap();
185
186 let deleted = db.delete_api_token(info.id).await.unwrap();
187 assert!(deleted);
188
189 let result = db.validate_api_token(&raw).await.unwrap();
190 assert!(result.is_none(), "deleted token must be rejected");
191 }
192
193 #[tokio::test]
194 async fn test_list_api_tokens() {
195 let db = test_db().await;
196 let user_id = create_test_user(&db).await;
197
198 db.create_api_token(user_id, "token-a", None, None)
199 .await
200 .unwrap();
201 db.create_api_token(user_id, "token-b", None, None)
202 .await
203 .unwrap();
204
205 let tokens = db.list_api_tokens(user_id).await.unwrap();
206 assert_eq!(tokens.len(), 2);
207 let names: Vec<&str> = tokens.iter().map(|t| t.name.as_str()).collect();
209 assert!(names.contains(&"token-a"));
210 assert!(names.contains(&"token-b"));
211 }
212
213 #[tokio::test]
214 async fn test_cascade_delete_removes_api_tokens() {
215 let db = test_db().await;
216 let user_id = create_test_user(&db).await;
217
218 db.create_api_token(user_id, "to-be-cascaded", None, None)
219 .await
220 .unwrap();
221
222 db.delete_user(user_id).await.unwrap();
224
225 let token_count: i64 =
226 sqlx::query_scalar("SELECT COUNT(*) FROM allowthem_api_tokens WHERE user_id = ?")
227 .bind(user_id)
228 .fetch_one(db.pool())
229 .await
230 .unwrap();
231
232 assert_eq!(token_count, 0, "api tokens must cascade-delete with user");
233 }
234
235 #[tokio::test]
236 async fn test_create_with_metadata() {
237 let db = test_db().await;
238 let user_id = create_test_user(&db).await;
239
240 let (raw, info) = db
241 .create_api_token(user_id, "meta-token", None, Some("key=value"))
242 .await
243 .unwrap();
244
245 assert_eq!(info.metadata.as_deref(), Some("key=value"));
246
247 let (uid, token_info) = db
249 .validate_api_token(&raw)
250 .await
251 .unwrap()
252 .expect("token must be valid");
253 assert_eq!(uid, user_id);
254 assert_eq!(token_info.metadata.as_deref(), Some("key=value"));
255
256 let tokens = db.list_api_tokens(user_id).await.unwrap();
258 assert_eq!(tokens.len(), 1);
259 assert_eq!(tokens[0].metadata.as_deref(), Some("key=value"));
260 }
261}