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