torii_storage_postgres/
magic_link.rs

1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use torii_core::{
4    UserId,
5    error::StorageError,
6    storage::{MagicLinkStorage, MagicToken},
7};
8
9use crate::PostgresStorage;
10
11#[derive(Debug, Clone, sqlx::FromRow)]
12pub struct PostgresMagicToken {
13    pub id: Option<i64>,
14    pub user_id: String,
15    pub token: String,
16    pub used_at: Option<DateTime<Utc>>,
17    pub expires_at: DateTime<Utc>,
18    pub created_at: DateTime<Utc>,
19    pub updated_at: DateTime<Utc>,
20}
21
22impl From<PostgresMagicToken> for MagicToken {
23    fn from(row: PostgresMagicToken) -> Self {
24        MagicToken::new(
25            UserId::new(&row.user_id),
26            row.token.clone(),
27            row.used_at,
28            row.expires_at,
29            row.created_at,
30            row.updated_at,
31        )
32    }
33}
34
35impl From<&MagicToken> for PostgresMagicToken {
36    fn from(token: &MagicToken) -> Self {
37        PostgresMagicToken {
38            id: None,
39            user_id: token.user_id.as_str().to_string(),
40            token: token.token.clone(),
41            used_at: token.used_at,
42            expires_at: token.expires_at,
43            created_at: token.created_at,
44            updated_at: token.updated_at,
45        }
46    }
47}
48
49#[async_trait]
50impl MagicLinkStorage for PostgresStorage {
51    type Error = StorageError;
52
53    async fn save_magic_token(
54        &self,
55        token: &MagicToken,
56    ) -> Result<(), <Self as MagicLinkStorage>::Error> {
57        let row = PostgresMagicToken::from(token);
58
59        sqlx::query("INSERT INTO magic_links (user_id, token, expires_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)")
60            .bind(row.user_id)
61            .bind(row.token)
62            .bind(row.expires_at)
63            .bind(row.created_at)
64            .bind(row.updated_at)
65            .execute(&self.pool)
66            .await
67            .map_err(|e| StorageError::Database(e.to_string()))?;
68
69        Ok(())
70    }
71
72    async fn get_magic_token(
73        &self,
74        token: &str,
75    ) -> Result<Option<MagicToken>, <Self as MagicLinkStorage>::Error> {
76        let row: Option<PostgresMagicToken> =
77            sqlx::query_as(
78                "SELECT id, user_id, token, used_at, expires_at, created_at, updated_at FROM magic_links WHERE token = $1 AND expires_at > $2 AND used_at IS NULL",
79            )
80            .bind(token)
81                .bind(Utc::now())
82                .fetch_optional(&self.pool)
83                .await
84                .map_err(|e| StorageError::Database(e.to_string()))?;
85
86        Ok(row.map(|row| row.into()))
87    }
88
89    async fn set_magic_token_used(
90        &self,
91        token: &str,
92    ) -> Result<(), <Self as MagicLinkStorage>::Error> {
93        sqlx::query("UPDATE magic_links SET used_at = $1 WHERE token = $2")
94            .bind(Utc::now())
95            .bind(token)
96            .execute(&self.pool)
97            .await
98            .map_err(|e| StorageError::Database(e.to_string()))?;
99
100        Ok(())
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    use chrono::Duration;
109    use torii_core::{NewUser, User, UserStorage, storage::MagicLinkStorage};
110    use uuid::Uuid;
111
112    use crate::PostgresStorage;
113
114    async fn create_test_user(storage: &PostgresStorage) -> User {
115        let user = NewUser::builder()
116            .email("test@test.com".to_string())
117            .build()
118            .expect("Failed to build test user");
119        storage
120            .create_user(&user)
121            .await
122            .expect("Failed to create test user")
123    }
124
125    #[tokio::test]
126    async fn test_save_and_get_magic_token() {
127        let storage = crate::tests::setup_test_db().await;
128
129        // Create a user
130        let user = create_test_user(&storage).await;
131
132        let token = MagicToken::new(
133            UserId::new(&user.id.to_string()),
134            Uuid::new_v4().to_string(),
135            None,
136            Utc::now() + Duration::minutes(5),
137            Utc::now(),
138            Utc::now(),
139        );
140        storage
141            .save_magic_token(&token)
142            .await
143            .expect("Failed to save magic token");
144
145        let stored_token = storage
146            .get_magic_token(&token.token)
147            .await
148            .expect("Failed to get magic token");
149        assert!(stored_token.is_some());
150
151        let stored_token = stored_token.unwrap();
152        assert_eq!(stored_token, token);
153    }
154
155    #[tokio::test]
156    async fn test_get_nonexistent_magic_token() {
157        let storage = crate::tests::setup_test_db().await;
158
159        let token = Uuid::new_v4().to_string();
160        let result = storage
161            .get_magic_token(&token)
162            .await
163            .expect("Failed to query magic token");
164        assert!(result.is_none());
165    }
166}