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 super::PaginatedQueryBuilder::new(
146 Self::columns_sql(None),
147 format!(
148 r#"
149 FROM user_security_keys
150 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.passkey IS NOT NULL AND {search}
151 "#,
152 search = super::search_sql(2, &["user_security_keys.name"], &["user_security_keys.uuid"]),
153 ),
154 "user_security_keys.created, user_security_keys.uuid",
155 )
156 .bind(user_uuid)
157 .bind(search)
158 .fetch(database, page, per_page)
159 .await
160 }
161
162 pub async fn delete_unconfigured_by_user_uuid_name(
163 database: &crate::database::Database,
164 user_uuid: uuid::Uuid,
165 name: &str,
166 ) -> Result<(), sqlx::Error> {
167 sqlx::query(
168 r#"
169 DELETE FROM user_security_keys
170 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.name = $2 AND user_security_keys.passkey IS NULL
171 "#,
172 )
173 .bind(user_uuid)
174 .bind(name)
175 .execute(database.write())
176 .await?;
177
178 Ok(())
179 }
180
181 pub async fn delete_unconfigured(
182 database: &crate::database::Database,
183 timeout_seconds: i64,
184 ) -> Result<u64, sqlx::Error> {
185 Ok(sqlx::query(
186 r#"
187 DELETE FROM user_security_keys
188 WHERE user_security_keys.created < $1 AND user_security_keys.passkey IS NULL
189 "#,
190 )
191 .bind(chrono::Utc::now().naive_utc() - chrono::Duration::seconds(timeout_seconds))
192 .execute(database.write())
193 .await?
194 .rows_affected())
195 }
196
197 pub async fn count_by_user_uuid(
198 database: &crate::database::Database,
199 user_uuid: uuid::Uuid,
200 ) -> Result<i64, sqlx::Error> {
201 sqlx::query_scalar(
202 r#"
203 SELECT COUNT(*)
204 FROM user_security_keys
205 WHERE user_security_keys.user_uuid = $1
206 "#,
207 )
208 .bind(user_uuid)
209 .fetch_one(database.read())
210 .await
211 }
212
213 pub async fn count_usable_by_user_uuid_for_update(
216 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
217 user_uuid: uuid::Uuid,
218 ) -> Result<i64, sqlx::Error> {
219 sqlx::query_scalar(
220 r#"
221 SELECT COUNT(*)
222 FROM (
223 SELECT user_security_keys.uuid
224 FROM user_security_keys
225 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.passkey IS NOT NULL
226 FOR UPDATE
227 ) AS usable_keys
228 "#,
229 )
230 .bind(user_uuid)
231 .fetch_one(&mut **transaction)
232 .await
233 }
234}
235
236#[async_trait::async_trait]
237impl IntoApiObject for UserSecurityKey {
238 type ApiObject = ApiUserSecurityKey;
239 type ExtraArgs<'a> = ();
240
241 async fn into_api_object<'a>(
242 self,
243 state: &crate::State,
244 _args: Self::ExtraArgs<'a>,
245 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
246 let api_object = ApiUserSecurityKey::init_hooks(&self, state).await?;
247
248 let api_object = finish_extendible!(
249 ApiUserSecurityKey {
250 uuid: self.uuid,
251 name: self.name,
252 credential_id: self.passkey.as_ref().map_or_else(
253 || "".to_string(),
254 |pk| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(pk.cred_id()),
255 ),
256 last_used: self.last_used.map(|dt| dt.and_utc()),
257 created: self.created.and_utc(),
258 },
259 api_object,
260 state
261 )?;
262
263 Ok(api_object)
264 }
265}
266
267#[derive(ToSchema, Deserialize, Validate)]
268pub struct CreateUserSecurityKeyOptions {
269 #[garde(skip)]
270 pub user_uuid: uuid::Uuid,
271
272 #[garde(length(chars, min = 3, max = 31))]
273 #[schema(min_length = 3, max_length = 31)]
274 pub name: compact_str::CompactString,
275
276 #[garde(skip)]
277 #[schema(value_type = serde_json::Value)]
278 pub registration: webauthn_rs::prelude::PasskeyRegistration,
279}
280
281#[async_trait::async_trait]
282impl CreatableModel for UserSecurityKey {
283 type CreateOptions<'a> = CreateUserSecurityKeyOptions;
284 type CreateResult = Self;
285
286 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
287 static CREATE_LISTENERS: LazyLock<CreateListenerList<UserSecurityKey>> =
288 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
289
290 &CREATE_LISTENERS
291 }
292
293 async fn create_with_transaction(
294 state: &crate::State,
295 mut options: Self::CreateOptions<'_>,
296 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
297 ) -> Result<Self, crate::database::DatabaseError> {
298 options.validate()?;
299
300 let mut query_builder = InsertQueryBuilder::new("user_security_keys");
301
302 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
303
304 query_builder
305 .set("user_uuid", options.user_uuid)
306 .set("name", &options.name)
307 .set(
308 "credential_id",
309 rand::random_iter().take(16).collect::<Vec<u8>>(),
310 )
311 .set("registration", serde_json::to_value(&options.registration)?);
312
313 let row = query_builder
314 .returning(&Self::columns_sql(None))
315 .fetch_one(&mut **transaction)
316 .await?;
317 let mut security_key = Self::map(None, &row)?;
318
319 Self::run_after_create_handlers(&mut security_key, &options, state, transaction).await?;
320
321 Ok(security_key)
322 }
323}
324
325#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
326pub struct UpdateUserSecurityKeyOptions {
327 #[garde(length(chars, min = 3, max = 31))]
328 #[schema(min_length = 3, max_length = 31)]
329 pub name: Option<compact_str::CompactString>,
330}
331
332#[async_trait::async_trait]
333impl UpdatableModel for UserSecurityKey {
334 type UpdateOptions = UpdateUserSecurityKeyOptions;
335
336 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
337 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserSecurityKey>> =
338 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
339
340 &UPDATE_LISTENERS
341 }
342
343 async fn update_with_transaction(
344 &mut self,
345 state: &crate::State,
346 mut options: Self::UpdateOptions,
347 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
348 ) -> Result<(), crate::database::DatabaseError> {
349 options.validate()?;
350
351 let mut query_builder = UpdateQueryBuilder::new("user_security_keys");
352
353 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
354 .await?;
355
356 query_builder
357 .set("name", options.name.as_ref())
358 .where_eq("uuid", self.uuid);
359
360 query_builder.execute(&mut **transaction).await?;
361
362 if let Some(name) = options.name {
363 self.name = name;
364 }
365
366 self.run_after_update_handlers(state, transaction).await?;
367
368 Ok(())
369 }
370}
371
372#[async_trait::async_trait]
373impl DeletableModel for UserSecurityKey {
374 type DeleteOptions = ();
375
376 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
377 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserSecurityKey>> =
378 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
379
380 &DELETE_LISTENERS
381 }
382
383 async fn delete_with_transaction(
384 &self,
385 state: &crate::State,
386 options: Self::DeleteOptions,
387 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
388 ) -> Result<(), anyhow::Error> {
389 self.run_delete_handlers(&options, state, transaction)
390 .await?;
391
392 sqlx::query(
393 r#"
394 DELETE FROM user_security_keys
395 WHERE user_security_keys.uuid = $1
396 "#,
397 )
398 .bind(self.uuid)
399 .execute(&mut **transaction)
400 .await?;
401
402 self.run_after_delete_handlers(&options, state, transaction)
403 .await?;
404
405 Ok(())
406 }
407}
408
409#[schema_extension_derive::extendible]
410#[init_args(UserSecurityKey, crate::State)]
411#[hook_args(crate::State)]
412#[derive(ToSchema, Serialize)]
413#[schema(title = "UserSecurityKey")]
414pub struct ApiUserSecurityKey {
415 pub uuid: uuid::Uuid,
416
417 pub name: compact_str::CompactString,
418
419 pub credential_id: String,
420
421 pub last_used: Option<chrono::DateTime<chrono::Utc>>,
422 pub created: chrono::DateTime<chrono::Utc>,
423}