Skip to main content

torii_storage_sqlite/
passkey.rs

1use 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        // Create a user
134        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}