Skip to main content

shared/domain/session/
model.rs

1use chrono::{DateTime, Duration, Utc};
2use mongodb::bson;
3use serde::{Deserialize, Serialize};
4use utoipa::ToSchema;
5
6use super::super::serde::{
7    deserialize_datetime, deserialize_object_id, deserialize_object_id_as_string,
8    deserialize_option_datetime,
9};
10use crate::domain::query::IntoBsonDocument;
11
12#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
13#[schema(example = json!({"id": Some(String::default()), "user_id": String::default(), "issued_at": "2026-02-19T22:42:23.467Z", "expires_at": "2026-02-19T22:42:23.467Z", "used_at": Some("2026-02-19T22:42:23.467Z"), "token": String::default()}))]
14pub struct Session {
15    #[serde(
16        rename = "_id",
17        default,
18        skip_serializing_if = "Option::is_none",
19        deserialize_with = "deserialize_object_id_as_string"
20    )]
21    pub id: Option<String>,
22
23    #[serde(
24        rename = "userId",
25        default,
26        // serialize_with = "serialize_object_id_as_string",
27        deserialize_with = "deserialize_object_id"
28    )]
29    pub user_id: String,
30
31    #[serde(rename = "issuedAt", deserialize_with = "deserialize_datetime")]
32    pub issued_at: DateTime<Utc>,
33    #[serde(rename = "expiresAt", deserialize_with = "deserialize_datetime")]
34    pub expires_at: DateTime<Utc>,
35    #[serde(rename = "usedAt", deserialize_with = "deserialize_option_datetime")]
36    pub used_at: Option<DateTime<Utc>>,
37
38    pub token: String,
39    pub roles: Vec<String>,
40    pub permissions: Vec<String>,
41}
42
43// Custom FromRow for SQLite JSON columns
44impl<'r> sqlx::FromRow<'r, sqlx::sqlite::SqliteRow> for Session {
45    fn from_row(row: &'r sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
46        use sqlx::Row;
47
48        let roles_str: String = row.try_get("roles")?;
49        let permissions_str: String = row.try_get("permissions")?;
50
51        Ok(Session {
52            id: row.try_get("id")?,
53            user_id: row.try_get("userId")?,
54            token: row.try_get("token")?,
55            issued_at: row.try_get("issuedAt")?,
56            expires_at: row.try_get("expiresAt")?,
57            used_at: row.try_get("usedAt")?,
58            roles: serde_json::from_str(&roles_str).map_err(|e| sqlx::Error::ColumnDecode {
59                index: "roles".to_string(),
60                source: Box::new(e),
61            })?,
62            permissions: serde_json::from_str(&permissions_str).map_err(|e| {
63                sqlx::Error::ColumnDecode {
64                    index: "permissions".to_string(),
65                    source: Box::new(e),
66                }
67            })?,
68        })
69    }
70}
71
72// Postgres: roles/permissions are native TEXT[] arrays
73impl<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> for Session {
74    fn from_row(row: &'r sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
75        use sqlx::Row;
76
77        Ok(Session {
78            id: row.try_get("id")?,
79            user_id: row.try_get("userId")?,
80            token: row.try_get("token")?,
81            issued_at: row.try_get("issuedAt")?,
82            expires_at: row.try_get("expiresAt")?,
83            used_at: row.try_get("usedAt")?,
84            roles: row.try_get("roles")?,             // native TEXT[]
85            permissions: row.try_get("permissions")?, // native TEXT[]
86        })
87    }
88}
89
90impl Default for Session {
91    fn default() -> Self {
92        Self {
93            id: None,
94            user_id: String::default(),
95            issued_at: Utc::now(),
96            expires_at: Utc::now() + Duration::hours(24),
97            used_at: None,
98            token: String::default(),
99            roles: vec![],
100            permissions: vec![],
101        }
102    }
103}
104
105impl Session {
106    pub fn from_request(session: Session) -> Self {
107        session
108    }
109}
110
111impl Session {
112    pub fn with_user_id(mut self, user_id: &str) -> Self {
113        self.user_id = user_id.into();
114        self
115    }
116    pub fn with_token(mut self, token: &str) -> Self {
117        self.token = token.into();
118        self
119    }
120    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
121        self.permissions = permissions;
122        self
123    }
124    pub fn with_role(mut self, role: &str) -> Self {
125        self.roles = vec![role.into()];
126        self
127    }
128}
129impl Session {
130    pub fn id(&self) -> Result<&str, crate::error::CoreError> {
131        self.id.as_deref().ok_or_else(|| {
132            tracing::error!(
133                error_code = "ValidationError::Malformed",
134                "Unexpected null/missing data"
135            );
136            crate::error::CoreError::Validation(crate::error::ValidationError::Malformed {
137                field: crate::error::CredentialField::ObjectId,
138            })
139        })
140    }
141}
142
143impl IntoBsonDocument for Session {
144    fn into_bson_document(self) -> Result<bson::Document, bson::ser::Error> {
145        let mut doc = bson::to_document(&self)?;
146
147        for key in &["expiresAt", "issuedAt", "usedAt"] {
148            if let Some(bson::Bson::String(s)) = doc.get(*key).cloned()
149                && let Ok(dt) = DateTime::parse_from_rfc3339(&s)
150            {
151                doc.insert(
152                    *key,
153                    bson::Bson::DateTime(bson::DateTime::from_millis(dt.timestamp_millis())),
154                );
155            }
156        }
157
158        Ok(doc)
159    }
160}