1use crate::{
2 models::{
3 InsertQueryBuilder,
4 user::{AuthMethod, GetAuthMethod},
5 },
6 prelude::*,
7};
8use garde::Validate;
9use rand::distr::SampleString;
10use serde::{Deserialize, Serialize};
11use sha2::Digest;
12use sqlx::{Row, postgres::PgRow};
13use std::{
14 collections::BTreeMap,
15 sync::{Arc, LazyLock},
16};
17use utoipa::ToSchema;
18
19#[derive(Serialize, Deserialize, Clone)]
20pub struct UserSession {
21 pub uuid: uuid::Uuid,
22
23 pub ip: sqlx::types::ipnetwork::IpNetwork,
24 pub user_agent: String,
25
26 pub last_used: chrono::NaiveDateTime,
27 pub created: chrono::NaiveDateTime,
28}
29
30impl BaseModel for UserSession {
31 const NAME: &'static str = "user_session";
32
33 #[inline]
34 fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
35 let prefix = prefix.unwrap_or_default();
36
37 BTreeMap::from([
38 (
39 "user_sessions.uuid",
40 compact_str::format_compact!("{prefix}uuid"),
41 ),
42 (
43 "user_sessions.ip",
44 compact_str::format_compact!("{prefix}ip"),
45 ),
46 (
47 "user_sessions.user_agent",
48 compact_str::format_compact!("{prefix}user_agent"),
49 ),
50 (
51 "user_sessions.last_used",
52 compact_str::format_compact!("{prefix}last_used"),
53 ),
54 (
55 "user_sessions.created",
56 compact_str::format_compact!("{prefix}created"),
57 ),
58 ])
59 }
60
61 #[inline]
62 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
63 let prefix = prefix.unwrap_or_default();
64
65 Ok(Self {
66 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
67 ip: row.try_get(compact_str::format_compact!("{prefix}ip").as_str())?,
68 user_agent: row.try_get(compact_str::format_compact!("{prefix}user_agent").as_str())?,
69 last_used: row.try_get(compact_str::format_compact!("{prefix}last_used").as_str())?,
70 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
71 })
72 }
73}
74
75impl UserSession {
76 pub async fn by_user_uuid_uuid(
77 database: &crate::database::Database,
78 user_uuid: uuid::Uuid,
79 uuid: uuid::Uuid,
80 ) -> Result<Option<Self>, crate::database::DatabaseError> {
81 let row = sqlx::query(&format!(
82 r#"
83 SELECT {}
84 FROM user_sessions
85 WHERE user_sessions.user_uuid = $1 AND user_sessions.uuid = $2
86 "#,
87 Self::columns_sql(None)
88 ))
89 .bind(user_uuid)
90 .bind(uuid)
91 .fetch_optional(database.read())
92 .await?;
93
94 row.try_map(|row| Self::map(None, &row))
95 }
96
97 pub async fn by_user_uuid_with_pagination(
98 database: &crate::database::Database,
99 user_uuid: uuid::Uuid,
100 page: i64,
101 per_page: i64,
102 search: Option<&str>,
103 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
104 let offset = (page - 1) * per_page;
105
106 let rows = sqlx::query(&format!(
107 r#"
108 SELECT {}, COUNT(*) OVER() AS total_count
109 FROM user_sessions
110 WHERE user_sessions.user_uuid = $1 AND ($2 IS NULL OR user_sessions.user_agent ILIKE '%' || $2 || '%')
111 ORDER BY user_sessions.created DESC
112 LIMIT $3 OFFSET $4
113 "#,
114 Self::columns_sql(None)
115 ))
116 .bind(user_uuid)
117 .bind(search)
118 .bind(per_page)
119 .bind(offset)
120 .fetch_all(database.read())
121 .await?;
122
123 Ok(super::Pagination {
124 total: rows
125 .first()
126 .map_or(Ok(0), |row| row.try_get("total_count"))?,
127 per_page,
128 page,
129 data: rows
130 .into_iter()
131 .map(|row| Self::map(None, &row))
132 .try_collect_vec()?,
133 })
134 }
135
136 pub async fn delete_unused(database: &crate::database::Database) -> Result<u64, sqlx::Error> {
137 Ok(sqlx::query(
138 r#"
139 DELETE FROM user_sessions
140 WHERE user_sessions.last_used < NOW() - INTERVAL '30 days'
141 "#,
142 )
143 .execute(database.write())
144 .await?
145 .rows_affected())
146 }
147
148 pub async fn update_last_used(
149 &self,
150 database: &Arc<crate::database::Database>,
151 ip: impl Into<sqlx::types::ipnetwork::IpNetwork>,
152 user_agent: &str,
153 ) {
154 let uuid = self.uuid;
155 let now = chrono::Utc::now().naive_utc();
156 let user_agent = crate::utils::slice_up_to(user_agent, 255).to_string();
157 let ip = ip.into();
158
159 database
160 .batch_action("update_user_session", uuid, {
161 let database = database.clone();
162
163 async move {
164 sqlx::query!(
165 "UPDATE user_sessions
166 SET ip = $2, user_agent = $3, last_used = $4
167 WHERE user_sessions.uuid = $1",
168 uuid,
169 ip,
170 user_agent,
171 now
172 )
173 .execute(database.write())
174 .await?;
175
176 Ok(())
177 }
178 })
179 .await;
180 }
181
182 #[inline]
183 pub fn into_api_object(self, auth: &GetAuthMethod) -> ApiUserSession {
184 ApiUserSession {
185 uuid: self.uuid,
186 ip: self.ip.ip().to_string(),
187 user_agent: self.user_agent,
188 is_using: match &**auth {
189 AuthMethod::Session(session) => session.uuid == self.uuid,
190 _ => false,
191 },
192 last_used: self.last_used.and_utc(),
193 created: self.created.and_utc(),
194 }
195 }
196}
197
198#[derive(ToSchema, Deserialize, Validate)]
199pub struct CreateUserSessionOptions {
200 #[garde(skip)]
201 pub user_uuid: uuid::Uuid,
202 #[garde(skip)]
203 #[schema(value_type = String)]
204 pub ip: sqlx::types::ipnetwork::IpNetwork,
205 #[garde(length(chars, min = 1, max = 1024))]
206 #[schema(min_length = 1, max_length = 1024)]
207 pub user_agent: compact_str::CompactString,
208}
209
210#[async_trait::async_trait]
211impl CreatableModel for UserSession {
212 type CreateOptions<'a> = CreateUserSessionOptions;
213 type CreateResult = String;
214
215 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
216 static CREATE_LISTENERS: LazyLock<CreateListenerList<UserSession>> =
217 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
218
219 &CREATE_LISTENERS
220 }
221
222 async fn create(
223 state: &crate::State,
224 mut options: Self::CreateOptions<'_>,
225 ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
226 options.validate()?;
227
228 let mut transaction = state.database.write().begin().await?;
229
230 let mut query_builder = InsertQueryBuilder::new("user_sessions");
231
232 Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
233 .await?;
234
235 let key_id = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
236
237 let mut hash = sha2::Sha256::new();
238 hash.update(chrono::Utc::now().timestamp().to_le_bytes());
239 hash.update(options.user_uuid.to_bytes_le());
240 let hash = format!("{:x}", hash.finalize());
241
242 query_builder
243 .set("user_uuid", options.user_uuid)
244 .set("key_id", key_id.clone())
245 .set_expr("key", "crypt($1, gen_salt('xdes', 321))", vec![&hash])
246 .set("ip", options.ip)
247 .set("user_agent", &options.user_agent);
248
249 query_builder.execute(&mut *transaction).await?;
250
251 transaction.commit().await?;
252
253 Ok(format!("{key_id}:{hash}"))
254 }
255}
256
257#[async_trait::async_trait]
258impl DeletableModel for UserSession {
259 type DeleteOptions = ();
260
261 fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
262 static DELETE_LISTENERS: LazyLock<DeleteListenerList<UserSession>> =
263 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
264
265 &DELETE_LISTENERS
266 }
267
268 async fn delete(
269 &self,
270 state: &crate::State,
271 options: Self::DeleteOptions,
272 ) -> Result<(), anyhow::Error> {
273 let mut transaction = state.database.write().begin().await?;
274
275 self.run_delete_handlers(&options, state, &mut transaction)
276 .await?;
277
278 sqlx::query(
279 r#"
280 DELETE FROM user_sessions
281 WHERE user_sessions.uuid = $1
282 "#,
283 )
284 .bind(self.uuid)
285 .execute(&mut *transaction)
286 .await?;
287
288 transaction.commit().await?;
289
290 Ok(())
291 }
292}
293
294#[derive(ToSchema, Serialize, Deserialize)]
295#[schema(title = "UserSession")]
296pub struct ApiUserSession {
297 pub uuid: uuid::Uuid,
298
299 pub ip: String,
300 pub user_agent: String,
301
302 pub is_using: bool,
303
304 pub last_used: chrono::DateTime<chrono::Utc>,
305 pub created: chrono::DateTime<chrono::Utc>,
306}