torii_storage_postgres/
session.rs

1use crate::PostgresStorage;
2use async_trait::async_trait;
3use chrono::{DateTime, Utc};
4use torii_core::error::StorageError;
5use torii_core::session::SessionId;
6use torii_core::{Session, SessionStorage, UserId};
7
8#[derive(Debug, Clone, sqlx::FromRow)]
9pub struct PostgresSession {
10    id: String,
11    user_id: String,
12    user_agent: Option<String>,
13    ip_address: Option<String>,
14    created_at: DateTime<Utc>,
15    updated_at: DateTime<Utc>,
16    expires_at: DateTime<Utc>,
17}
18
19impl From<PostgresSession> for Session {
20    fn from(session: PostgresSession) -> Self {
21        Session::builder()
22            .id(SessionId::new(&session.id))
23            .user_id(UserId::new(&session.user_id))
24            .user_agent(session.user_agent)
25            .ip_address(session.ip_address)
26            .created_at(session.created_at)
27            .updated_at(session.updated_at)
28            .expires_at(session.expires_at)
29            .build()
30            .unwrap()
31    }
32}
33
34impl From<Session> for PostgresSession {
35    fn from(session: Session) -> Self {
36        PostgresSession {
37            id: session.id.into_inner(),
38            user_id: session.user_id.into_inner(),
39            user_agent: session.user_agent,
40            ip_address: session.ip_address,
41            created_at: session.created_at,
42            updated_at: session.updated_at,
43            expires_at: session.expires_at,
44        }
45    }
46}
47
48#[async_trait]
49impl SessionStorage for PostgresStorage {
50    type Error = torii_core::Error;
51
52    async fn create_session(&self, session: &Session) -> Result<Session, Self::Error> {
53        sqlx::query("INSERT INTO sessions (id, user_id, user_agent, ip_address, created_at, updated_at, expires_at) VALUES ($1::uuid, $2::uuid, $3, $4, $5, $6, $7)")
54            .bind(session.id.as_str())
55            .bind(session.user_id.as_str())
56            .bind(&session.user_agent)
57            .bind(&session.ip_address)
58            .bind(session.created_at)
59            .bind(session.updated_at)
60            .bind(session.expires_at)
61            .execute(&self.pool)
62            .await
63            .map_err(|e| {
64                tracing::error!(error = %e, "Failed to create session");
65                StorageError::Database("Failed to create session".to_string())
66            })?;
67
68        Ok(self.get_session(&session.id).await?)
69    }
70
71    async fn get_session(&self, id: &SessionId) -> Result<Session, Self::Error> {
72        let session = sqlx::query_as::<_, PostgresSession>(
73            r#"
74            SELECT id::text, user_id::text, user_agent, ip_address, created_at, updated_at, expires_at
75            FROM sessions
76            WHERE id::text = $1
77            "#,
78        )
79        .bind(id.as_str())
80        .fetch_one(&self.pool)
81        .await
82        .map_err(|e| {
83            tracing::error!(error = %e, "Failed to get session");
84            StorageError::Database("Failed to get session".to_string())
85        })?;
86
87        Ok(session.into())
88    }
89
90    async fn delete_session(&self, id: &SessionId) -> Result<(), Self::Error> {
91        sqlx::query("DELETE FROM sessions WHERE id::text = $1")
92            .bind(id.as_str())
93            .execute(&self.pool)
94            .await
95            .map_err(|e| {
96                tracing::error!(error = %e, "Failed to delete session");
97                StorageError::Database("Failed to delete session".to_string())
98            })?;
99
100        Ok(())
101    }
102
103    async fn cleanup_expired_sessions(&self) -> Result<(), Self::Error> {
104        sqlx::query("DELETE FROM sessions WHERE expires_at < $1")
105            .bind(Utc::now())
106            .execute(&self.pool)
107            .await
108            .map_err(|e| {
109                tracing::error!(error = %e, "Failed to cleanup expired sessions");
110                StorageError::Database("Failed to cleanup expired sessions".to_string())
111            })?;
112
113        Ok(())
114    }
115
116    async fn delete_sessions_for_user(&self, user_id: &UserId) -> Result<(), Self::Error> {
117        sqlx::query("DELETE FROM sessions WHERE user_id::text = $1")
118            .bind(user_id.as_str())
119            .execute(&self.pool)
120            .await
121            .map_err(|e| {
122                tracing::error!(error = %e, "Failed to delete sessions for user");
123                StorageError::Database("Failed to delete sessions for user".to_string())
124            })?;
125
126        Ok(())
127    }
128}
129
130#[cfg(test)]
131mod test {
132    use super::*;
133    use std::time::Duration;
134    use torii_core::session::SessionId;
135    use torii_core::{Session, UserId, UserStorage};
136
137    #[tokio::test]
138    async fn test_postgres_storage() {
139        let storage = crate::tests::setup_test_db().await;
140        let user_id = UserId::new_random();
141        let user = crate::tests::create_test_user(&storage, &user_id)
142            .await
143            .expect("Failed to create user");
144        assert_eq!(user.email, format!("test{}@example.com", user_id));
145
146        let fetched = storage
147            .get_user(&user_id)
148            .await
149            .expect("Failed to get user");
150        assert_eq!(
151            fetched.expect("User should exist").email,
152            format!("test{}@example.com", user_id)
153        );
154
155        storage
156            .delete_user(&user_id)
157            .await
158            .expect("Failed to delete user");
159        let deleted = storage
160            .get_user(&user_id)
161            .await
162            .expect("Failed to get user");
163        assert!(deleted.is_none());
164    }
165
166    #[tokio::test]
167    async fn test_postgres_session_storage() {
168        let storage = crate::tests::setup_test_db().await;
169        let user_id = UserId::new_random();
170        let session_id = SessionId::new_random();
171        crate::tests::create_test_user(&storage, &user_id)
172            .await
173            .expect("Failed to create user");
174
175        let _session = crate::tests::create_test_session(
176            &storage,
177            &session_id,
178            &user_id,
179            Duration::from_secs(1000),
180        )
181        .await
182        .expect("Failed to create session");
183
184        let fetched = storage
185            .get_session(&session_id)
186            .await
187            .expect("Failed to get session");
188        assert_eq!(fetched.user_id, user_id);
189
190        storage
191            .delete_session(&session_id)
192            .await
193            .expect("Failed to delete session");
194        let deleted = storage.get_session(&session_id).await;
195        assert!(deleted.is_err());
196    }
197
198    #[tokio::test]
199    async fn test_postgres_session_cleanup() {
200        let storage = crate::tests::setup_test_db().await;
201        let user_id = UserId::new_random();
202        crate::tests::create_test_user(&storage, &user_id)
203            .await
204            .expect("Failed to create user");
205
206        // Create an already expired session by setting expires_at in the past
207        let session_id = SessionId::new_random();
208        let expired_session = Session {
209            id: SessionId::new_random(),
210            user_id: user_id.clone(),
211            user_agent: None,
212            ip_address: None,
213            created_at: chrono::Utc::now(),
214            updated_at: chrono::Utc::now(),
215            expires_at: chrono::Utc::now() - chrono::Duration::seconds(1),
216        };
217        storage
218            .create_session(&expired_session)
219            .await
220            .expect("Failed to create expired session");
221
222        // Create valid session
223        crate::tests::create_test_session(
224            &storage,
225            &session_id,
226            &user_id,
227            Duration::from_secs(3600),
228        )
229        .await
230        .expect("Failed to create valid session");
231
232        // Run cleanup
233        storage
234            .cleanup_expired_sessions()
235            .await
236            .expect("Failed to cleanup sessions");
237
238        // Verify expired session was removed
239        let expired_session = storage.get_session(&expired_session.id).await;
240        assert!(expired_session.is_err());
241
242        // Verify valid session remains
243        let valid_session = storage
244            .get_session(&session_id)
245            .await
246            .expect("Failed to get valid session");
247        assert_eq!(valid_session.user_id, user_id);
248    }
249
250    #[tokio::test]
251    async fn test_delete_sessions_for_user() {
252        let storage = crate::tests::setup_test_db().await;
253
254        // Create test users
255        let user_id1 = UserId::new_random();
256        crate::tests::create_test_user(&storage, &user_id1)
257            .await
258            .expect("Failed to create user 1");
259        let user_id2 = UserId::new_random();
260        crate::tests::create_test_user(&storage, &user_id2)
261            .await
262            .expect("Failed to create user 2");
263
264        // Create sessions for user 1
265        let session_id1 = SessionId::new_random();
266        crate::tests::create_test_session(
267            &storage,
268            &session_id1,
269            &user_id1,
270            Duration::from_secs(3600),
271        )
272        .await
273        .expect("Failed to create session 1");
274        let session_id2 = SessionId::new_random();
275        crate::tests::create_test_session(
276            &storage,
277            &session_id2,
278            &user_id1,
279            Duration::from_secs(3600),
280        )
281        .await
282        .expect("Failed to create session 2");
283
284        // Create session for user 2
285        let session_id3 = SessionId::new_random();
286        crate::tests::create_test_session(
287            &storage,
288            &session_id3,
289            &user_id2,
290            Duration::from_secs(3600),
291        )
292        .await
293        .expect("Failed to create session 3");
294
295        // Delete all sessions for user 1
296        storage
297            .delete_sessions_for_user(&user_id1)
298            .await
299            .expect("Failed to delete sessions for user");
300
301        // Verify user 1's sessions are deleted
302        let session1 = storage.get_session(&session_id1).await;
303        assert!(session1.is_err());
304        let session2 = storage.get_session(&session_id2).await;
305        assert!(session2.is_err());
306
307        // Verify user 2's session remains
308        let session3 = storage
309            .get_session(&session_id3)
310            .await
311            .expect("Failed to get session 3");
312        assert_eq!(session3.user_id, user_id2);
313    }
314}