Skip to main content

shared/models/user/
mod.rs

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