use crate::db::repository::traits::SessionsRepository; use async_trait::async_trait; use sqlx::PgPool; use crate::db::models::Session; pub struct PostgresSessionsRepository { pub pool: PgPool, } #[async_trait] impl SessionsRepository for PostgresSessionsRepository { async fn create( &self, id: &str, user_id: &str, token_hash: &str, ip_address: Option<&str>, user_agent: Option<&str>, expires_at: &str, ) -> Result { sqlx::query_as::<_, Session>( r#" INSERT INTO sessions (id, user_id, token_hash, ip_address, user_agent, expires_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * "#, ) .bind(id) .bind(user_id) .bind(token_hash) .bind(ip_address) .bind(user_agent) .bind(expires_at) .fetch_one(&self.pool) .await } async fn find_by_token_hash(&self, token_hash: &str) -> Result, sqlx::Error> { sqlx::query_as::<_, Session>("SELECT * FROM sessions WHERE token_hash = $1 AND revoked = 0") .bind(token_hash) .fetch_optional(&self.pool) .await } async fn revoke(&self, id: &str) -> Result<(), sqlx::Error> { sqlx::query("UPDATE sessions SET revoked = 1 WHERE id = $1") .bind(id) .execute(&self.pool) .await?; Ok(()) } async fn revoke_all_for_user(&self, user_id: &str) -> Result<(), sqlx::Error> { sqlx::query("UPDATE sessions SET revoked = 1 WHERE user_id = $1") .bind(user_id) .execute(&self.pool) .await?; Ok(()) } async fn update_last_seen(&self, id: &str) -> Result<(), sqlx::Error> { sqlx::query( "UPDATE sessions SET last_seen_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = $1", ) .bind(id) .execute(&self.pool) .await?; Ok(()) } /// List active (non-revoked, non-expired) sessions for a user. async fn list_active_for_user(&self, user_id: &str) -> Result, sqlx::Error> { sqlx::query_as::<_, Session>( r#" SELECT * FROM sessions WHERE user_id = $1 AND revoked = 0 AND expires_at >= strftime('%Y-%m-%dT%H:%M:%SZ', 'now') ORDER BY last_seen_at DESC "#, ) .bind(user_id) .fetch_all(&self.pool) .await } async fn list_all_active(&self) -> Result, sqlx::Error> { unimplemented!() } /// Count active sessions system-wide. async fn count_active(&self) -> Result { let row: (i64,) = sqlx::query_as( r#" SELECT COUNT(*) FROM sessions WHERE revoked = 0 AND expires_at >= strftime('%Y-%m-%dT%H:%M:%SZ', 'now') "#, ) .fetch_one(&self.pool) .await?; Ok(row.0) } /// Delete sessions that are expired or revoked. Called once at startup. async fn cleanup_expired(&self) -> Result { let result = sqlx::query( r#" DELETE FROM sessions WHERE revoked = 1 OR expires_at < strftime('%Y-%m-%dT%H:%M:%SZ', 'now') "#, ) .execute(&self.pool) .await?; Ok(result.rows_affected()) } async fn revoke_others(&self, _user_id: &str, _except_id: &str) -> Result { unimplemented!() } }