Skip to main content

shared/models/user/
mod.rs

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