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 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_test_session(&storage, "valid", "1", Duration::from_secs(3600))
222 .await
223 .expect("Failed to create valid session");
224
225 storage
227 .cleanup_expired_sessions()
228 .await
229 .expect("Failed to cleanup sessions");
230
231 let expired_session = storage.get_session(&SessionToken::new("expired")).await;
233 assert!(expired_session.is_err());
234
235 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_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_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_test_session(&storage, "session3", "2", Duration::from_secs(3600))
267 .await
268 .expect("Failed to create session 3");
269
270 storage
272 .delete_sessions_for_user(&UserId::new("1"))
273 .await
274 .expect("Failed to delete sessions for user");
275
276 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 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}