torii_storage_postgres/
passkey.rs

1use crate::PostgresStorage;
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 PostgresStorage {
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 ($1, $2, $3)
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 = $1
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 = $1
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 ($1, $2, $3)
74            "#,
75        )
76        .bind(challenge_id)
77        .bind(challenge)
78        .bind(Utc::now() + expires_in)
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 = $1 AND expires_at > $2
94            "#,
95        )
96        .bind(challenge_id)
97        .bind(Utc::now())
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 torii_core::{NewUser, User, UserStorage, storage::PasskeyStorage};
109    use uuid::Uuid;
110
111    use crate::PostgresStorage;
112
113    async fn create_test_user(storage: &PostgresStorage) -> User {
114        let user = NewUser::builder()
115            .email("test@test.com".to_string())
116            .build()
117            .unwrap();
118        storage.create_user(&user).await.unwrap()
119    }
120
121    #[tokio::test]
122    async fn test_add_and_get_passkey() {
123        let storage = crate::tests::setup_test_db().await;
124
125        // Create a user
126        let user = create_test_user(&storage).await;
127
128        let credential_id = Uuid::new_v4().to_string();
129        let passkey_json = "passkey_json";
130        storage
131            .add_passkey(&user.id, &credential_id, passkey_json)
132            .await
133            .unwrap();
134
135        let passkeys = storage.get_passkeys(&user.id).await.unwrap();
136        assert_eq!(passkeys.len(), 1);
137        assert_eq!(passkeys[0], passkey_json);
138    }
139
140    #[tokio::test]
141    async fn test_set_and_get_passkey_challenge() {
142        let storage = crate::tests::setup_test_db().await;
143
144        let challenge_id = Uuid::new_v4().to_string();
145        let challenge = "challenge";
146        let expires_in = Duration::minutes(5);
147        storage
148            .set_passkey_challenge(&challenge_id, challenge, expires_in)
149            .await
150            .unwrap();
151
152        let stored_challenge = storage.get_passkey_challenge(&challenge_id).await.unwrap();
153        assert!(stored_challenge.is_some());
154        assert_eq!(stored_challenge.unwrap(), challenge);
155    }
156}