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 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 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 storage
235 .cleanup_expired_sessions()
236 .await
237 .expect("Failed to cleanup sessions");
238
239 let expired_session = storage.get_session(&expired_session.id).await;
241 assert!(expired_session.is_err());
242
243 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 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 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 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 storage
299 .delete_sessions_for_user(&user_id1)
300 .await
301 .expect("Failed to delete sessions for user");
302
303 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 let session3 = storage
311 .get_session(&session_id3)
312 .await
313 .expect("Failed to get session 3");
314 assert!(session3.is_some());
315 }
316}