Skip to main content

systemprompt_users/repository/user/
session.rs

1//! User session-row persistence.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use chrono::Utc;
7use systemprompt_identifiers::{SessionId, UserId};
8
9use crate::error::Result;
10use crate::models::{UserSession, UserSessionRow};
11use crate::repository::{MAX_PAGE_SIZE, UserRepository};
12
13impl UserRepository {
14    pub async fn list_sessions(&self, user_id: &UserId) -> Result<Vec<UserSession>> {
15        let rows = sqlx::query_as!(
16            UserSessionRow,
17            r#"
18            SELECT session_id, user_id as "user_id: UserId", ip_address, user_agent, device_type,
19                   started_at, last_activity_at, ended_at
20            FROM user_sessions
21            WHERE user_id = $1
22            ORDER BY last_activity_at DESC
23            "#,
24            user_id.as_str()
25        )
26        .fetch_all(&*self.pool)
27        .await?;
28
29        Ok(rows.into_iter().map(UserSession::from).collect())
30    }
31
32    pub async fn list_active_sessions(&self, user_id: &UserId) -> Result<Vec<UserSession>> {
33        let rows = sqlx::query_as!(
34            UserSessionRow,
35            r#"
36            SELECT session_id, user_id as "user_id: UserId", ip_address, user_agent, device_type,
37                   started_at, last_activity_at, ended_at
38            FROM user_sessions
39            WHERE user_id = $1 AND ended_at IS NULL
40            ORDER BY last_activity_at DESC
41            "#,
42            user_id.as_str()
43        )
44        .fetch_all(&*self.pool)
45        .await?;
46
47        Ok(rows.into_iter().map(UserSession::from).collect())
48    }
49
50    pub async fn list_recent_sessions(
51        &self,
52        user_id: &UserId,
53        limit: i64,
54    ) -> Result<Vec<UserSession>> {
55        let safe_limit = limit.min(MAX_PAGE_SIZE);
56        let rows = sqlx::query_as!(
57            UserSessionRow,
58            r#"
59            SELECT session_id, user_id as "user_id: UserId", ip_address, user_agent, device_type,
60                   started_at, last_activity_at, ended_at
61            FROM user_sessions
62            WHERE user_id = $1
63            ORDER BY last_activity_at DESC
64            LIMIT $2
65            "#,
66            user_id.as_str(),
67            safe_limit
68        )
69        .fetch_all(&*self.pool)
70        .await?;
71
72        Ok(rows.into_iter().map(UserSession::from).collect())
73    }
74
75    pub async fn session_exists(&self, session_id: &SessionId) -> Result<bool> {
76        let exists = sqlx::query_scalar!(
77            r#"SELECT EXISTS(SELECT 1 FROM user_sessions WHERE session_id = $1 AND ended_at IS NULL) as "exists!""#,
78            session_id.as_str()
79        )
80        .fetch_one(&*self.pool)
81        .await?;
82
83        Ok(exists)
84    }
85
86    pub async fn end_session(&self, session_id: &SessionId) -> Result<bool> {
87        let result = sqlx::query!(
88            r#"
89            UPDATE user_sessions
90            SET ended_at = $1
91            WHERE session_id = $2 AND ended_at IS NULL
92            "#,
93            Utc::now(),
94            session_id.as_str()
95        )
96        .execute(&*self.write_pool)
97        .await?;
98
99        Ok(result.rows_affected() > 0)
100    }
101
102    pub async fn end_all_sessions(&self, user_id: &UserId) -> Result<u64> {
103        let result = sqlx::query!(
104            r#"
105            UPDATE user_sessions
106            SET ended_at = $1
107            WHERE user_id = $2 AND ended_at IS NULL
108            "#,
109            Utc::now(),
110            user_id.as_str()
111        )
112        .execute(&*self.write_pool)
113        .await?;
114
115        Ok(result.rows_affected())
116    }
117}