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 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 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 storage
234 .cleanup_expired_sessions()
235 .await
236 .expect("Failed to cleanup sessions");
237
238 let expired_session = storage.get_session(&expired_session.id).await;
240 assert!(expired_session.is_err());
241
242 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 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 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 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 storage
297 .delete_sessions_for_user(&user_id1)
298 .await
299 .expect("Failed to delete sessions for user");
300
301 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 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}