two_factor.rs (27680B)
1 use crate::{ 2 api::EmptyResult, 3 db::{ 4 DbConn, FromDb, 5 schema::{totp, webauthn}, 6 }, 7 error::Error, 8 }; 9 use serde::ser::{Serialize, SerializeStruct as _, Serializer}; 10 use tokio::task; 11 use webauthn_rp::{ 12 AggErr, AuthenticatedCredential, RegisteredCredential, 13 bin::{Decode as _, Encode as _}, 14 request::{ 15 Credentials, PublicKeyCredentialDescriptor, auth::AllowedCredentials, 16 register::UserHandle16, 17 }, 18 response::{ 19 AuthTransports, CredentialId, 20 register::{ 21 AuthenticatorExtensionOutputStaticState, CompressedP256PubKey, CompressedP384PubKey, 22 CompressedPubKey, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, RsaPubKey, 23 StaticState, UncompressedPubKey, 24 }, 25 }, 26 }; 27 db_object! { 28 /// Exactly one of the following is true: 29 /// * [`Self::ed25519_key`] is `Some`. 30 /// * [`Self::p256_x`] is `Some`. 31 /// * [`Self::p384_x`] is `Some`. 32 /// * [`Self::rsa_n`] is `Some`. 33 /// 34 /// [`Self::p256_x`] is `Some` iff [`Self::p256_y_is_odd`] is `Some`. [`Self::p384_x`] is `Some` iff 35 /// [`Self::p384_y_is_odd`] is `Some`. [`Self::rsa_n`] is `Some` iff [`Self::rsa_e`] is `Some`. 36 /// 37 /// [`Self::transports`] is actually a `u8`. [`Self::rsa_e`] is actually an `Option` of `u32` such that the 38 /// contained `i32` is converted to `u32` via `as`. [`Self::cred_protect`] is actually `0`, `1`, `2`, or `3`. 39 #[derive(Insertable, Queryable)] 40 #[diesel(table_name = webauthn)] 41 pub struct WebAuthn { 42 credential_id: Vec<u8>, 43 transports: i16, 44 user_uuid: String, 45 ed25519_key: Option<[u8; 32]>, 46 p256_x: Option<[u8; 32]>, 47 p256_y_is_odd: Option<bool>, 48 p384_x: Option<[u8; 32]>, 49 p384_y_is_odd: Option<bool>, 50 rsa_n: Option<Vec<u8>>, 51 rsa_e: Option<i32>, 52 cred_protect: i16, 53 hmac_secret: Option<bool>, 54 dynamic_state: [u8; 7], 55 metadata: String, 56 id: i64, 57 name: String, 58 } 59 #[derive(Insertable, Queryable)] 60 #[diesel(table_name = totp)] 61 pub struct Totp { 62 user_uuid: String, 63 pub token: String, 64 last_used: i64, 65 } 66 } 67 impl WebAuthn { 68 #[allow(clippy::as_conversions, clippy::cast_possible_wrap)] 69 pub fn new( 70 cred: &RegisteredCredential<'_, 16>, 71 user_uuid: String, 72 id: i64, 73 name: String, 74 ) -> Result<Self, Error> { 75 Ok(Self { 76 credential_id: cred.id().encode()?.to_owned(), 77 transports: i16::from(cred.transports().encode()?), 78 user_uuid, 79 ed25519_key: match cred.static_state().credential_public_key { 80 UncompressedPubKey::Ed25519(ref key) => Some(key.into_inner().try_into().unwrap_or_else(|_e| unreachable!("there is a bug in webauthn_rp::response::register::Ed25519PubKey"))), 81 UncompressedPubKey::P256(_) 82 | UncompressedPubKey::P384(_) 83 | UncompressedPubKey::Rsa(_) => None, 84 }, 85 p256_x: match cred.static_state().credential_public_key { 86 UncompressedPubKey::P256(ref key) => Some(key.x().try_into().unwrap_or_else(|_e| unreachable!("there is a bug in webauthn_rp::response::register::UncompressedP256PubKey"))), 87 UncompressedPubKey::Ed25519(_) 88 | UncompressedPubKey::P384(_) 89 | UncompressedPubKey::Rsa(_) => None, 90 }, 91 p256_y_is_odd: match cred.static_state().credential_public_key { 92 UncompressedPubKey::P256(ref key) => Some(key.y()[31] & 1 == 1), 93 UncompressedPubKey::Ed25519(_) 94 | UncompressedPubKey::P384(_) 95 | UncompressedPubKey::Rsa(_) => None, 96 }, 97 p384_x: match cred.static_state().credential_public_key { 98 UncompressedPubKey::P384(ref key) => Some(key.x().try_into().unwrap_or_else(|_e| unreachable!("there is a bug in webauthn_rp::response::register::UncompressedP384PubKey"))), 99 UncompressedPubKey::Ed25519(_) 100 | UncompressedPubKey::P256(_) 101 | UncompressedPubKey::Rsa(_) => None, 102 }, 103 p384_y_is_odd: match cred.static_state().credential_public_key { 104 UncompressedPubKey::P384(ref key) => Some(key.y()[47] & 1 == 1), 105 UncompressedPubKey::Ed25519(_) 106 | UncompressedPubKey::P256(_) 107 | UncompressedPubKey::Rsa(_) => None, 108 }, 109 rsa_n: match cred.static_state().credential_public_key { 110 UncompressedPubKey::Rsa(ref key) => Some((*key.n()).to_owned()), 111 UncompressedPubKey::Ed25519(_) 112 | UncompressedPubKey::P256(_) 113 | UncompressedPubKey::P384(_) => None, 114 }, 115 rsa_e: match cred.static_state().credential_public_key { 116 UncompressedPubKey::Rsa(ref key) => Some(key.e() as i32), 117 UncompressedPubKey::Ed25519(_) 118 | UncompressedPubKey::P256(_) 119 | UncompressedPubKey::P384(_) => None, 120 }, 121 cred_protect: match cred.static_state().extensions.cred_protect { 122 CredentialProtectionPolicy::None => 0, 123 CredentialProtectionPolicy::UserVerificationOptional => 1, 124 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => 2, 125 CredentialProtectionPolicy::UserVerificationRequired => 3, 126 }, 127 hmac_secret: cred.static_state().extensions.hmac_secret, 128 dynamic_state: cred.dynamic_state().encode()?, 129 metadata: cred.metadata().into_json(), 130 id, 131 name, 132 }) 133 } 134 } 135 #[derive(Queryable)] 136 pub struct WebAuthnInfo { 137 id: i64, 138 name: String, 139 } 140 impl Serialize for WebAuthnInfo { 141 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 142 where 143 S: Serializer, 144 { 145 let mut s = serializer.serialize_struct("WebAuthnInfo", 3)?; 146 s.serialize_field("id", &self.id)?; 147 s.serialize_field("name", self.name.as_str())?; 148 s.serialize_field("migrated", &false)?; 149 s.end() 150 } 151 } 152 153 #[derive(Clone, Copy)] 154 pub enum TwoFactorType { 155 Totp = 0, 156 WebAuthn = 7, 157 } 158 impl From<TwoFactorType> for i32 { 159 fn from(value: TwoFactorType) -> Self { 160 match value { 161 TwoFactorType::Totp => 0i32, 162 TwoFactorType::WebAuthn => 7i32, 163 } 164 } 165 } 166 impl TryFrom<i32> for TwoFactorType { 167 type Error = Error; 168 fn try_from(value: i32) -> Result<Self, Self::Error> { 169 match value { 170 0i32 => Ok(Self::Totp), 171 7i32 => Ok(Self::WebAuthn), 172 _ => Err(Error::from(String::from( 173 "i32 is not a valid TwoFactorType", 174 ))), 175 } 176 } 177 } 178 impl Serialize for TwoFactorType { 179 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 180 where 181 S: Serializer, 182 { 183 let mut s = serializer.serialize_struct("TwoFactorType", 3)?; 184 s.serialize_field("enabled", &true)?; 185 s.serialize_field("type", &i32::from(*self))?; 186 s.serialize_field("object", "twoFactorProvider")?; 187 s.end() 188 } 189 } 190 impl Totp { 191 pub const fn new(user_uuid: String, token: String) -> Self { 192 Self { 193 user_uuid, 194 token, 195 last_used: 0, 196 } 197 } 198 pub fn get_last_used(&self) -> u64 { 199 u64::try_from(self.last_used).expect("underflow") 200 } 201 pub fn set_last_used(&mut self, last_used: u64) { 202 self.last_used = i64::try_from(last_used).expect("overflow"); 203 } 204 } 205 impl TwoFactorType { 206 #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] 207 pub async fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { 208 use diesel::prelude::{Connection as _, ExpressionMethods as _, RunQueryDsl as _}; 209 use diesel::result; 210 let mut con_res = conn.conn.clone().lock_owned().await; 211 let con = con_res.as_mut().expect("unable to get a pooled connection"); 212 task::block_in_place(move || { 213 con.transaction(|con| { 214 diesel::delete(webauthn::table) 215 .filter(webauthn::user_uuid.eq(user_uuid)) 216 .execute(con) 217 .and_then(|_| { 218 diesel::delete(totp::table) 219 .filter(totp::user_uuid.eq(user_uuid)) 220 .execute(con) 221 .map(|_| ()) 222 }) 223 .map_err(result::Error::into) 224 }) 225 }) 226 } 227 #[allow(clippy::clone_on_ref_ptr)] 228 pub async fn delete_by_user(self, user_uuid: &str, conn: &DbConn) -> EmptyResult { 229 use diesel::prelude::{ExpressionMethods as _, RunQueryDsl as _}; 230 use diesel::result; 231 let mut con_res = conn.conn.clone().lock_owned().await; 232 let con = con_res.as_mut().expect("unable to get a pooled connection"); 233 task::block_in_place(move || { 234 match self { 235 Self::Totp => diesel::delete(totp::table) 236 .filter(totp::user_uuid.eq(user_uuid)) 237 .execute(con), 238 Self::WebAuthn => diesel::delete(webauthn::table) 239 .filter(webauthn::user_uuid.eq(user_uuid)) 240 .execute(con), 241 } 242 .map(|_| ()) 243 .map_err(result::Error::into) 244 }) 245 } 246 #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] 247 pub async fn has_twofactor(user_uuid: &str, conn: &DbConn) -> Result<bool, Error> { 248 use diesel::prelude::{ 249 Connection as _, ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _, 250 }; 251 use diesel::result; 252 let mut con_res = conn.conn.clone().lock_owned().await; 253 let con = con_res.as_mut().expect("unable to get a pooled connection"); 254 task::block_in_place(move || { 255 con.transaction(|con| { 256 webauthn::table 257 .count() 258 .filter(webauthn::user_uuid.eq(user_uuid)) 259 .get_result::<i64>(con) 260 .map_err(result::Error::into) 261 .and_then(|count| { 262 if count == 0 { 263 totp::table 264 .count() 265 .filter(totp::user_uuid.eq(user_uuid)) 266 .get_result::<i64>(con) 267 .map_err(result::Error::into) 268 .map(|count| count > 0) 269 } else { 270 Ok(true) 271 } 272 }) 273 }) 274 }) 275 } 276 /// The `bool` represents if WebAuthn is enabled. 277 /// The `Option` represents if TOTP is enabled; and if so, contains the secret token. 278 #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] 279 pub async fn get_factors( 280 user_uuid: &str, 281 conn: &DbConn, 282 ) -> Result<(bool, Option<String>), Error> { 283 use diesel::OptionalExtension as _; 284 use diesel::prelude::{ 285 Connection as _, ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _, 286 }; 287 use diesel::result; 288 let mut con_res = conn.conn.clone().lock_owned().await; 289 let con = con_res.as_mut().expect("unable to get a pooled connection"); 290 task::block_in_place(move || { 291 con.transaction(|con| { 292 webauthn::table 293 .count() 294 .filter(webauthn::user_uuid.eq(user_uuid)) 295 .get_result::<i64>(con) 296 .and_then(|count| { 297 let authn = count > 0; 298 totp::table 299 .select(totp::token) 300 .filter(totp::user_uuid.eq(user_uuid)) 301 .first(con) 302 .optional() 303 .map(|token| (authn, token)) 304 }) 305 .map_err(result::Error::into) 306 }) 307 }) 308 } 309 } 310 impl WebAuthnInfo { 311 #[allow(clippy::clone_on_ref_ptr)] 312 pub async fn get_all_by_user(user_uuid: &str, conn: &DbConn) -> Result<Vec<Self>, Error> { 313 use diesel::prelude::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; 314 use diesel::result; 315 let mut con_res = conn.conn.clone().lock_owned().await; 316 let con = con_res.as_mut().expect("unable to get a pooled connection"); 317 task::block_in_place(move || { 318 webauthn::table 319 .select((webauthn::id, webauthn::name)) 320 .filter(webauthn::user_uuid.eq(user_uuid)) 321 .load::<Self>(con) 322 .map_err(result::Error::into) 323 }) 324 } 325 } 326 327 impl WebAuthn { 328 #[allow(clippy::clone_on_ref_ptr)] 329 async fn get_creds<T>(user_uuid: &str, conn: &DbConn) -> Result<T, Error> 330 where 331 T: Credentials, 332 PublicKeyCredentialDescriptor<Vec<u8>>: Into<T::Credential>, 333 { 334 use diesel::prelude::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; 335 use diesel::result; 336 let mut con_res = conn.conn.clone().lock_owned().await; 337 let con = con_res.as_mut().expect("unable to get a pooled connection"); 338 task::block_in_place(move || { 339 webauthn::table 340 .select((webauthn::credential_id, webauthn::transports)) 341 .filter(webauthn::user_uuid.eq(user_uuid)) 342 .load::<(Vec<u8>, i16)>(con) 343 .map_err(result::Error::into) 344 .and_then(|rows| { 345 let len = rows.len(); 346 rows.into_iter() 347 .try_fold(T::with_capacity(len), |mut creds, parts| { 348 let id = CredentialId::decode(parts.0).map_err(AggErr::CredentialId)?; 349 let transports = 350 AuthTransports::decode(u8::try_from(parts.1).map_err(|_e| { 351 Error::from(String::from("Encoded AuthTransports is not a u8")) 352 })?) 353 .map_err(AggErr::DecodeAuthTransports)?; 354 creds.push(PublicKeyCredentialDescriptor { id, transports }.into()); 355 Ok(creds) 356 }) 357 }) 358 }) 359 } 360 pub async fn get_registered_creds( 361 user_uuid: &str, 362 conn: &DbConn, 363 ) -> Result<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, Error> { 364 Self::get_creds(user_uuid, conn).await 365 } 366 pub async fn get_allowed_creds( 367 user_uuid: &str, 368 conn: &DbConn, 369 ) -> Result<AllowedCredentials, Error> { 370 Self::get_creds(user_uuid, conn).await 371 } 372 #[allow(clippy::clone_on_ref_ptr)] 373 pub async fn insert(self, conn: &DbConn) -> EmptyResult { 374 use __sqlite_model::WebAuthnDb; 375 use diesel::prelude::RunQueryDsl as _; 376 use diesel::result; 377 let mut con_res = conn.conn.clone().lock_owned().await; 378 let con = con_res.as_mut().expect("unable to get a pooled connection"); 379 task::block_in_place(move || { 380 diesel::insert_into(webauthn::table) 381 .values(WebAuthnDb::to_db(&self)) 382 .execute(con) 383 .map_err(result::Error::into) 384 .and_then(|count| { 385 if count == 1 { 386 Ok(()) 387 } else { 388 Err(Error::from(String::from( 389 "exactly one row would not have been inserted into webauthn", 390 ))) 391 } 392 }) 393 }) 394 } 395 #[allow(clippy::clone_on_ref_ptr)] 396 pub async fn update( 397 id: CredentialId<&[u8]>, 398 dynamic_state: DynamicState, 399 conn: &DbConn, 400 ) -> EmptyResult { 401 use diesel::prelude::{ExpressionMethods as _, RunQueryDsl as _}; 402 use diesel::result; 403 let mut con_res = conn.conn.clone().lock_owned().await; 404 let con = con_res.as_mut().expect("unable to get a pooled connection"); 405 task::block_in_place(move || { 406 diesel::update(webauthn::table) 407 .set(webauthn::dynamic_state.eq(dynamic_state.encode()?)) 408 .filter(webauthn::credential_id.eq(id.into_inner())) 409 .execute(con) 410 .map_err(result::Error::into) 411 .and_then(|count| { 412 if count == 1 { 413 Ok(()) 414 } else { 415 Err(Error::from(String::from( 416 "exactly one webauthn row would not have been updated", 417 ))) 418 } 419 }) 420 }) 421 } 422 #[allow( 423 clippy::as_conversions, 424 clippy::cast_sign_loss, 425 clippy::clone_on_ref_ptr 426 )] 427 pub async fn get_credential<'a, 'b>( 428 credential_id: CredentialId<&'a [u8]>, 429 user_uuid: &str, 430 user_handle: &'b UserHandle16, 431 conn: &DbConn, 432 ) -> Result< 433 Option< 434 AuthenticatedCredential< 435 'a, 436 'b, 437 16, 438 CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>, 439 >, 440 >, 441 Error, 442 > { 443 use diesel::OptionalExtension as _; 444 use diesel::prelude::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; 445 use diesel::result; 446 let mut con_res = conn.conn.clone().lock_owned().await; 447 let con = con_res.as_mut().expect("unable to get a pooled connection"); 448 task::block_in_place(move || { 449 webauthn::table 450 .select(( 451 webauthn::ed25519_key, 452 webauthn::p256_x, 453 webauthn::p256_y_is_odd, 454 webauthn::p384_x, 455 webauthn::p384_y_is_odd, 456 webauthn::rsa_n, 457 webauthn::rsa_e, 458 webauthn::cred_protect, 459 webauthn::hmac_secret, 460 webauthn::dynamic_state, 461 )) 462 .filter(webauthn::credential_id.eq(credential_id.into_inner())) 463 .filter(webauthn::user_uuid.eq(user_uuid)) 464 .first::<( 465 Option<Vec<u8>>, 466 Option<Vec<u8>>, 467 Option<bool>, 468 Option<Vec<u8>>, 469 Option<bool>, 470 Option<Vec<u8>>, 471 Option<i32>, 472 i16, 473 Option<bool>, 474 Vec<u8>, 475 )>(con) 476 .optional() 477 .map_err(result::Error::into) 478 .and_then(|row| { 479 row.map_or_else( 480 || Ok(None), 481 |r| { 482 let credential_public_key = r.0.map_or_else( 483 || { 484 r.1.map_or_else( 485 || { 486 r.3.map_or_else( 487 || { 488 r.5.ok_or_else(|| Error::from("Encoded RsaPubKey is invalid".to_owned())).and_then(|n| { 489 r.6.ok_or_else(|| Error::from("Encoded RsaPubKey is invalid".to_owned())).and_then(|e| { 490 RsaPubKey::try_from((n, e as u32)).map_err(|e| Error::from(e.to_string())).map(CompressedPubKey::Rsa) 491 }) 492 }) 493 }, 494 |k| { 495 if k.len() == 48 { 496 r.4.ok_or_else(|| Error::from("Encoded CompressedP384PubKey".to_owned())).map(|y_is_odd| { 497 let mut key = [0; 48]; 498 key.copy_from_slice(k.as_slice()); 499 CompressedPubKey::P384(CompressedP384PubKey::from((key, y_is_odd))) 500 }) 501 } else { 502 Err(Error::from("Encoded CompressedP384PubKey is invalid".to_owned())) 503 } 504 } 505 ) 506 }, 507 |k| { 508 if k.len() == 32 { 509 r.2.ok_or_else(|| Error::from("Encoded CompressedP256PubKey".to_owned())).map(|y_is_odd| { 510 let mut key = [0; 32]; 511 key.copy_from_slice(k.as_slice()); 512 CompressedPubKey::P256(CompressedP256PubKey::from((key, y_is_odd))) 513 }) 514 } else { 515 Err(Error::from("Encoded CompressedP256PubKey is invalid".to_owned())) 516 } 517 } 518 ) 519 }, 520 |k| { 521 if k.len() == 32 { 522 let mut key = [0; 32]; 523 key.copy_from_slice(k.as_slice()); 524 Ok(CompressedPubKey::Ed25519(Ed25519PubKey::from(key))) 525 } else { 526 Err(Error::from("Encoded Ed25519PubKey is invalid".to_owned())) 527 } 528 } 529 )?; 530 let cred_protect = match r.7 { 531 0 => Ok(CredentialProtectionPolicy::None), 532 1 => Ok(CredentialProtectionPolicy::UserVerificationOptional), 533 2 => Ok(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList), 534 3 => Ok(CredentialProtectionPolicy::UserVerificationRequired), 535 _ => Err(Error::from("Encoded AuthTransports is invalid".to_owned())), 536 }?; 537 if r.9.len() == 7 { 538 let mut dec_data = [0; 7]; 539 dec_data.copy_from_slice(r.9.as_slice()); 540 AuthenticatedCredential::new( 541 credential_id, 542 user_handle, 543 StaticState { 544 credential_public_key, 545 extensions: AuthenticatorExtensionOutputStaticState { cred_protect, hmac_secret: r.8 }, 546 }, 547 DynamicState::decode(dec_data) 548 .map_err(AggErr::DecodeDynamicState)?, 549 ) 550 .map_err(AggErr::Credential) 551 .map_err(Error::from) 552 .map(Some) 553 } else { 554 Err(Error::from("Encoded DynamicState is invalid".to_owned())) 555 } 556 }, 557 ) 558 }) 559 }) 560 } 561 #[allow(clippy::clone_on_ref_ptr)] 562 pub async fn delete_by_user_uuid_and_id( 563 user_uuid: &str, 564 id: i64, 565 conn: &DbConn, 566 ) -> EmptyResult { 567 use diesel::prelude::{ExpressionMethods as _, RunQueryDsl as _}; 568 use diesel::result; 569 let mut con_res = conn.conn.clone().lock_owned().await; 570 let con = con_res.as_mut().expect("unable to get a pooled connection"); 571 task::block_in_place(move || { 572 diesel::delete(webauthn::table) 573 .filter(webauthn::user_uuid.eq(user_uuid)) 574 .filter(webauthn::id.eq(id)) 575 .execute(con) 576 .map_err(result::Error::into) 577 .and_then(|count| { 578 if count == 1 { 579 Ok(()) 580 } else { 581 Err(Error::from(String::from( 582 "exactly one webauthn row would not have been removed for the user", 583 ))) 584 } 585 }) 586 }) 587 } 588 } 589 590 impl Totp { 591 #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] 592 pub async fn replace(self, conn: &DbConn) -> EmptyResult { 593 use __sqlite_model::TotpDb; 594 use diesel::prelude::{ExpressionMethods as _, RunQueryDsl as _}; 595 use diesel::result; 596 let mut con_res = conn.conn.clone().lock_owned().await; 597 let con = con_res.as_mut().expect("unable to get a pooled connection"); 598 task::block_in_place(move || { 599 diesel::update(totp::table) 600 .set(totp::last_used.eq(self.last_used)) 601 .filter(totp::user_uuid.eq(&self.user_uuid)) 602 .execute(con) 603 .map_err(result::Error::into) 604 .and_then(|count| { 605 if count == 1 { 606 Ok(()) 607 } else { 608 diesel::insert_into(totp::table) 609 .values(TotpDb::to_db(&self)) 610 .execute(con) 611 .map_err(result::Error::into) 612 .and_then(|count| { 613 if count == 1 { 614 Ok(()) 615 } else { 616 Err(Error::from(String::from( 617 "exactly one totp row was not inserted/updated", 618 ))) 619 } 620 }) 621 } 622 }) 623 }) 624 } 625 #[allow(clippy::clone_on_ref_ptr)] 626 pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Result<Option<Self>, Error> { 627 use __sqlite_model::TotpDb; 628 use diesel::OptionalExtension as _; 629 use diesel::prelude::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; 630 use diesel::result; 631 let mut con_res = conn.conn.clone().lock_owned().await; 632 let con = con_res.as_mut().expect("unable to get a pooled connection"); 633 task::block_in_place(move || { 634 totp::table 635 .filter(totp::user_uuid.eq(user_uuid)) 636 .first::<TotpDb>(con) 637 .optional() 638 .map_err(result::Error::into) 639 .map(FromDb::from_db) 640 }) 641 } 642 }