Skip to main content

torii_storage_sqlite/
session.rs

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