1use crate::{
2 crypt::{BcryptString, EncryptedString},
3 models::{InsertQueryBuilder, UpdateQueryBuilder},
4 prelude::*,
5 storage::StorageUrlRetriever,
6};
7use garde::Validate;
8use serde::{Deserialize, Serialize};
9use sqlx::{Row, postgres::PgRow};
10use std::{
11 collections::BTreeMap,
12 sync::{Arc, LazyLock},
13};
14use utoipa::ToSchema;
15use webauthn_rs::prelude::CredentialID;
16
17mod auth;
18pub use auth::*;
19
20pub mod avatar;
21pub mod settings;
22
23#[derive(Serialize, Deserialize, Clone)]
24pub struct User {
25 pub uuid: uuid::Uuid,
26 pub role: Option<super::role::Role>,
27 pub external_id: Option<compact_str::CompactString>,
28
29 pub avatar: Option<String>,
30 pub username: compact_str::CompactString,
31 pub email: compact_str::CompactString,
32
33 pub name_first: Option<compact_str::CompactString>,
34 pub name_last: Option<compact_str::CompactString>,
35
36 pub admin: bool,
37 pub frozen: bool,
38 pub suspended: bool,
39
40 pub totp_enabled: bool,
41 pub totp_last_used: Option<chrono::NaiveDateTime>,
42 pub totp_secret: Option<EncryptedString>,
43 pub email_two_factor_enabled: bool,
44 pub has_security_key: bool,
45
46 pub email_verified: bool,
47 pub password_login_disabled: bool,
48
49 pub language: compact_str::CompactString,
50
51 pub has_password: bool,
52
53 pub created: chrono::NaiveDateTime,
54
55 extension_data: super::ModelExtensionData,
56}
57
58impl BaseModel for User {
59 const NAME: &'static str = "user";
60
61 fn get_extension_list() -> &'static super::ModelExtensionList {
62 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
63 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
64
65 &EXTENSIONS
66 }
67
68 fn get_extension_data(&self) -> &super::ModelExtensionData {
69 &self.extension_data
70 }
71
72 #[inline]
73 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
74 let prefix = prefix.unwrap_or_default();
75
76 let mut columns = BTreeMap::from([
77 ("users.uuid", compact_str::format_compact!("{prefix}uuid")),
78 (
79 "users.external_id",
80 compact_str::format_compact!("{prefix}external_id"),
81 ),
82 (
83 "users.avatar",
84 compact_str::format_compact!("{prefix}avatar"),
85 ),
86 (
87 "users.username",
88 compact_str::format_compact!("{prefix}username"),
89 ),
90 ("users.email", compact_str::format_compact!("{prefix}email")),
91 (
92 "users.name_first",
93 compact_str::format_compact!("{prefix}name_first"),
94 ),
95 (
96 "users.name_last",
97 compact_str::format_compact!("{prefix}name_last"),
98 ),
99 ("users.admin", compact_str::format_compact!("{prefix}admin")),
100 (
101 "users.frozen",
102 compact_str::format_compact!("{prefix}frozen"),
103 ),
104 (
105 "users.suspended",
106 compact_str::format_compact!("{prefix}suspended"),
107 ),
108 (
109 "users.totp_enabled",
110 compact_str::format_compact!("{prefix}totp_enabled"),
111 ),
112 (
113 "users.totp_last_used",
114 compact_str::format_compact!("{prefix}totp_last_used"),
115 ),
116 (
117 "users.totp_secret",
118 compact_str::format_compact!("{prefix}totp_secret"),
119 ),
120 (
121 "users.email_two_factor_enabled",
122 compact_str::format_compact!("{prefix}email_two_factor_enabled"),
123 ),
124 (
125 "EXISTS (SELECT 1 FROM user_security_keys WHERE user_security_keys.user_uuid = users.uuid AND user_security_keys.passkey IS NOT NULL)",
126 compact_str::format_compact!("{prefix}has_security_key"),
127 ),
128 (
129 "users.email_verified",
130 compact_str::format_compact!("{prefix}email_verified"),
131 ),
132 (
133 "users.password_login_disabled",
134 compact_str::format_compact!("{prefix}password_login_disabled"),
135 ),
136 (
137 "users.language",
138 compact_str::format_compact!("{prefix}language"),
139 ),
140 (
141 "(users.password IS NOT NULL)",
142 compact_str::format_compact!("{prefix}has_password"),
143 ),
144 (
145 "users.created",
146 compact_str::format_compact!("{prefix}created"),
147 ),
148 ]);
149
150 columns.extend(super::role::Role::base_columns(Some("role_")));
151
152 columns
153 }
154
155 #[inline]
156 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
157 let prefix = prefix.unwrap_or_default();
158
159 Ok(Self {
160 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
161 role: if row
162 .try_get::<uuid::Uuid, _>(
163 compact_str::format_compact!("{prefix}role_uuid").as_str(),
164 )
165 .is_ok()
166 {
167 Some(super::role::Role::map(Some("role_"), row)?)
168 } else {
169 None
170 },
171 external_id: row
172 .try_get(compact_str::format_compact!("{prefix}external_id").as_str())?,
173 avatar: row.try_get(compact_str::format_compact!("{prefix}avatar").as_str())?,
174 username: row.try_get(compact_str::format_compact!("{prefix}username").as_str())?,
175 email: row.try_get(compact_str::format_compact!("{prefix}email").as_str())?,
176 name_first: row.try_get(compact_str::format_compact!("{prefix}name_first").as_str())?,
177 name_last: row.try_get(compact_str::format_compact!("{prefix}name_last").as_str())?,
178 admin: row.try_get(compact_str::format_compact!("{prefix}admin").as_str())?,
179 frozen: row.try_get(compact_str::format_compact!("{prefix}frozen").as_str())?,
180 suspended: row.try_get(compact_str::format_compact!("{prefix}suspended").as_str())?,
181 totp_enabled: row
182 .try_get(compact_str::format_compact!("{prefix}totp_enabled").as_str())?,
183 totp_last_used: row
184 .try_get(compact_str::format_compact!("{prefix}totp_last_used").as_str())?,
185 totp_secret: row
186 .try_get(compact_str::format_compact!("{prefix}totp_secret").as_str())?,
187 email_two_factor_enabled: row.try_get(
188 compact_str::format_compact!("{prefix}email_two_factor_enabled").as_str(),
189 )?,
190 has_security_key: row
191 .try_get(compact_str::format_compact!("{prefix}has_security_key").as_str())?,
192 email_verified: row
193 .try_get(compact_str::format_compact!("{prefix}email_verified").as_str())?,
194 password_login_disabled: row.try_get(
195 compact_str::format_compact!("{prefix}password_login_disabled").as_str(),
196 )?,
197 language: row.try_get(compact_str::format_compact!("{prefix}language").as_str())?,
198 has_password: row
199 .try_get(compact_str::format_compact!("{prefix}has_password").as_str())?,
200 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
201 extension_data: Self::map_extensions(prefix, row)?,
202 })
203 }
204
205 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
206 vec![compact_str::format_compact!(
207 "{}::{}",
208 Self::NAME,
209 self.uuid
210 )]
211 }
212}
213
214impl User {
215 pub async fn create_automatic_admin(
216 database: &crate::database::Database,
217 username: &str,
218 email: &str,
219 name_first: Option<&str>,
220 name_last: Option<&str>,
221 password: &str,
222 language: &str,
223 ) -> Result<uuid::Uuid, crate::database::DatabaseError> {
224 let password = BcryptString::hash(password).await?;
225
226 let row = sqlx::query(
227 r#"
228 INSERT INTO users (username, email, name_first, name_last, password, language, admin)
229 VALUES ($1, $2, $3, $4, $5, $6, (SELECT COUNT(*) = 0 FROM users))
230 RETURNING users.uuid
231 "#,
232 )
233 .bind(username)
234 .bind(email)
235 .bind(name_first)
236 .bind(name_last)
237 .bind(password)
238 .bind(language)
239 .fetch_one(database.write())
240 .await?;
241
242 Ok(row.try_get("uuid")?)
243 }
244
245 pub async fn by_external_id(
246 database: &crate::database::Database,
247 external_id: &str,
248 ) -> Result<Option<Self>, crate::database::DatabaseError> {
249 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
250 r#"
251 SELECT {}
252 FROM users
253 LEFT JOIN roles ON roles.uuid = users.role_uuid
254 WHERE users.external_id = $1
255 "#,
256 Self::columns_sql(None)
257 )))
258 .bind(external_id)
259 .fetch_optional(database.read())
260 .await?;
261
262 row.try_map(|row| Self::map(None, &row))
263 }
264
265 pub async fn by_session_cached(
269 database: &crate::database::Database,
270 session: &str,
271 ) -> Result<Option<(Self, super::user_session::UserSession)>, anyhow::Error> {
272 let Some(session) =
273 super::user_session::UserSession::resolve_cached(database, session).await?
274 else {
275 return Ok(None);
276 };
277
278 Ok(Self::by_uuid_optional_cached(database, session.user_uuid)
279 .await?
280 .map(|user| (user, session)))
281 }
282
283 pub async fn by_api_key_cached(
287 database: &crate::database::Database,
288 key: &str,
289 ) -> Result<Option<(Self, super::user_api_key::UserApiKey)>, anyhow::Error> {
290 let Some(api_key) = super::user_api_key::UserApiKey::resolve_cached(database, key).await?
291 else {
292 return Ok(None);
293 };
294
295 if api_key
296 .expires
297 .is_some_and(|expires| expires <= chrono::Utc::now().naive_utc())
298 {
299 return Ok(None);
300 }
301
302 Ok(Self::by_uuid_optional_cached(database, api_key.user_uuid)
303 .await?
304 .map(|user| (user, api_key)))
305 }
306
307 pub async fn by_credential_id(
308 database: &crate::database::Database,
309 credential_id: &CredentialID,
310 ) -> Result<
311 Option<(Self, super::user_security_key::UserSecurityKey)>,
312 crate::database::DatabaseError,
313 > {
314 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
315 r#"
316 SELECT {}, {}
317 FROM users
318 LEFT JOIN roles ON roles.uuid = users.role_uuid
319 JOIN user_security_keys ON user_security_keys.user_uuid = users.uuid
320 WHERE user_security_keys.credential_id = $1
321 "#,
322 Self::columns_sql(None),
323 super::user_security_key::UserSecurityKey::columns_sql(Some("security_key_"))
324 )))
325 .bind(credential_id.to_vec())
326 .fetch_optional(database.read())
327 .await?;
328
329 row.try_map(|row| {
330 Ok((
331 Self::map(None, &row)?,
332 super::user_security_key::UserSecurityKey::map(Some("security_key_"), &row)?,
333 ))
334 })
335 }
336
337 pub async fn by_email(
338 database: &crate::database::Database,
339 email: &str,
340 ) -> Result<Option<Self>, crate::database::DatabaseError> {
341 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
342 r#"
343 SELECT {}
344 FROM users
345 LEFT JOIN roles ON roles.uuid = users.role_uuid
346 WHERE lower(users.email) = lower($1)
347 "#,
348 Self::columns_sql(None)
349 )))
350 .bind(email)
351 .fetch_optional(database.read())
352 .await?;
353
354 row.try_map(|row| Self::map(None, &row))
355 }
356
357 pub async fn by_email_password(
358 database: &crate::database::Database,
359 email: &str,
360 password: &str,
361 ) -> Result<Option<Self>, crate::database::DatabaseError> {
362 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
363 r#"
364 SELECT {}, users.password AS password_hash
365 FROM users
366 LEFT JOIN roles ON roles.uuid = users.role_uuid
367 WHERE lower(users.email) = lower($1)
368 "#,
369 Self::columns_sql(None)
370 )))
371 .bind(email)
372 .fetch_optional(database.read())
373 .await?;
374
375 Self::verify_password_row(database, row, password).await
376 }
377
378 pub async fn by_username(
379 database: &crate::database::Database,
380 username: &str,
381 ) -> Result<Option<Self>, crate::database::DatabaseError> {
382 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
383 r#"
384 SELECT {}
385 FROM users
386 LEFT JOIN roles ON roles.uuid = users.role_uuid
387 WHERE lower(users.username) = lower($1)
388 "#,
389 Self::columns_sql(None)
390 )))
391 .bind(username)
392 .fetch_optional(database.read())
393 .await?;
394
395 row.try_map(|row| Self::map(None, &row))
396 }
397
398 pub async fn by_username_password(
399 database: &crate::database::Database,
400 username: &str,
401 password: &str,
402 ) -> Result<Option<Self>, crate::database::DatabaseError> {
403 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
404 r#"
405 SELECT {}, users.password AS password_hash
406 FROM users
407 LEFT JOIN roles ON roles.uuid = users.role_uuid
408 WHERE lower(users.username) = lower($1)
409 "#,
410 Self::columns_sql(None)
411 )))
412 .bind(username)
413 .fetch_optional(database.read())
414 .await?;
415
416 Self::verify_password_row(database, row, password).await
417 }
418
419 async fn verify_password_row(
420 database: &crate::database::Database,
421 row: Option<PgRow>,
422 password: &str,
423 ) -> Result<Option<Self>, crate::database::DatabaseError> {
424 let Some(row) = row else {
425 BcryptString::verify_dummy(password).await?;
426
427 return Ok(None);
428 };
429
430 let Some(hash) = row.try_get::<Option<BcryptString>, _>("password_hash")? else {
431 BcryptString::verify_dummy(password).await?;
432
433 return Ok(None);
434 };
435
436 if !hash.verify(password).await? {
437 return Ok(None);
438 }
439
440 let user = Self::map(None, &row)?;
441 user.rehash_password(database, password, &hash).await?;
442
443 Ok(Some(user))
444 }
445
446 fn totp_secret_is_legacy(secret: &EncryptedString) -> bool {
449 let bytes = secret.as_ref();
450
451 !bytes.is_empty()
452 && bytes.len() <= 32
453 && bytes
454 .iter()
455 .all(|b| matches!(b, b'A'..=b'Z' | b'2'..=b'7' | b'='))
456 }
457
458 pub async fn totp(
459 &self,
460 database: &crate::database::Database,
461 ) -> Result<Option<totp_rs::Totp>, anyhow::Error> {
462 let Some(secret) = &self.totp_secret else {
463 return Ok(None);
464 };
465
466 let base32 = if Self::totp_secret_is_legacy(secret) {
467 compact_str::CompactString::from_utf8_lossy(secret.as_ref())
468 } else {
469 secret.decrypt(database).await?
470 };
471
472 Ok(Some(
473 totp_rs::Builder::new()
474 .with_algorithm(totp_rs::Algorithm::SHA1)
475 .with_digits(6)
476 .with_skew(1)
477 .with_step_duration(30)
478 .with_secret(totp_rs::Secret::try_from_base32(base32)?)
479 .build()?,
480 ))
481 }
482
483 pub async fn consume_totp_step(
486 &self,
487 database: &crate::database::Database,
488 step: u64,
489 ) -> Result<bool, anyhow::Error> {
490 let result = sqlx::query(
491 r#"
492 UPDATE users
493 SET totp_last_used = NOW(), totp_last_step = $2
494 WHERE users.uuid = $1 AND (users.totp_last_step IS NULL OR users.totp_last_step < $2)
495 "#,
496 )
497 .bind(self.uuid)
498 .bind(step as i64)
499 .execute(database.write())
500 .await?;
501
502 Self::invalidate_cached(database, self.uuid).await;
503
504 Ok(result.rows_affected() == 1)
505 }
506
507 pub async fn reencrypt_totp_secret(
510 &self,
511 database: &crate::database::Database,
512 ) -> Result<(), anyhow::Error> {
513 let Some(secret) = &self.totp_secret else {
514 return Ok(());
515 };
516 if !Self::totp_secret_is_legacy(secret) {
517 return Ok(());
518 }
519
520 let encrypted = EncryptedString::from_plaintext(secret.as_ref().to_vec(), database).await?;
521
522 sqlx::query(
523 r#"
524 UPDATE users
525 SET totp_secret = $2
526 WHERE users.uuid = $1 AND users.totp_secret = $3
527 "#,
528 )
529 .bind(self.uuid)
530 .bind(encrypted)
531 .bind(secret)
532 .execute(database.write())
533 .await?;
534
535 Self::invalidate_cached(database, self.uuid).await;
536
537 Ok(())
538 }
539
540 async fn rehash_password(
543 &self,
544 database: &crate::database::Database,
545 password: &str,
546 current_hash: &BcryptString,
547 ) -> Result<(), crate::database::DatabaseError> {
548 if !current_hash.needs_rehash() {
549 return Ok(());
550 }
551
552 let new_hash = BcryptString::hash(password).await?;
553
554 sqlx::query(
555 r#"
556 UPDATE users
557 SET password = $2
558 WHERE users.uuid = $1 AND users.password = $3
559 "#,
560 )
561 .bind(self.uuid)
562 .bind(new_hash)
563 .bind(current_hash)
564 .execute(database.write())
565 .await?;
566
567 Ok(())
568 }
569
570 pub async fn by_username_public_key(
571 database: &crate::database::Database,
572 username: &str,
573 public_key: russh::keys::PublicKey,
574 ) -> Result<Option<Self>, crate::database::DatabaseError> {
575 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
576 r#"
577 SELECT {}
578 FROM users
579 LEFT JOIN roles ON roles.uuid = users.role_uuid
580 JOIN user_ssh_keys ON user_ssh_keys.user_uuid = users.uuid
581 WHERE lower(users.username) = lower($1) AND user_ssh_keys.fingerprint = $2
582 "#,
583 Self::columns_sql(None)
584 )))
585 .bind(username)
586 .bind(
587 public_key
588 .fingerprint(russh::keys::HashAlg::Sha256)
589 .to_string(),
590 )
591 .fetch_optional(database.read())
592 .await?;
593
594 row.try_map(|row| Self::map(None, &row))
595 }
596
597 pub async fn by_role_uuid_with_pagination(
598 database: &crate::database::Database,
599 role_uuid: uuid::Uuid,
600 page: i64,
601 per_page: i64,
602 search: Option<&str>,
603 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
604 super::PaginatedQueryBuilder::new(
605 Self::columns_sql(None),
606 format!(
607 r#"
608 FROM users
609 LEFT JOIN roles ON roles.uuid = users.role_uuid
610 WHERE users.role_uuid = $1 AND {search}
611 "#,
612 search = super::search_sql(2, &["users.username", "users.email"], &["users.uuid"]),
613 ),
614 "users.created, users.uuid",
615 )
616 .bind(role_uuid)
617 .bind(search)
618 .fetch(database, page, per_page)
619 .await
620 }
621
622 pub async fn all_with_pagination(
623 database: &crate::database::Database,
624 page: i64,
625 per_page: i64,
626 search: Option<&str>,
627 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
628 super::PaginatedQueryBuilder::new(
629 Self::columns_sql(None),
630 format!(
631 r#"
632 FROM users
633 LEFT JOIN roles ON roles.uuid = users.role_uuid
634 WHERE {search}
635 "#,
636 search = super::search_sql(1, &["users.username", "users.email"], &["users.uuid"]),
637 ),
638 "users.created, users.uuid",
639 )
640 .bind(search)
641 .fetch(database, page, per_page)
642 .await
643 }
644
645 pub async fn count(database: &crate::database::Database) -> i64 {
646 sqlx::query_scalar(
647 r#"
648 SELECT COUNT(*)
649 FROM users
650 "#,
651 )
652 .fetch_one(database.read())
653 .await
654 .unwrap_or(0)
655 }
656
657 pub async fn validate_password(
658 &self,
659 database: &crate::database::Database,
660 password: &str,
661 ) -> Result<bool, crate::database::DatabaseError> {
662 if !self.has_password {
663 return Ok(true);
664 }
665
666 let row = sqlx::query(
667 r#"
668 SELECT users.password
669 FROM users
670 WHERE users.uuid = $1
671 "#,
672 )
673 .bind(self.uuid)
674 .fetch_optional(database.read())
675 .await?;
676
677 let hash = match row {
678 Some(row) => row.try_get::<Option<BcryptString>, _>("password")?,
679 None => None,
680 };
681
682 let Some(hash) = hash else {
683 BcryptString::verify_dummy(password).await?;
684
685 return Ok(false);
686 };
687
688 if !hash.verify(password).await? {
689 return Ok(false);
690 }
691
692 self.rehash_password(database, password, &hash).await?;
693
694 Ok(true)
695 }
696
697 pub async fn update_password(
699 &mut self,
700 database: &Arc<crate::database::Database>,
701 password: Option<&str>,
702 ) -> Result<(), crate::database::DatabaseError> {
703 if let Some(password) = password {
704 let password = BcryptString::hash(password).await?;
705
706 sqlx::query(
707 r#"
708 UPDATE users
709 SET password = $2
710 WHERE users.uuid = $1
711 "#,
712 )
713 .bind(self.uuid)
714 .bind(password)
715 .execute(database.write())
716 .await?;
717
718 self.has_password = true;
719 } else {
720 sqlx::query(
721 r#"
722 UPDATE users
723 SET password = NULL
724 WHERE users.uuid = $1
725 "#,
726 )
727 .bind(self.uuid)
728 .bind(password)
729 .execute(database.write())
730 .await?;
731
732 self.has_password = false;
733 }
734
735 Self::invalidate_cached(database, self.uuid).await;
736 super::node::Node::invalidate_permissions_everywhere(
737 database,
738 super::node::PermissionsInvalidation {
739 revoke_users: vec![self.uuid],
740 ..Default::default()
741 },
742 );
743
744 Ok(())
745 }
746
747 pub async fn update_password_with_transaction(
749 &mut self,
750 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
751 password: Option<&str>,
752 ) -> Result<(), crate::database::DatabaseError> {
753 if let Some(password) = password {
754 let password = BcryptString::hash(password).await?;
755
756 sqlx::query(
757 r#"
758 UPDATE users
759 SET password = $2
760 WHERE users.uuid = $1
761 "#,
762 )
763 .bind(self.uuid)
764 .bind(password)
765 .execute(&mut **transaction)
766 .await?;
767
768 self.has_password = true;
769 } else {
770 sqlx::query(
771 r#"
772 UPDATE users
773 SET password = NULL
774 WHERE users.uuid = $1
775 "#,
776 )
777 .bind(self.uuid)
778 .bind(password)
779 .execute(&mut **transaction)
780 .await?;
781
782 self.has_password = false;
783 }
784
785 Ok(())
786 }
787
788 pub fn require_two_factor(&self, settings: &crate::settings::AppSettings) -> bool {
789 if let Some(role) = &self.role {
790 role.require_two_factor
791 } else {
792 match settings.app.two_factor_requirement {
793 crate::settings::app::TwoFactorRequirement::Admins => self.admin,
794 crate::settings::app::TwoFactorRequirement::AllUsers => true,
795 crate::settings::app::TwoFactorRequirement::None => false,
796 }
797 }
798 }
799
800 fn email_two_factor_available(&self, settings: &crate::settings::AppSettings) -> bool {
803 self.email_two_factor_enabled
804 && settings.app.email_two_factor_enabled
805 && !matches!(settings.mail_mode, crate::settings::MailMode::None)
806 }
807
808 pub fn has_two_factor_method(
809 &self,
810 method: crate::settings::app::TwoFactorMethod,
811 settings: &crate::settings::AppSettings,
812 ) -> bool {
813 match method {
814 crate::settings::app::TwoFactorMethod::Totp => self.totp_enabled,
815 crate::settings::app::TwoFactorMethod::SecurityKey => {
816 self.has_security_key && settings.webauthn.enabled
817 }
818 crate::settings::app::TwoFactorMethod::Email => {
819 self.email_two_factor_available(settings)
820 }
821 }
822 }
823
824 pub fn two_factor_methods(
826 &self,
827 settings: &crate::settings::AppSettings,
828 ) -> Vec<crate::settings::app::TwoFactorMethod> {
829 crate::settings::app::TwoFactorMethod::ALL
830 .iter()
831 .copied()
832 .filter(|method| self.has_two_factor_method(*method, settings))
833 .collect()
834 }
835
836 pub fn satisfies_two_factor(&self, settings: &crate::settings::AppSettings) -> bool {
837 settings
838 .app
839 .two_factor_accepted_methods
840 .iter()
841 .any(|method| self.has_two_factor_method(*method, settings))
842 }
843
844 pub fn wings_access_denial(
846 &self,
847 settings: &crate::settings::AppSettings,
848 ) -> Option<&'static str> {
849 if self.suspended {
850 Some("account is suspended")
851 } else if self.require_two_factor(settings) && !self.satisfies_two_factor(settings) {
852 Some("two-factor authentication required")
853 } else if self.require_email_verification(settings) && !self.email_verified {
854 Some("email verification required")
855 } else {
856 None
857 }
858 }
859
860 pub fn can_login_with_password(&self, settings: &crate::settings::AppSettings) -> bool {
861 settings.app.password_login_enabled && !self.password_login_disabled
862 }
863
864 pub fn require_email_verification(&self, settings: &crate::settings::AppSettings) -> bool {
865 settings.app.email_verification_required
866 && !matches!(settings.mail_mode, crate::settings::MailMode::None)
867 }
868
869 pub async fn into_api_full_object(
870 self,
871 state: &crate::State,
872 storage_url_retriever: &StorageUrlRetriever<'_>,
873 ) -> Result<ApiFullUser, crate::database::DatabaseError> {
874 let api_object = ApiFullUser::init_hooks(&self, state).await?;
875
876 let settings = storage_url_retriever.get_settings();
877 let require_two_factor = self.require_two_factor(settings);
878 let two_factor_satisfied = self.satisfies_two_factor(settings);
879 let two_factor_methods = self.two_factor_methods(settings);
880 let require_email_verification = self.require_email_verification(settings);
881
882 let role = if let Some(r) = self.role {
883 Some(r.into_admin_api_object(state, ()).await?)
884 } else {
885 None
886 };
887
888 let api_object = finish_extendible!(
889 ApiFullUser {
890 uuid: self.uuid,
891 username: self.username,
892 role,
893 avatar: self
894 .avatar
895 .as_ref()
896 .map(|a| storage_url_retriever.get_url(a)),
897 email: self.email,
898 name_first: self.name_first,
899 name_last: self.name_last,
900 admin: self.admin,
901 frozen: self.frozen,
902 suspended: self.suspended,
903 totp_enabled: self.totp_enabled,
904 totp_last_used: self.totp_last_used.map(|dt| dt.and_utc()),
905 email_two_factor_enabled: self.email_two_factor_enabled,
906 two_factor_methods,
907 require_two_factor,
908 two_factor_satisfied,
909 email_verified: self.email_verified,
910 require_email_verification,
911 password_login_disabled: self.password_login_disabled,
912 language: self.language,
913 has_password: self.has_password,
914 created: self.created.and_utc(),
915 },
916 api_object,
917 state
918 )?;
919
920 Ok(api_object)
921 }
922}
923
924#[async_trait::async_trait]
925impl IntoApiObject for User {
926 type ApiObject = ApiUser;
927 type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
928
929 async fn into_api_object<'a>(
930 self,
931 state: &crate::State,
932 storage_url_retriever: Self::ExtraArgs<'a>,
933 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
934 let api_object = ApiUser::init_hooks(&self, state).await?;
935
936 let api_object = finish_extendible!(
937 ApiUser {
938 uuid: self.uuid,
939 username: self.username,
940 avatar: self
941 .avatar
942 .as_ref()
943 .map(|a| storage_url_retriever.get_url(a)),
944 totp_enabled: self.totp_enabled,
945 created: self.created.and_utc(),
946 },
947 api_object,
948 state
949 )?;
950
951 Ok(api_object)
952 }
953}
954
955#[async_trait::async_trait]
956impl IntoAdminApiObject for User {
957 type AdminApiObject = AdminApiUser;
958 type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
959
960 async fn into_admin_api_object<'a>(
961 self,
962 state: &crate::State,
963 storage_url_retriever: Self::ExtraArgs<'a>,
964 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
965 let api_object = AdminApiUser::init_hooks(&self, state).await?;
966
967 let settings = storage_url_retriever.get_settings();
968 let require_two_factor = self.require_two_factor(settings);
969 let two_factor_satisfied = self.satisfies_two_factor(settings);
970 let two_factor_methods = self.two_factor_methods(settings);
971 let require_email_verification = self.require_email_verification(settings);
972
973 let role = if let Some(r) = self.role {
974 Some(r.into_admin_api_object(state, ()).await?)
975 } else {
976 None
977 };
978
979 let api_object = finish_extendible!(
980 AdminApiUser {
981 uuid: self.uuid,
982 external_id: self.external_id,
983 username: self.username,
984 role,
985 avatar: self
986 .avatar
987 .as_ref()
988 .map(|a| storage_url_retriever.get_url(a)),
989 email: self.email,
990 name_first: self.name_first,
991 name_last: self.name_last,
992 admin: self.admin,
993 frozen: self.frozen,
994 suspended: self.suspended,
995 totp_enabled: self.totp_enabled,
996 totp_last_used: self.totp_last_used.map(|dt| dt.and_utc()),
997 email_two_factor_enabled: self.email_two_factor_enabled,
998 two_factor_methods,
999 require_two_factor,
1000 two_factor_satisfied,
1001 email_verified: self.email_verified,
1002 require_email_verification,
1003 password_login_disabled: self.password_login_disabled,
1004 language: self.language,
1005 has_password: self.has_password,
1006 created: self.created.and_utc(),
1007 },
1008 api_object,
1009 state
1010 )?;
1011
1012 Ok(api_object)
1013 }
1014}
1015
1016#[inline]
1017fn default_true() -> bool {
1018 true
1019}
1020
1021#[derive(ToSchema, Deserialize, Validate)]
1022pub struct CreateUserOptions {
1023 #[garde(skip)]
1024 pub role_uuid: Option<uuid::Uuid>,
1025
1026 #[garde(length(max = 255))]
1027 #[schema(max_length = 255)]
1028 pub external_id: Option<compact_str::CompactString>,
1029
1030 #[garde(length(chars, min = 3, max = 15), pattern("^[a-zA-Z0-9_]+$"))]
1031 #[schema(min_length = 3, max_length = 15)]
1032 #[schema(pattern = "^[a-zA-Z0-9_]+$")]
1033 pub username: compact_str::CompactString,
1034 #[garde(email, length(max = 255))]
1035 #[schema(format = "email", max_length = 255)]
1036 pub email: compact_str::CompactString,
1037 #[garde(length(chars, min = 1, max = 255))]
1038 #[schema(min_length = 1, max_length = 255)]
1039 pub name_first: Option<compact_str::CompactString>,
1040 #[garde(length(chars, min = 1, max = 255))]
1041 #[schema(min_length = 1, max_length = 255)]
1042 pub name_last: Option<compact_str::CompactString>,
1043 #[garde(length(chars, min = 1, max = 512))]
1044 #[schema(min_length = 1, max_length = 512)]
1045 pub password: Option<String>,
1046
1047 #[garde(skip)]
1048 pub admin: bool,
1049 #[garde(skip)]
1050 #[serde(default)]
1051 pub frozen: bool,
1052 #[garde(skip)]
1053 #[serde(default)]
1054 pub suspended: bool,
1055 #[garde(skip)]
1056 #[serde(default = "default_true")]
1057 pub verify_email: bool,
1058 #[garde(skip)]
1059 #[serde(default)]
1060 pub send_email: bool,
1061
1062 #[garde(
1063 length(chars, min = 2, max = 15),
1064 custom(crate::utils::validate_language)
1065 )]
1066 #[schema(min_length = 2, max_length = 15)]
1067 pub language: compact_str::CompactString,
1068}
1069
1070#[async_trait::async_trait]
1071impl CreatableModel for User {
1072 type CreateOptions<'a> = CreateUserOptions;
1073 type CreateResult = Self;
1074
1075 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
1076 static CREATE_LISTENERS: LazyLock<CreateListenerList<User>> =
1077 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1078
1079 &CREATE_LISTENERS
1080 }
1081
1082 async fn create_with_transaction(
1083 state: &crate::State,
1084 mut options: Self::CreateOptions<'_>,
1085 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1086 ) -> Result<Self, crate::database::DatabaseError> {
1087 options.validate()?;
1088
1089 if let Some(role_uuid) = options.role_uuid {
1090 super::role::Role::by_uuid_optional_cached(&state.database, role_uuid)
1091 .await?
1092 .ok_or(crate::database::InvalidRelationError("role"))?;
1093 }
1094
1095 let mut query_builder = InsertQueryBuilder::new("users");
1096
1097 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
1098
1099 query_builder
1100 .set("role_uuid", options.role_uuid)
1101 .set("external_id", options.external_id.as_deref())
1102 .set("username", &options.username)
1103 .set("email", &options.email)
1104 .set("name_first", options.name_first.as_deref())
1105 .set("name_last", options.name_last.as_deref());
1106
1107 if let Some(password) = &options.password {
1108 query_builder.set("password", BcryptString::hash(password).await?);
1109 }
1110
1111 query_builder
1112 .set("admin", options.admin)
1113 .set("frozen", options.frozen)
1114 .set("suspended", options.suspended)
1115 .set("email_verified", options.verify_email)
1116 .set("language", &options.language);
1117
1118 let row = query_builder
1119 .returning("uuid")
1120 .fetch_one(&mut **transaction)
1121 .await?;
1122 let uuid: uuid::Uuid = row.get("uuid");
1123
1124 let mut result = Self::by_uuid_with_transaction(transaction, uuid).await?;
1125
1126 Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
1127
1128 if options.send_email {
1129 match super::user_password_reset::UserPasswordReset::create_with_transaction(
1130 transaction,
1131 result.uuid,
1132 )
1133 .await
1134 {
1135 Ok(token) => {
1136 let settings = state.settings.get().await?;
1137
1138 super::user_activity::UserActivity::create_with_transaction(
1139 state,
1140 super::user_activity::CreateUserActivityOptions {
1141 user_uuid: result.uuid,
1142 impersonator_uuid: None,
1143 api_key_uuid: None,
1144 event: "email:account-created".into(),
1145 ip: None,
1146 data: serde_json::json!({}),
1147 created: None,
1148 },
1149 transaction,
1150 )
1151 .await?;
1152
1153 state
1154 .mail
1155 .send_template(
1156 state,
1157 "account_created",
1158 &result,
1159 minijinja::context! {
1160 user => minijinja::value::Serde(&result),
1161 reset_link => format!(
1162 "{}/auth/reset-password?token={}",
1163 settings.app.url,
1164 urlencoding::encode(&token),
1165 )
1166 },
1167 )
1168 .await;
1169 }
1170 Err(err) => {
1171 tracing::warn!(
1172 user = %result.uuid,
1173 "failed to create user password reset token: {:#?}",
1174 err
1175 );
1176 }
1177 };
1178 }
1179
1180 Ok(result)
1181 }
1182}
1183
1184#[derive(Default, ToSchema, Serialize, Deserialize, Validate)]
1185pub struct UpdateUserOptions {
1186 #[garde(skip)]
1187 #[serde(
1188 default,
1189 skip_serializing_if = "Option::is_none",
1190 with = "::serde_with::rust::double_option"
1191 )]
1192 pub role_uuid: Option<Option<uuid::Uuid>>,
1193
1194 #[garde(length(chars, min = 1, max = 255))]
1195 #[schema(min_length = 1, max_length = 255)]
1196 #[serde(
1197 default,
1198 skip_serializing_if = "Option::is_none",
1199 with = "::serde_with::rust::double_option"
1200 )]
1201 pub external_id: Option<Option<compact_str::CompactString>>,
1202
1203 #[garde(length(chars, min = 3, max = 15), pattern("^[a-zA-Z0-9_]+$"))]
1204 #[schema(min_length = 3, max_length = 15)]
1205 #[schema(pattern = "^[a-zA-Z0-9_]+$")]
1206 pub username: Option<compact_str::CompactString>,
1207 #[garde(email, length(max = 255))]
1208 #[schema(format = "email", max_length = 255)]
1209 pub email: Option<compact_str::CompactString>,
1210 #[garde(length(chars, min = 1, max = 255))]
1211 #[schema(min_length = 1, max_length = 255)]
1212 #[serde(
1213 default,
1214 skip_serializing_if = "Option::is_none",
1215 with = "::serde_with::rust::double_option"
1216 )]
1217 pub name_first: Option<Option<compact_str::CompactString>>,
1218 #[garde(length(chars, min = 1, max = 255))]
1219 #[schema(min_length = 1, max_length = 255)]
1220 #[serde(
1221 default,
1222 skip_serializing_if = "Option::is_none",
1223 with = "::serde_with::rust::double_option"
1224 )]
1225 pub name_last: Option<Option<compact_str::CompactString>>,
1226 #[garde(length(chars, min = 8, max = 512))]
1227 #[schema(min_length = 8, max_length = 512)]
1228 pub password: Option<Option<compact_str::CompactString>>,
1229
1230 #[garde(skip)]
1231 pub admin: Option<bool>,
1232 #[garde(skip)]
1233 pub frozen: Option<bool>,
1234 #[garde(skip)]
1235 pub suspended: Option<bool>,
1236
1237 #[garde(
1238 length(chars, min = 2, max = 15),
1239 inner(custom(crate::utils::validate_language))
1240 )]
1241 #[schema(min_length = 2, max_length = 15)]
1242 pub language: Option<compact_str::CompactString>,
1243}
1244
1245#[async_trait::async_trait]
1246impl UpdatableModel for User {
1247 type UpdateOptions = UpdateUserOptions;
1248
1249 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
1250 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<User>> =
1251 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1252
1253 &UPDATE_LISTENERS
1254 }
1255
1256 async fn update(
1257 &mut self,
1258 state: &crate::State,
1259 options: Self::UpdateOptions,
1260 ) -> Result<(), crate::database::DatabaseError> {
1261 let credentials_changed = options.password.is_some();
1262 let access_changed = options
1263 .suspended
1264 .is_some_and(|suspended| suspended != self.suspended)
1265 || options.admin.is_some_and(|admin| admin != self.admin)
1266 || options
1267 .role_uuid
1268 .is_some_and(|role_uuid| role_uuid != self.role.as_ref().map(|role| role.uuid));
1269
1270 let mut transaction = state.database.write().begin().await?;
1271
1272 if let Err(err) = self
1273 .update_with_transaction(state, options, &mut transaction)
1274 .await
1275 {
1276 transaction.rollback().await?;
1277 return Err(err);
1278 }
1279
1280 transaction.commit().await?;
1281
1282 super::invalidate_cache_keys(&state.database, &self.cache_invalidation_keys()).await;
1283
1284 super::node::Node::invalidate_permissions_everywhere(
1285 &state.database,
1286 super::node::PermissionsInvalidation {
1287 users: if access_changed && !credentials_changed {
1288 vec![self.uuid]
1289 } else {
1290 Vec::new()
1291 },
1292 revoke_users: if credentials_changed {
1293 vec![self.uuid]
1294 } else {
1295 Vec::new()
1296 },
1297 ..Default::default()
1298 },
1299 );
1300
1301 Ok(())
1302 }
1303
1304 async fn update_with_transaction(
1305 &mut self,
1306 state: &crate::State,
1307 mut options: Self::UpdateOptions,
1308 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1309 ) -> Result<(), crate::database::DatabaseError> {
1310 options.validate()?;
1311
1312 let role = if let Some(role_uuid) = options.role_uuid {
1313 if let Some(role_uuid) = role_uuid {
1314 Some(Some(
1315 super::role::Role::by_uuid_optional_cached(&state.database, role_uuid)
1316 .await?
1317 .ok_or(crate::database::InvalidRelationError("role"))?,
1318 ))
1319 } else {
1320 Some(None)
1321 }
1322 } else {
1323 None
1324 };
1325
1326 let mut query_builder = UpdateQueryBuilder::new("users");
1327
1328 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
1329 .await?;
1330
1331 query_builder
1332 .set("role_uuid", options.role_uuid.as_ref())
1333 .set("external_id", options.external_id.as_ref())
1334 .set("username", options.username.as_ref())
1335 .set("email", options.email.as_ref())
1336 .set("name_first", options.name_first.as_ref())
1337 .set("name_last", options.name_last.as_ref())
1338 .set("admin", options.admin)
1339 .set("frozen", options.frozen)
1340 .set("suspended", options.suspended)
1341 .set("language", options.language.as_ref())
1342 .where_eq("uuid", self.uuid);
1343
1344 query_builder.execute(&mut **transaction).await?;
1345
1346 if let Some(role) = role {
1347 self.role = role;
1348 }
1349 if let Some(external_id) = options.external_id {
1350 self.external_id = external_id;
1351 }
1352 if let Some(username) = options.username {
1353 self.username = username;
1354 }
1355 if let Some(email) = options.email {
1356 self.email = email;
1357 }
1358 if let Some(name_first) = options.name_first {
1359 self.name_first = name_first;
1360 }
1361 if let Some(name_last) = options.name_last {
1362 self.name_last = name_last;
1363 }
1364 if let Some(admin) = options.admin {
1365 self.admin = admin;
1366 }
1367 if let Some(frozen) = options.frozen {
1368 self.frozen = frozen;
1369 }
1370 if let Some(suspended) = options.suspended {
1371 self.suspended = suspended;
1372 }
1373 if let Some(language) = options.language {
1374 self.language = language;
1375 }
1376
1377 if let Some(password) = options.password {
1378 self.update_password_with_transaction(transaction, password.as_deref())
1379 .await?;
1380 }
1381
1382 self.run_after_update_handlers(state, transaction).await?;
1383
1384 Ok(())
1385 }
1386}
1387
1388#[async_trait::async_trait]
1389impl DeletableModel for User {
1390 type DeleteOptions = ();
1391
1392 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
1393 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<User>> =
1394 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1395
1396 &DELETE_LISTENERS
1397 }
1398
1399 async fn delete_with_transaction(
1400 &self,
1401 state: &crate::State,
1402 options: Self::DeleteOptions,
1403 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1404 ) -> Result<(), anyhow::Error> {
1405 self.run_delete_handlers(&options, state, transaction)
1406 .await?;
1407
1408 sqlx::query(
1409 r#"
1410 DELETE FROM users
1411 WHERE users.uuid = $1
1412 "#,
1413 )
1414 .bind(self.uuid)
1415 .execute(&mut **transaction)
1416 .await?;
1417
1418 self.run_after_delete_handlers(&options, state, transaction)
1419 .await?;
1420
1421 Ok(())
1422 }
1423
1424 async fn delete(
1425 &self,
1426 state: &crate::State,
1427 options: Self::DeleteOptions,
1428 ) -> Result<(), anyhow::Error> {
1429 let mut transaction = state.database.write().begin().await?;
1430 self.delete_with_transaction(state, options, &mut transaction)
1431 .await?;
1432 transaction.commit().await?;
1433
1434 Self::invalidate_cached(&state.database, self.uuid).await;
1435 super::node::Node::invalidate_permissions_everywhere(
1436 &state.database,
1437 super::node::PermissionsInvalidation {
1438 revoke_users: vec![self.uuid],
1439 ..Default::default()
1440 },
1441 );
1442
1443 state.storage.remove(self.avatar.as_deref()).await?;
1444
1445 Ok(())
1446 }
1447}
1448
1449#[async_trait::async_trait]
1450impl ByUuid for User {
1451 async fn by_uuid(
1452 database: &crate::database::Database,
1453 uuid: uuid::Uuid,
1454 ) -> Result<Self, crate::database::DatabaseError> {
1455 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
1456 r#"
1457 SELECT {}
1458 FROM users
1459 LEFT JOIN roles ON roles.uuid = users.role_uuid
1460 WHERE users.uuid = $1
1461 "#,
1462 Self::columns_sql(None)
1463 )))
1464 .bind(uuid)
1465 .fetch_one(database.read())
1466 .await?;
1467
1468 Self::map(None, &row)
1469 }
1470
1471 async fn by_uuid_with_transaction(
1472 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1473 uuid: uuid::Uuid,
1474 ) -> Result<Self, crate::database::DatabaseError> {
1475 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
1476 r#"
1477 SELECT {}
1478 FROM users
1479 LEFT JOIN roles ON roles.uuid = users.role_uuid
1480 WHERE users.uuid = $1
1481 "#,
1482 Self::columns_sql(None)
1483 )))
1484 .bind(uuid)
1485 .fetch_one(&mut **transaction)
1486 .await?;
1487
1488 Self::map(None, &row)
1489 }
1490}
1491
1492#[schema_extension_derive::extendible]
1493#[init_args(User, crate::State)]
1494#[hook_args(crate::State)]
1495#[derive(ToSchema, Serialize)]
1496#[schema(title = "User")]
1497pub struct ApiUser {
1498 pub uuid: uuid::Uuid,
1499
1500 pub username: compact_str::CompactString,
1501 pub avatar: Option<String>,
1502
1503 pub totp_enabled: bool,
1504
1505 pub created: chrono::DateTime<chrono::Utc>,
1506}
1507
1508#[schema_extension_derive::extendible]
1509#[init_args(User, crate::State)]
1510#[hook_args(crate::State)]
1511#[derive(ToSchema, Serialize)]
1512#[schema(title = "FullUser")]
1513pub struct ApiFullUser {
1514 pub uuid: uuid::Uuid,
1515
1516 pub username: compact_str::CompactString,
1517 pub role: Option<super::role::AdminApiRole>,
1518 pub avatar: Option<String>,
1519 pub email: compact_str::CompactString,
1520
1521 pub name_first: Option<compact_str::CompactString>,
1522 pub name_last: Option<compact_str::CompactString>,
1523
1524 pub admin: bool,
1525 pub frozen: bool,
1526 pub suspended: bool,
1527
1528 pub totp_enabled: bool,
1529 pub totp_last_used: Option<chrono::DateTime<chrono::Utc>>,
1530 pub email_two_factor_enabled: bool,
1531 pub two_factor_methods: Vec<crate::settings::app::TwoFactorMethod>,
1532 pub require_two_factor: bool,
1533 pub two_factor_satisfied: bool,
1534
1535 pub email_verified: bool,
1536 pub require_email_verification: bool,
1537 pub password_login_disabled: bool,
1538
1539 pub language: compact_str::CompactString,
1540
1541 pub has_password: bool,
1542
1543 pub created: chrono::DateTime<chrono::Utc>,
1544}
1545
1546#[schema_extension_derive::extendible]
1547#[init_args(User, crate::State)]
1548#[hook_args(crate::State)]
1549#[derive(ToSchema, Serialize)]
1550#[schema(title = "AdminUser")]
1551pub struct AdminApiUser {
1552 pub uuid: uuid::Uuid,
1553 pub external_id: Option<compact_str::CompactString>,
1554
1555 pub username: compact_str::CompactString,
1556 pub role: Option<super::role::AdminApiRole>,
1557 pub avatar: Option<String>,
1558 pub email: compact_str::CompactString,
1559
1560 pub name_first: Option<compact_str::CompactString>,
1561 pub name_last: Option<compact_str::CompactString>,
1562
1563 pub admin: bool,
1564 pub frozen: bool,
1565 pub suspended: bool,
1566
1567 pub totp_enabled: bool,
1568 pub totp_last_used: Option<chrono::DateTime<chrono::Utc>>,
1569 pub email_two_factor_enabled: bool,
1570 pub two_factor_methods: Vec<crate::settings::app::TwoFactorMethod>,
1571 pub require_two_factor: bool,
1572 pub two_factor_satisfied: bool,
1573
1574 pub email_verified: bool,
1575 pub require_email_verification: bool,
1576 pub password_login_disabled: bool,
1577
1578 pub language: compact_str::CompactString,
1579
1580 pub has_password: bool,
1581
1582 pub created: chrono::DateTime<chrono::Utc>,
1583}