1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use base64::Engine;
6use garde::Validate;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow};
9use std::{
10 collections::BTreeMap,
11 sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(Serialize, Deserialize)]
16pub struct UserSecurityKey {
17 pub uuid: uuid::Uuid,
18
19 pub name: compact_str::CompactString,
20
21 pub passkey: Option<webauthn_rs::prelude::Passkey>,
22 pub registration: Option<webauthn_rs::prelude::PasskeyRegistration>,
23
24 pub last_used: Option<chrono::NaiveDateTime>,
25 pub created: chrono::NaiveDateTime,
26
27 extension_data: super::ModelExtensionData,
28}
29
30impl BaseModel for UserSecurityKey {
31 const NAME: &'static str = "user_security_key";
32
33 fn get_extension_list() -> &'static super::ModelExtensionList {
34 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
35 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
36
37 &EXTENSIONS
38 }
39
40 fn get_extension_data(&self) -> &super::ModelExtensionData {
41 &self.extension_data
42 }
43
44 #[inline]
45 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
46 let prefix = prefix.unwrap_or_default();
47
48 BTreeMap::from([
49 (
50 "user_security_keys.uuid",
51 compact_str::format_compact!("{prefix}uuid"),
52 ),
53 (
54 "user_security_keys.name",
55 compact_str::format_compact!("{prefix}name"),
56 ),
57 (
58 "user_security_keys.passkey",
59 compact_str::format_compact!("{prefix}passkey"),
60 ),
61 (
62 "user_security_keys.registration",
63 compact_str::format_compact!("{prefix}registration"),
64 ),
65 (
66 "user_security_keys.last_used",
67 compact_str::format_compact!("{prefix}last_used"),
68 ),
69 (
70 "user_security_keys.created",
71 compact_str::format_compact!("{prefix}created"),
72 ),
73 ])
74 }
75
76 #[inline]
77 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
78 let prefix = prefix.unwrap_or_default();
79
80 Ok(Self {
81 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
82 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
83 passkey: if row
84 .try_get::<serde_json::Value, _>(
85 compact_str::format_compact!("{prefix}passkey").as_str(),
86 )
87 .is_ok()
88 {
89 serde_json::from_value(
90 row.try_get(compact_str::format_compact!("{prefix}passkey").as_str())?,
91 )
92 .ok()
93 } else {
94 None
95 },
96 registration: if row
97 .try_get::<serde_json::Value, _>(
98 compact_str::format_compact!("{prefix}registration").as_str(),
99 )
100 .is_ok()
101 {
102 serde_json::from_value(
103 row.try_get(compact_str::format_compact!("{prefix}registration").as_str())?,
104 )
105 .ok()
106 } else {
107 None
108 },
109 last_used: row.try_get(compact_str::format_compact!("{prefix}last_used").as_str())?,
110 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
111 extension_data: Self::map_extensions(prefix, row)?,
112 })
113 }
114}
115
116impl UserSecurityKey {
117 pub async fn by_user_uuid_uuid(
118 database: &crate::database::Database,
119 user_uuid: uuid::Uuid,
120 uuid: uuid::Uuid,
121 ) -> Result<Option<Self>, crate::database::DatabaseError> {
122 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
123 r#"
124 SELECT {}
125 FROM user_security_keys
126 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.uuid = $2
127 "#,
128 Self::columns_sql(None)
129 )))
130 .bind(user_uuid)
131 .bind(uuid)
132 .fetch_optional(database.read())
133 .await?;
134
135 row.try_map(|row| Self::map(None, &row))
136 }
137
138 pub async fn by_user_uuid_with_pagination(
139 database: &crate::database::Database,
140 user_uuid: uuid::Uuid,
141 page: i64,
142 per_page: i64,
143 search: Option<&str>,
144 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
145 let offset = (page - 1) * per_page;
146
147 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
148 r#"
149 SELECT {}, COUNT(*) OVER() AS total_count
150 FROM user_security_keys
151 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.passkey IS NOT NULL AND ($2 IS NULL OR user_security_keys.name ILIKE '%' || $2 || '%')
152 ORDER BY user_security_keys.created
153 LIMIT $3 OFFSET $4
154 "#,
155 Self::columns_sql(None)
156 )))
157 .bind(user_uuid)
158 .bind(search)
159 .bind(per_page)
160 .bind(offset)
161 .fetch_all(database.read())
162 .await?;
163
164 Ok(super::Pagination {
165 total: rows
166 .first()
167 .map_or(Ok(0), |row| row.try_get("total_count"))?,
168 per_page,
169 page,
170 data: rows
171 .into_iter()
172 .map(|row| Self::map(None, &row))
173 .try_collect_vec()?,
174 })
175 }
176
177 pub async fn delete_unconfigured_by_user_uuid_name(
178 database: &crate::database::Database,
179 user_uuid: uuid::Uuid,
180 name: &str,
181 ) -> Result<(), sqlx::Error> {
182 sqlx::query(
183 r#"
184 DELETE FROM user_security_keys
185 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.name = $2 AND user_security_keys.passkey IS NULL
186 "#,
187 )
188 .bind(user_uuid)
189 .bind(name)
190 .execute(database.write())
191 .await?;
192
193 Ok(())
194 }
195
196 pub async fn delete_unconfigured(
197 database: &crate::database::Database,
198 timeout_seconds: i64,
199 ) -> Result<u64, sqlx::Error> {
200 Ok(sqlx::query(
201 r#"
202 DELETE FROM user_security_keys
203 WHERE user_security_keys.created < $1 AND user_security_keys.passkey IS NULL
204 "#,
205 )
206 .bind(chrono::Utc::now().naive_utc() - chrono::Duration::seconds(timeout_seconds))
207 .execute(database.write())
208 .await?
209 .rows_affected())
210 }
211
212 pub async fn count_by_user_uuid(
213 database: &crate::database::Database,
214 user_uuid: uuid::Uuid,
215 ) -> Result<i64, sqlx::Error> {
216 sqlx::query_scalar(
217 r#"
218 SELECT COUNT(*)
219 FROM user_security_keys
220 WHERE user_security_keys.user_uuid = $1
221 "#,
222 )
223 .bind(user_uuid)
224 .fetch_one(database.read())
225 .await
226 }
227
228 pub async fn count_usable_by_user_uuid_for_update(
231 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
232 user_uuid: uuid::Uuid,
233 ) -> Result<i64, sqlx::Error> {
234 sqlx::query_scalar(
235 r#"
236 SELECT COUNT(*)
237 FROM (
238 SELECT user_security_keys.uuid
239 FROM user_security_keys
240 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.passkey IS NOT NULL
241 FOR UPDATE
242 ) AS usable_keys
243 "#,
244 )
245 .bind(user_uuid)
246 .fetch_one(&mut **transaction)
247 .await
248 }
249}
250
251#[async_trait::async_trait]
252impl IntoApiObject for UserSecurityKey {
253 type ApiObject = ApiUserSecurityKey;
254 type ExtraArgs<'a> = ();
255
256 async fn into_api_object<'a>(
257 self,
258 state: &crate::State,
259 _args: Self::ExtraArgs<'a>,
260 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
261 let api_object = ApiUserSecurityKey::init_hooks(&self, state).await?;
262
263 let api_object = finish_extendible!(
264 ApiUserSecurityKey {
265 uuid: self.uuid,
266 name: self.name,
267 credential_id: self.passkey.as_ref().map_or_else(
268 || "".to_string(),
269 |pk| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(pk.cred_id()),
270 ),
271 last_used: self.last_used.map(|dt| dt.and_utc()),
272 created: self.created.and_utc(),
273 },
274 api_object,
275 state
276 )?;
277
278 Ok(api_object)
279 }
280}
281
282#[derive(ToSchema, Deserialize, Validate)]
283pub struct CreateUserSecurityKeyOptions {
284 #[garde(skip)]
285 pub user_uuid: uuid::Uuid,
286
287 #[garde(length(chars, min = 3, max = 31))]
288 #[schema(min_length = 3, max_length = 31)]
289 pub name: compact_str::CompactString,
290
291 #[garde(skip)]
292 #[schema(value_type = serde_json::Value)]
293 pub registration: webauthn_rs::prelude::PasskeyRegistration,
294}
295
296#[async_trait::async_trait]
297impl CreatableModel for UserSecurityKey {
298 type CreateOptions<'a> = CreateUserSecurityKeyOptions;
299 type CreateResult = Self;
300
301 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
302 static CREATE_LISTENERS: LazyLock<CreateListenerList<UserSecurityKey>> =
303 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
304
305 &CREATE_LISTENERS
306 }
307
308 async fn create_with_transaction(
309 state: &crate::State,
310 mut options: Self::CreateOptions<'_>,
311 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
312 ) -> Result<Self, crate::database::DatabaseError> {
313 options.validate()?;
314
315 let mut query_builder = InsertQueryBuilder::new("user_security_keys");
316
317 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
318
319 query_builder
320 .set("user_uuid", options.user_uuid)
321 .set("name", &options.name)
322 .set(
323 "credential_id",
324 rand::random_iter().take(16).collect::<Vec<u8>>(),
325 )
326 .set("registration", serde_json::to_value(&options.registration)?);
327
328 let row = query_builder
329 .returning(&Self::columns_sql(None))
330 .fetch_one(&mut **transaction)
331 .await?;
332 let mut security_key = Self::map(None, &row)?;
333
334 Self::run_after_create_handlers(&mut security_key, &options, state, transaction).await?;
335
336 Ok(security_key)
337 }
338}
339
340#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
341pub struct UpdateUserSecurityKeyOptions {
342 #[garde(length(chars, min = 3, max = 31))]
343 #[schema(min_length = 3, max_length = 31)]
344 pub name: Option<compact_str::CompactString>,
345}
346
347#[async_trait::async_trait]
348impl UpdatableModel for UserSecurityKey {
349 type UpdateOptions = UpdateUserSecurityKeyOptions;
350
351 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
352 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserSecurityKey>> =
353 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
354
355 &UPDATE_LISTENERS
356 }
357
358 async fn update_with_transaction(
359 &mut self,
360 state: &crate::State,
361 mut options: Self::UpdateOptions,
362 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
363 ) -> Result<(), crate::database::DatabaseError> {
364 options.validate()?;
365
366 let mut query_builder = UpdateQueryBuilder::new("user_security_keys");
367
368 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
369 .await?;
370
371 query_builder
372 .set("name", options.name.as_ref())
373 .where_eq("uuid", self.uuid);
374
375 query_builder.execute(&mut **transaction).await?;
376
377 if let Some(name) = options.name {
378 self.name = name;
379 }
380
381 self.run_after_update_handlers(state, transaction).await?;
382
383 Ok(())
384 }
385}
386
387#[async_trait::async_trait]
388impl DeletableModel for UserSecurityKey {
389 type DeleteOptions = ();
390
391 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
392 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserSecurityKey>> =
393 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
394
395 &DELETE_LISTENERS
396 }
397
398 async fn delete_with_transaction(
399 &self,
400 state: &crate::State,
401 options: Self::DeleteOptions,
402 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
403 ) -> Result<(), anyhow::Error> {
404 self.run_delete_handlers(&options, state, transaction)
405 .await?;
406
407 sqlx::query(
408 r#"
409 DELETE FROM user_security_keys
410 WHERE user_security_keys.uuid = $1
411 "#,
412 )
413 .bind(self.uuid)
414 .execute(&mut **transaction)
415 .await?;
416
417 self.run_after_delete_handlers(&options, state, transaction)
418 .await?;
419
420 Ok(())
421 }
422}
423
424#[schema_extension_derive::extendible]
425#[init_args(UserSecurityKey, crate::State)]
426#[hook_args(crate::State)]
427#[derive(ToSchema, Serialize)]
428#[schema(title = "UserSecurityKey")]
429pub struct ApiUserSecurityKey {
430 pub uuid: uuid::Uuid,
431
432 pub name: compact_str::CompactString,
433
434 pub credential_id: String,
435
436 pub last_used: Option<chrono::DateTime<chrono::Utc>>,
437 pub created: chrono::DateTime<chrono::Utc>,
438}