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 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 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}