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