torii_storage_sqlite/
passkey.rs1use crate::SqliteStorage;
2use async_trait::async_trait;
3use chrono::Utc;
4use torii_core::UserId;
5use torii_core::error::StorageError;
6use torii_core::storage::PasskeyStorage;
7
8#[async_trait]
9impl PasskeyStorage for SqliteStorage {
10 async fn add_passkey(
11 &self,
12 user_id: &UserId,
13 credential_id: &str,
14 passkey_json: &str,
15 ) -> Result<(), torii_core::Error> {
16 sqlx::query(
17 r#"
18 INSERT INTO passkeys (credential_id, user_id, public_key)
19 VALUES (?, ?, ?)
20 "#,
21 )
22 .bind(credential_id)
23 .bind(user_id.as_str())
24 .bind(passkey_json)
25 .execute(&self.pool)
26 .await
27 .map_err(|e| StorageError::Database(e.to_string()))?;
28 Ok(())
29 }
30
31 async fn get_passkey_by_credential_id(
32 &self,
33 credential_id: &str,
34 ) -> Result<Option<String>, torii_core::Error> {
35 let passkey: Option<String> = sqlx::query_scalar(
36 r#"
37 SELECT public_key
38 FROM passkeys
39 WHERE credential_id = ?
40 "#,
41 )
42 .bind(credential_id)
43 .fetch_optional(&self.pool)
44 .await
45 .map_err(|e| StorageError::Database(e.to_string()))?;
46 Ok(passkey)
47 }
48
49 async fn get_passkeys(&self, user_id: &UserId) -> Result<Vec<String>, torii_core::Error> {
50 let passkeys: Vec<String> = sqlx::query_scalar(
51 r#"
52 SELECT public_key
53 FROM passkeys
54 WHERE user_id = ?
55 "#,
56 )
57 .bind(user_id.as_str())
58 .fetch_all(&self.pool)
59 .await
60 .map_err(|e| StorageError::Database(e.to_string()))?;
61 Ok(passkeys)
62 }
63
64 async fn set_passkey_challenge(
65 &self,
66 challenge_id: &str,
67 challenge: &str,
68 expires_in: chrono::Duration,
69 ) -> Result<(), torii_core::Error> {
70 sqlx::query(
71 r#"
72 INSERT INTO passkey_challenges (challenge_id, challenge, expires_at)
73 VALUES (?, ?, ?)
74 "#,
75 )
76 .bind(challenge_id)
77 .bind(challenge)
78 .bind((Utc::now() + expires_in).timestamp())
79 .execute(&self.pool)
80 .await
81 .map_err(|e| StorageError::Database(e.to_string()))?;
82 Ok(())
83 }
84
85 async fn get_passkey_challenge(
86 &self,
87 challenge_id: &str,
88 ) -> Result<Option<String>, torii_core::Error> {
89 let challenge: Option<String> = sqlx::query_scalar(
90 r#"
91 SELECT challenge
92 FROM passkey_challenges
93 WHERE challenge_id = ? AND expires_at > ?
94 "#,
95 )
96 .bind(challenge_id)
97 .bind(Utc::now().timestamp())
98 .fetch_optional(&self.pool)
99 .await
100 .map_err(|e| StorageError::Database(e.to_string()))?;
101 Ok(challenge)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use chrono::Duration;
108 use sqlx::SqlitePool;
109 use torii_core::{NewUser, User, UserStorage, storage::PasskeyStorage};
110
111 use crate::SqliteStorage;
112
113 async fn setup_sqlite_storage() -> SqliteStorage {
114 let _ = tracing_subscriber::fmt::try_init();
115 let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
116 let storage = SqliteStorage::new(pool);
117 storage.migrate().await.unwrap();
118 storage
119 }
120
121 async fn create_test_user(storage: &SqliteStorage) -> User {
122 let user = NewUser::builder()
123 .email("test@test.com".to_string())
124 .build()
125 .unwrap();
126 storage.create_user(&user).await.unwrap()
127 }
128
129 #[tokio::test]
130 async fn test_add_and_get_passkey() {
131 let storage = setup_sqlite_storage().await;
132
133 let user = create_test_user(&storage).await;
135
136 let credential_id = "credential_id";
137 let passkey_json = "passkey_json";
138 storage
139 .add_passkey(&user.id, credential_id, passkey_json)
140 .await
141 .unwrap();
142
143 let passkeys = storage.get_passkeys(&user.id).await.unwrap();
144 assert_eq!(passkeys.len(), 1);
145 assert_eq!(passkeys[0], passkey_json);
146 }
147
148 #[tokio::test]
149 async fn test_set_and_get_passkey_challenge() {
150 let storage = setup_sqlite_storage().await;
151
152 let challenge_id = "challenge_id";
153 let challenge = "challenge";
154 let expires_in = Duration::minutes(5);
155 storage
156 .set_passkey_challenge(challenge_id, challenge, expires_in)
157 .await
158 .unwrap();
159
160 let stored_challenge = storage.get_passkey_challenge(challenge_id).await.unwrap();
161 assert!(stored_challenge.is_some());
162 assert_eq!(stored_challenge.unwrap(), challenge);
163 }
164}