Skip to main content

shared/models/user/
mod.rs

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    /// Returns the user and session associated with the given session string, if valid.
266    ///
267    /// Both rows are cached until they are written to.
268    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    /// Returns the user and API key associated with the given API key string, if valid.
284    ///
285    /// Both rows are cached until they are written to.
286    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    /// Seeds written before encryption at rest are the bare base32 value; an encrypted blob is
447    /// binary and far longer than any base32 seed.
448    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    /// Atomically records `step` as the last accepted TOTP step. Returns `false` when a code from
484    /// this step or a later one was already accepted, so the caller must reject it as a replay.
485    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    /// Rewrites a legacy plaintext seed encrypted once a code has verified against it, so every
508    /// active account converges on encryption at rest.
509    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    /// Rewrites a matching hash that was produced at a different cost or format (imports, older
541    /// versions) so every active account converges on the current parameters.
542    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    /// Update the User password, `None` will disallow password login and not require one when changing
698    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    /// Update the User password, `None` will disallow password login and not require one when changing
748    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    /// A stale `email_two_factor_enabled` is ignored once an admin turns the feature off, degrading to
801    /// password only rather than locking the user out of a mailbox nobody can deliver to.
802    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    /// Every factor the user has, regardless of whether an admin counts it towards the requirement.
825    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    /// Why the user may not reach any server through wings right now, whatever the credential.
845    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}