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