1use chrono::Utc;
4use uuid::Uuid;
5
6use crate::api_key_store::ApiKeyStore;
7use crate::entities::{ApiKey, ApiKeyUpdate, NewApiKey};
8use crate::error::StoreError;
9use crate::store::StoreFuture;
10
11use super::InMemoryStore;
12
13impl ApiKeyStore for InMemoryStore {
14 fn create_api_key(&self, req: NewApiKey) -> StoreFuture<'_, ApiKey> {
15 Box::pin(async move {
16 let mut state = self.state.write().await;
17 let now = Utc::now();
18 let id = Uuid::now_v7();
19 let key = ApiKey {
20 id,
21 user_id: req.user_id,
22 name: req.name,
23 key_hash: req.key_hash,
24 key_prefix: req.key_prefix,
25 scopes: req.scopes,
26 is_active: true,
27 expires_at: req.expires_at,
28 last_used_at: None,
29 created_at: now,
30 updated_at: now,
31 };
32 state.api_keys.insert(id, key.clone());
33 Ok(key)
34 })
35 }
36
37 fn find_api_key_by_prefix(&self, prefix: &str) -> StoreFuture<'_, Option<ApiKey>> {
38 let prefix = prefix.to_string();
39 Box::pin(async move {
40 let state = self.state.read().await;
41 Ok(state
42 .api_keys
43 .values()
44 .find(|k| k.key_prefix == prefix && k.is_active)
45 .cloned())
46 })
47 }
48
49 fn find_api_key_by_id(&self, id: Uuid) -> StoreFuture<'_, Option<ApiKey>> {
50 Box::pin(async move {
51 let state = self.state.read().await;
52 Ok(state.api_keys.get(&id).cloned())
53 })
54 }
55
56 fn list_api_keys_by_user(&self, user_id: Uuid) -> StoreFuture<'_, Vec<ApiKey>> {
57 Box::pin(async move {
58 let state = self.state.read().await;
59 let keys: Vec<ApiKey> = state
60 .api_keys
61 .values()
62 .filter(|k| k.user_id == user_id)
63 .cloned()
64 .collect();
65 Ok(keys)
66 })
67 }
68
69 fn update_api_key(&self, id: Uuid, update: ApiKeyUpdate) -> StoreFuture<'_, ()> {
70 Box::pin(async move {
71 let mut state = self.state.write().await;
72 let key = state
73 .api_keys
74 .get_mut(&id)
75 .ok_or(StoreError::Database(format!("API key {id} not found")))?;
76 if let Some(name) = update.name {
77 key.name = name;
78 }
79 if let Some(scopes) = update.scopes {
80 key.scopes = scopes;
81 }
82 if let Some(is_active) = update.is_active {
83 key.is_active = is_active;
84 }
85 if let Some(expires_at) = update.expires_at {
86 key.expires_at = expires_at;
87 }
88 key.updated_at = Utc::now();
89 Ok(())
90 })
91 }
92
93 fn touch_api_key(&self, id: Uuid) -> StoreFuture<'_, ()> {
94 Box::pin(async move {
95 let mut state = self.state.write().await;
96 if let Some(key) = state.api_keys.get_mut(&id) {
97 key.last_used_at = Some(Utc::now());
98 }
99 Ok(())
100 })
101 }
102
103 fn delete_api_key(&self, id: Uuid) -> StoreFuture<'_, ()> {
104 Box::pin(async move {
105 let mut state = self.state.write().await;
106 state.api_keys.remove(&id);
107 Ok(())
108 })
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use crate::entities::ApiKeyScope;
115
116 use super::*;
117
118 #[tokio::test]
119 async fn create_api_key_returns_active_key() {
120 let store = InMemoryStore::new();
121 let user_id = Uuid::now_v7();
122
123 let key = store
124 .create_api_key(NewApiKey {
125 user_id,
126 name: "production".to_string(),
127 key_hash: "bcrypt_hash".to_string(),
128 key_prefix: "sk_prod_abc123".to_string(),
129 scopes: vec![ApiKeyScope::WorkflowsRead],
130 expires_at: None,
131 })
132 .await
133 .unwrap();
134
135 assert_eq!(key.user_id, user_id);
136 assert_eq!(key.name, "production");
137 assert!(key.is_active);
138 assert!(key.last_used_at.is_none());
139 }
140
141 #[tokio::test]
142 async fn find_api_key_by_prefix_existing() {
143 let store = InMemoryStore::new();
144 let user_id = Uuid::now_v7();
145
146 let created = store
147 .create_api_key(NewApiKey {
148 user_id,
149 name: "test".to_string(),
150 key_hash: "hash".to_string(),
151 key_prefix: "sk_test_xyz".to_string(),
152 scopes: vec![],
153 expires_at: None,
154 })
155 .await
156 .unwrap();
157
158 let found = store
159 .find_api_key_by_prefix("sk_test_xyz")
160 .await
161 .unwrap()
162 .expect("key should exist");
163
164 assert_eq!(found.id, created.id);
165 }
166
167 #[tokio::test]
168 async fn find_api_key_by_prefix_missing_returns_none() {
169 let store = InMemoryStore::new();
170 let found = store
171 .find_api_key_by_prefix("sk_nonexistent")
172 .await
173 .unwrap();
174
175 assert!(found.is_none());
176 }
177
178 #[tokio::test]
179 async fn find_api_key_by_prefix_inactive_returns_none() {
180 let store = InMemoryStore::new();
181 let user_id = Uuid::now_v7();
182
183 let created = store
184 .create_api_key(NewApiKey {
185 user_id,
186 name: "test".to_string(),
187 key_hash: "hash".to_string(),
188 key_prefix: "sk_inactive".to_string(),
189 scopes: vec![],
190 expires_at: None,
191 })
192 .await
193 .unwrap();
194
195 store
197 .update_api_key(
198 created.id,
199 ApiKeyUpdate {
200 name: None,
201 scopes: None,
202 is_active: Some(false),
203 expires_at: None,
204 },
205 )
206 .await
207 .unwrap();
208
209 let found = store.find_api_key_by_prefix("sk_inactive").await.unwrap();
210
211 assert!(found.is_none());
212 }
213
214 #[tokio::test]
215 async fn find_api_key_by_id_existing() {
216 let store = InMemoryStore::new();
217 let user_id = Uuid::now_v7();
218
219 let created = store
220 .create_api_key(NewApiKey {
221 user_id,
222 name: "test".to_string(),
223 key_hash: "hash".to_string(),
224 key_prefix: "sk_test".to_string(),
225 scopes: vec![],
226 expires_at: None,
227 })
228 .await
229 .unwrap();
230
231 let found = store
232 .find_api_key_by_id(created.id)
233 .await
234 .unwrap()
235 .expect("key should exist");
236
237 assert_eq!(found.id, created.id);
238 }
239
240 #[tokio::test]
241 async fn find_api_key_by_id_missing_returns_none() {
242 let store = InMemoryStore::new();
243 let found = store.find_api_key_by_id(Uuid::now_v7()).await.unwrap();
244
245 assert!(found.is_none());
246 }
247
248 #[tokio::test]
249 async fn list_api_keys_by_user_returns_only_user_keys() {
250 let store = InMemoryStore::new();
251 let user1 = Uuid::now_v7();
252 let user2 = Uuid::now_v7();
253
254 store
255 .create_api_key(NewApiKey {
256 user_id: user1,
257 name: "key1".to_string(),
258 key_hash: "hash1".to_string(),
259 key_prefix: "sk_1".to_string(),
260 scopes: vec![],
261 expires_at: None,
262 })
263 .await
264 .unwrap();
265
266 store
267 .create_api_key(NewApiKey {
268 user_id: user1,
269 name: "key2".to_string(),
270 key_hash: "hash2".to_string(),
271 key_prefix: "sk_2".to_string(),
272 scopes: vec![],
273 expires_at: None,
274 })
275 .await
276 .unwrap();
277
278 store
279 .create_api_key(NewApiKey {
280 user_id: user2,
281 name: "key3".to_string(),
282 key_hash: "hash3".to_string(),
283 key_prefix: "sk_3".to_string(),
284 scopes: vec![],
285 expires_at: None,
286 })
287 .await
288 .unwrap();
289
290 let user1_keys = store.list_api_keys_by_user(user1).await.unwrap();
291 assert_eq!(user1_keys.len(), 2);
292 assert!(user1_keys.iter().all(|k| k.user_id == user1));
293 }
294
295 #[tokio::test]
296 async fn update_api_key_name() {
297 let store = InMemoryStore::new();
298 let user_id = Uuid::now_v7();
299
300 let created = store
301 .create_api_key(NewApiKey {
302 user_id,
303 name: "old-name".to_string(),
304 key_hash: "hash".to_string(),
305 key_prefix: "sk_test".to_string(),
306 scopes: vec![],
307 expires_at: None,
308 })
309 .await
310 .unwrap();
311
312 store
313 .update_api_key(
314 created.id,
315 ApiKeyUpdate {
316 name: Some("new-name".to_string()),
317 scopes: None,
318 is_active: None,
319 expires_at: None,
320 },
321 )
322 .await
323 .unwrap();
324
325 let updated = store.find_api_key_by_id(created.id).await.unwrap().unwrap();
326
327 assert_eq!(updated.name, "new-name");
328 }
329
330 #[tokio::test]
331 async fn update_api_key_scopes() {
332 let store = InMemoryStore::new();
333 let user_id = Uuid::now_v7();
334
335 let created = store
336 .create_api_key(NewApiKey {
337 user_id,
338 name: "test".to_string(),
339 key_hash: "hash".to_string(),
340 key_prefix: "sk_test".to_string(),
341 scopes: vec![ApiKeyScope::WorkflowsRead],
342 expires_at: None,
343 })
344 .await
345 .unwrap();
346
347 let new_scopes = vec![ApiKeyScope::WorkflowsRead, ApiKeyScope::RunsWrite];
348
349 store
350 .update_api_key(
351 created.id,
352 ApiKeyUpdate {
353 name: None,
354 scopes: Some(new_scopes.clone()),
355 is_active: None,
356 expires_at: None,
357 },
358 )
359 .await
360 .unwrap();
361
362 let updated = store.find_api_key_by_id(created.id).await.unwrap().unwrap();
363
364 assert_eq!(updated.scopes, new_scopes);
365 }
366
367 #[tokio::test]
368 async fn touch_api_key_updates_last_used() {
369 let store = InMemoryStore::new();
370 let user_id = Uuid::now_v7();
371
372 let created = store
373 .create_api_key(NewApiKey {
374 user_id,
375 name: "test".to_string(),
376 key_hash: "hash".to_string(),
377 key_prefix: "sk_test".to_string(),
378 scopes: vec![],
379 expires_at: None,
380 })
381 .await
382 .unwrap();
383
384 assert!(created.last_used_at.is_none());
385
386 store.touch_api_key(created.id).await.unwrap();
387
388 let touched = store.find_api_key_by_id(created.id).await.unwrap().unwrap();
389
390 assert!(touched.last_used_at.is_some());
391 }
392
393 #[tokio::test]
394 async fn delete_api_key() {
395 let store = InMemoryStore::new();
396 let user_id = Uuid::now_v7();
397
398 let created = store
399 .create_api_key(NewApiKey {
400 user_id,
401 name: "test".to_string(),
402 key_hash: "hash".to_string(),
403 key_prefix: "sk_test".to_string(),
404 scopes: vec![],
405 expires_at: None,
406 })
407 .await
408 .unwrap();
409
410 store.delete_api_key(created.id).await.unwrap();
411
412 let found = store.find_api_key_by_id(created.id).await.unwrap();
413
414 assert!(found.is_none());
415 }
416
417 #[tokio::test]
418 async fn delete_nonexistent_api_key_is_idempotent() {
419 let store = InMemoryStore::new();
420 let result = store.delete_api_key(Uuid::now_v7()).await;
421 assert!(result.is_ok());
422 }
423}