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
28impl BaseModel for UserSecurityKey {
29 const NAME: &'static str = "user_security_key";
30
31 #[inline]
32 fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
33 let prefix = prefix.unwrap_or_default();
34
35 BTreeMap::from([
36 (
37 "user_security_keys.uuid",
38 compact_str::format_compact!("{prefix}uuid"),
39 ),
40 (
41 "user_security_keys.name",
42 compact_str::format_compact!("{prefix}name"),
43 ),
44 (
45 "user_security_keys.passkey",
46 compact_str::format_compact!("{prefix}passkey"),
47 ),
48 (
49 "user_security_keys.registration",
50 compact_str::format_compact!("{prefix}registration"),
51 ),
52 (
53 "user_security_keys.last_used",
54 compact_str::format_compact!("{prefix}last_used"),
55 ),
56 (
57 "user_security_keys.created",
58 compact_str::format_compact!("{prefix}created"),
59 ),
60 ])
61 }
62
63 #[inline]
64 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
65 let prefix = prefix.unwrap_or_default();
66
67 Ok(Self {
68 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
69 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
70 passkey: if row
71 .try_get::<serde_json::Value, _>(
72 compact_str::format_compact!("{prefix}passkey").as_str(),
73 )
74 .is_ok()
75 {
76 serde_json::from_value(
77 row.try_get(compact_str::format_compact!("{prefix}passkey").as_str())?,
78 )
79 .ok()
80 } else {
81 None
82 },
83 registration: if row
84 .try_get::<serde_json::Value, _>(
85 compact_str::format_compact!("{prefix}registration").as_str(),
86 )
87 .is_ok()
88 {
89 serde_json::from_value(
90 row.try_get(compact_str::format_compact!("{prefix}registration").as_str())?,
91 )
92 .ok()
93 } else {
94 None
95 },
96 last_used: row.try_get(compact_str::format_compact!("{prefix}last_used").as_str())?,
97 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
98 })
99 }
100}
101
102impl UserSecurityKey {
103 pub async fn by_user_uuid_uuid(
104 database: &crate::database::Database,
105 user_uuid: uuid::Uuid,
106 uuid: uuid::Uuid,
107 ) -> Result<Option<Self>, crate::database::DatabaseError> {
108 let row = sqlx::query(&format!(
109 r#"
110 SELECT {}
111 FROM user_security_keys
112 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.uuid = $2
113 "#,
114 Self::columns_sql(None)
115 ))
116 .bind(user_uuid)
117 .bind(uuid)
118 .fetch_optional(database.read())
119 .await?;
120
121 row.try_map(|row| Self::map(None, &row))
122 }
123
124 pub async fn by_user_uuid_with_pagination(
125 database: &crate::database::Database,
126 user_uuid: uuid::Uuid,
127 page: i64,
128 per_page: i64,
129 search: Option<&str>,
130 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
131 let offset = (page - 1) * per_page;
132
133 let rows = sqlx::query(&format!(
134 r#"
135 SELECT {}, COUNT(*) OVER() AS total_count
136 FROM user_security_keys
137 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 || '%')
138 ORDER BY user_security_keys.created
139 LIMIT $3 OFFSET $4
140 "#,
141 Self::columns_sql(None)
142 ))
143 .bind(user_uuid)
144 .bind(search)
145 .bind(per_page)
146 .bind(offset)
147 .fetch_all(database.read())
148 .await?;
149
150 Ok(super::Pagination {
151 total: rows
152 .first()
153 .map_or(Ok(0), |row| row.try_get("total_count"))?,
154 per_page,
155 page,
156 data: rows
157 .into_iter()
158 .map(|row| Self::map(None, &row))
159 .try_collect_vec()?,
160 })
161 }
162
163 pub async fn delete_unconfigured_by_user_uuid_name(
164 database: &crate::database::Database,
165 user_uuid: uuid::Uuid,
166 name: &str,
167 ) -> Result<(), sqlx::Error> {
168 sqlx::query(
169 r#"
170 DELETE FROM user_security_keys
171 WHERE user_security_keys.user_uuid = $1 AND user_security_keys.name = $2 AND user_security_keys.passkey IS NULL
172 "#,
173 )
174 .bind(user_uuid)
175 .bind(name)
176 .execute(database.write())
177 .await?;
178
179 Ok(())
180 }
181
182 pub async fn delete_unconfigured(
183 database: &crate::database::Database,
184 ) -> Result<u64, sqlx::Error> {
185 Ok(sqlx::query(
186 r#"
187 DELETE FROM user_security_keys
188 WHERE user_security_keys.created < NOW() - INTERVAL '1 day' AND user_security_keys.passkey IS NULL
189 "#,
190 )
191 .execute(database.write())
192 .await?
193 .rows_affected())
194 }
195
196 #[inline]
197 pub fn into_api_object(self) -> ApiUserSecurityKey {
198 ApiUserSecurityKey {
199 uuid: self.uuid,
200 name: self.name,
201 credential_id: self.passkey.as_ref().map_or_else(
202 || "".to_string(),
203 |pk| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(pk.cred_id()),
204 ),
205 last_used: self.last_used.map(|dt| dt.and_utc()),
206 created: self.created.and_utc(),
207 }
208 }
209}
210
211#[derive(ToSchema, Deserialize, Validate)]
212pub struct CreateUserSecurityKeyOptions {
213 #[garde(skip)]
214 pub user_uuid: uuid::Uuid,
215
216 #[garde(length(chars, min = 3, max = 31))]
217 #[schema(min_length = 3, max_length = 31)]
218 pub name: compact_str::CompactString,
219
220 #[garde(skip)]
221 #[schema(value_type = serde_json::Value)]
222 pub registration: webauthn_rs::prelude::PasskeyRegistration,
223}
224
225#[async_trait::async_trait]
226impl CreatableModel for UserSecurityKey {
227 type CreateOptions<'a> = CreateUserSecurityKeyOptions;
228 type CreateResult = Self;
229
230 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
231 static CREATE_LISTENERS: LazyLock<CreateListenerList<UserSecurityKey>> =
232 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
233
234 &CREATE_LISTENERS
235 }
236
237 async fn create(
238 state: &crate::State,
239 mut options: Self::CreateOptions<'_>,
240 ) -> Result<Self, crate::database::DatabaseError> {
241 let mut transaction = state.database.write().begin().await?;
242
243 let mut query_builder = InsertQueryBuilder::new("user_security_keys");
244
245 Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
246 .await?;
247
248 query_builder
249 .set("user_uuid", options.user_uuid)
250 .set("name", &options.name)
251 .set(
252 "credential_id",
253 rand::random_iter().take(16).collect::<Vec<u8>>(),
254 )
255 .set("registration", serde_json::to_value(options.registration)?);
256
257 let row = query_builder
258 .returning(&Self::columns_sql(None))
259 .fetch_one(&mut *transaction)
260 .await?;
261 let security_key = Self::map(None, &row)?;
262
263 transaction.commit().await?;
264
265 Ok(security_key)
266 }
267}
268
269#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
270pub struct UpdateUserSecurityKeyOptions {
271 #[garde(length(chars, min = 3, max = 31))]
272 #[schema(min_length = 3, max_length = 31)]
273 pub name: Option<compact_str::CompactString>,
274}
275
276#[async_trait::async_trait]
277impl UpdatableModel for UserSecurityKey {
278 type UpdateOptions = UpdateUserSecurityKeyOptions;
279
280 fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
281 static UPDATE_LISTENERS: LazyLock<UpdateListenerList<UserSecurityKey>> =
282 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
283
284 &UPDATE_LISTENERS
285 }
286
287 async fn update(
288 &mut self,
289 state: &crate::State,
290 mut options: Self::UpdateOptions,
291 ) -> Result<(), crate::database::DatabaseError> {
292 options.validate()?;
293
294 let mut transaction = state.database.write().begin().await?;
295
296 let mut query_builder = UpdateQueryBuilder::new("user_security_keys");
297
298 Self::run_update_handlers(
299 self,
300 &mut options,
301 &mut query_builder,
302 state,
303 &mut transaction,
304 )
305 .await?;
306
307 query_builder
308 .set("name", options.name.as_ref())
309 .where_eq("uuid", self.uuid);
310
311 query_builder.execute(&mut *transaction).await?;
312
313 if let Some(name) = options.name {
314 self.name = name;
315 }
316
317 transaction.commit().await?;
318
319 Ok(())
320 }
321}
322
323#[async_trait::async_trait]
324impl DeletableModel for UserSecurityKey {
325 type DeleteOptions = ();
326
327 fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
328 static DELETE_LISTENERS: LazyLock<DeleteListenerList<UserSecurityKey>> =
329 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
330
331 &DELETE_LISTENERS
332 }
333
334 async fn delete(
335 &self,
336 state: &crate::State,
337 options: Self::DeleteOptions,
338 ) -> Result<(), anyhow::Error> {
339 let mut transaction = state.database.write().begin().await?;
340
341 self.run_delete_handlers(&options, state, &mut transaction)
342 .await?;
343
344 sqlx::query(
345 r#"
346 DELETE FROM user_security_keys
347 WHERE user_security_keys.uuid = $1
348 "#,
349 )
350 .bind(self.uuid)
351 .execute(&mut *transaction)
352 .await?;
353
354 transaction.commit().await?;
355
356 Ok(())
357 }
358}
359
360#[derive(ToSchema, Serialize)]
361#[schema(title = "UserSecurityKey")]
362pub struct ApiUserSecurityKey {
363 pub uuid: uuid::Uuid,
364
365 pub name: compact_str::CompactString,
366
367 pub credential_id: String,
368
369 pub last_used: Option<chrono::DateTime<chrono::Utc>>,
370 pub created: chrono::DateTime<chrono::Utc>,
371}