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<String>,
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    async fn save_magic_token(&self, token: &MagicToken) -> Result<(), Self::Error> {
52        let row = PostgresMagicToken::from(token);
53
54        sqlx::query("INSERT INTO magic_links (user_id, token, expires_at, created_at, updated_at) VALUES ($1::uuid, $2, $3, $4, $5)")
55            .bind(row.user_id)
56            .bind(row.token)
57            .bind(row.expires_at)
58            .bind(row.created_at)
59            .bind(row.updated_at)
60            .execute(&self.pool)
61            .await
62            .map_err(|e| StorageError::Database(e.to_string()))?;
63
64        Ok(())
65    }
66
67    async fn get_magic_token(&self, token: &str) -> Result<Option<MagicToken>, Self::Error> {
68        let row: Option<PostgresMagicToken> =
69            sqlx::query_as("SELECT id::text, user_id::text, token, used_at, expires_at, created_at, updated_at FROM magic_links WHERE token = $1 AND expires_at > $2 AND used_at IS NULL")
70                .bind(token)
71                .bind(Utc::now())
72                .fetch_optional(&self.pool)
73                .await
74                .map_err(|e| StorageError::Database(e.to_string()))?;
75
76        Ok(row.map(|row| row.into()))
77    }
78
79    async fn set_magic_token_used(&self, token: &str) -> Result<(), Self::Error> {
80        sqlx::query("UPDATE magic_links SET used_at = $1 WHERE token = $2")
81            .bind(Utc::now())
82            .bind(token)
83            .execute(&self.pool)
84            .await
85            .map_err(|e| StorageError::Database(e.to_string()))?;
86
87        Ok(())
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    use chrono::Duration;
96    use torii_core::{NewUser, User, UserStorage, storage::MagicLinkStorage};
97    use uuid::Uuid;
98
99    use crate::PostgresStorage;
100
101    async fn create_test_user(storage: &PostgresStorage) -> User {
102        let user = NewUser::builder()
103            .email("test@test.com".to_string())
104            .build()
105            .expect("Failed to build test user");
106        storage
107            .create_user(&user)
108            .await
109            .expect("Failed to create test user")
110    }
111
112    #[tokio::test]
113    async fn test_save_and_get_magic_token() {
114        let storage = crate::tests::setup_test_db().await;
115
116        // Create a user
117        let user = create_test_user(&storage).await;
118
119        let token = MagicToken::new(
120            UserId::new(&user.id.to_string()),
121            Uuid::new_v4().to_string(),
122            None,
123            Utc::now() + Duration::minutes(5),
124            Utc::now(),
125            Utc::now(),
126        );
127        storage
128            .save_magic_token(&token)
129            .await
130            .expect("Failed to save magic token");
131
132        let stored_token = storage
133            .get_magic_token(&token.token)
134            .await
135            .expect("Failed to get magic token");
136        assert!(stored_token.is_some());
137
138        let stored_token = stored_token.unwrap();
139        assert_eq!(stored_token, token);
140    }
141
142    #[tokio::test]
143    async fn test_get_nonexistent_magic_token() {
144        let storage = crate::tests::setup_test_db().await;
145
146        let token = Uuid::new_v4().to_string();
147        let result = storage
148            .get_magic_token(&token)
149            .await
150            .expect("Failed to query magic token");
151        assert!(result.is_none());
152    }
153}