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_ref())
55            .bind(session.user_id.as_ref())
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?.unwrap())
69    }
70
71    async fn get_session(&self, id: &SessionId) -> Result<Option<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_ref())
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(Some(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_ref())
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_ref())
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            .expect("Session should exist");
189        assert_eq!(fetched.user_id, user_id);
190
191        storage
192            .delete_session(&session_id)
193            .await
194            .expect("Failed to delete session");
195        let deleted = storage.get_session(&session_id).await;
196        assert!(deleted.is_err());
197    }
198
199    #[tokio::test]
200    async fn test_postgres_session_cleanup() {
201        let storage = crate::tests::setup_test_db().await;
202        let user_id = UserId::new_random();
203        crate::tests::create_test_user(&storage, &user_id)
204            .await
205            .expect("Failed to create user");
206
207        // Create an already expired session by setting expires_at in the past
208        let session_id = SessionId::new_random();
209        let expired_session = Session {
210            id: SessionId::new_random(),
211            user_id: user_id.clone(),
212            user_agent: None,
213            ip_address: None,
214            created_at: chrono::Utc::now(),
215            updated_at: chrono::Utc::now(),
216            expires_at: chrono::Utc::now() - chrono::Duration::seconds(1),
217        };
218        storage
219            .create_session(&expired_session)
220            .await
221            .expect("Failed to create expired session");
222
223        // Create valid session
224        crate::tests::create_test_session(
225            &storage,
226            &session_id,
227            &user_id,
228            Duration::from_secs(3600),
229        )
230        .await
231        .expect("Failed to create valid session");
232
233        // Run cleanup
234        storage
235            .cleanup_expired_sessions()
236            .await
237            .expect("Failed to cleanup sessions");
238
239        // Verify expired session was removed
240        let expired_session = storage.get_session(&expired_session.id).await;
241        assert!(expired_session.is_err());
242
243        // Verify valid session remains
244        let valid_session = storage
245            .get_session(&session_id)
246            .await
247            .expect("Failed to get valid session")
248            .expect("Session should exist");
249        assert_eq!(valid_session.user_id, user_id);
250    }
251
252    #[tokio::test]
253    async fn test_delete_sessions_for_user() {
254        let storage = crate::tests::setup_test_db().await;
255
256        // Create test users
257        let user_id1 = UserId::new_random();
258        crate::tests::create_test_user(&storage, &user_id1)
259            .await
260            .expect("Failed to create user 1");
261        let user_id2 = UserId::new_random();
262        crate::tests::create_test_user(&storage, &user_id2)
263            .await
264            .expect("Failed to create user 2");
265
266        // Create sessions for user 1
267        let session_id1 = SessionId::new_random();
268        crate::tests::create_test_session(
269            &storage,
270            &session_id1,
271            &user_id1,
272            Duration::from_secs(3600),
273        )
274        .await
275        .expect("Failed to create session 1");
276        let session_id2 = SessionId::new_random();
277        crate::tests::create_test_session(
278            &storage,
279            &session_id2,
280            &user_id1,
281            Duration::from_secs(3600),
282        )
283        .await
284        .expect("Failed to create session 2");
285
286        // Create session for user 2
287        let session_id3 = SessionId::new_random();
288        crate::tests::create_test_session(
289            &storage,
290            &session_id3,
291            &user_id2,
292            Duration::from_secs(3600),
293        )
294        .await
295        .expect("Failed to create session 3");
296
297        // Delete all sessions for user 1
298        storage
299            .delete_sessions_for_user(&user_id1)
300            .await
301            .expect("Failed to delete sessions for user");
302
303        // Verify user 1's sessions are deleted
304        let session1 = storage.get_session(&session_id1).await;
305        assert!(session1.is_err());
306        let session2 = storage.get_session(&session_id2).await;
307        assert!(session2.is_err());
308
309        // Verify user 2's session remains
310        let session3 = storage
311            .get_session(&session_id3)
312            .await
313            .expect("Failed to get session 3");
314        assert!(session3.is_some());
315    }
316}