torii_storage_postgres/
passkey.rs1use 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 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}