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 sqlx::{Row, postgres::PgRow, prelude::Type};
9use std::{
10    collections::BTreeMap,
11    sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14use webauthn_rs::prelude::CredentialID;
15
16mod auth;
17pub use auth::*;
18
19#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
20#[serde(rename_all = "snake_case")]
21#[sqlx(type_name = "user_toast_position", rename_all = "SCREAMING_SNAKE_CASE")]
22pub enum UserToastPosition {
23    TopLeft,
24    TopCenter,
25    TopRight,
26    BottomLeft,
27    BottomCenter,
28    BottomRight,
29}
30
31#[derive(Serialize, Deserialize, Clone)]
32pub struct User {
33    pub uuid: uuid::Uuid,
34    pub role: Option<super::role::Role>,
35    pub external_id: Option<compact_str::CompactString>,
36
37    pub avatar: Option<String>,
38    pub username: compact_str::CompactString,
39    pub email: compact_str::CompactString,
40
41    pub name_first: compact_str::CompactString,
42    pub name_last: compact_str::CompactString,
43
44    pub admin: bool,
45    pub totp_enabled: bool,
46    pub totp_last_used: Option<chrono::NaiveDateTime>,
47    pub totp_secret: Option<String>,
48
49    pub language: compact_str::CompactString,
50    pub toast_position: UserToastPosition,
51    pub start_on_grouped_servers: bool,
52
53    pub has_password: bool,
54
55    pub created: chrono::NaiveDateTime,
56
57    extension_data: super::ModelExtensionData,
58}
59
60impl BaseModel for User {
61    const NAME: &'static str = "user";
62
63    fn get_extension_list() -> &'static super::ModelExtensionList {
64        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
65            LazyLock::new(|| std::sync::RwLock::new(Vec::new()));
66
67        &EXTENSIONS
68    }
69
70    fn get_extension_data(&self) -> &super::ModelExtensionData {
71        &self.extension_data
72    }
73
74    #[inline]
75    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
76        let prefix = prefix.unwrap_or_default();
77
78        let mut columns = BTreeMap::from([
79            ("users.uuid", compact_str::format_compact!("{prefix}uuid")),
80            (
81                "users.external_id",
82                compact_str::format_compact!("{prefix}external_id"),
83            ),
84            (
85                "users.avatar",
86                compact_str::format_compact!("{prefix}avatar"),
87            ),
88            (
89                "users.username",
90                compact_str::format_compact!("{prefix}username"),
91            ),
92            ("users.email", compact_str::format_compact!("{prefix}email")),
93            (
94                "users.name_first",
95                compact_str::format_compact!("{prefix}name_first"),
96            ),
97            (
98                "users.name_last",
99                compact_str::format_compact!("{prefix}name_last"),
100            ),
101            ("users.admin", compact_str::format_compact!("{prefix}admin")),
102            (
103                "users.totp_enabled",
104                compact_str::format_compact!("{prefix}totp_enabled"),
105            ),
106            (
107                "users.totp_last_used",
108                compact_str::format_compact!("{prefix}totp_last_used"),
109            ),
110            (
111                "users.totp_secret",
112                compact_str::format_compact!("{prefix}totp_secret"),
113            ),
114            (
115                "users.language",
116                compact_str::format_compact!("{prefix}language"),
117            ),
118            (
119                "users.toast_position",
120                compact_str::format_compact!("{prefix}toast_position"),
121            ),
122            (
123                "users.start_on_grouped_servers",
124                compact_str::format_compact!("{prefix}start_on_grouped_servers"),
125            ),
126            (
127                "(users.password IS NOT NULL)",
128                compact_str::format_compact!("{prefix}has_password"),
129            ),
130            (
131                "users.created",
132                compact_str::format_compact!("{prefix}created"),
133            ),
134        ]);
135
136        columns.extend(super::role::Role::base_columns(Some("role_")));
137
138        columns
139    }
140
141    #[inline]
142    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
143        let prefix = prefix.unwrap_or_default();
144
145        Ok(Self {
146            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
147            role: if row
148                .try_get::<uuid::Uuid, _>(
149                    compact_str::format_compact!("{prefix}role_uuid").as_str(),
150                )
151                .is_ok()
152            {
153                Some(super::role::Role::map(Some("role_"), row)?)
154            } else {
155                None
156            },
157            external_id: row
158                .try_get(compact_str::format_compact!("{prefix}external_id").as_str())?,
159            avatar: row.try_get(compact_str::format_compact!("{prefix}avatar").as_str())?,
160            username: row.try_get(compact_str::format_compact!("{prefix}username").as_str())?,
161            email: row.try_get(compact_str::format_compact!("{prefix}email").as_str())?,
162            name_first: row.try_get(compact_str::format_compact!("{prefix}name_first").as_str())?,
163            name_last: row.try_get(compact_str::format_compact!("{prefix}name_last").as_str())?,
164            admin: row.try_get(compact_str::format_compact!("{prefix}admin").as_str())?,
165            totp_enabled: row
166                .try_get(compact_str::format_compact!("{prefix}totp_enabled").as_str())?,
167            totp_last_used: row
168                .try_get(compact_str::format_compact!("{prefix}totp_last_used").as_str())?,
169            totp_secret: row
170                .try_get(compact_str::format_compact!("{prefix}totp_secret").as_str())?,
171            language: row.try_get(compact_str::format_compact!("{prefix}language").as_str())?,
172            toast_position: row
173                .try_get(compact_str::format_compact!("{prefix}toast_position").as_str())?,
174            start_on_grouped_servers: row.try_get(
175                compact_str::format_compact!("{prefix}start_on_grouped_servers").as_str(),
176            )?,
177            has_password: row
178                .try_get(compact_str::format_compact!("{prefix}has_password").as_str())?,
179            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
180            extension_data: Self::map_extensions(prefix, row)?,
181        })
182    }
183}
184
185impl User {
186    pub async fn create_automatic_admin(
187        database: &crate::database::Database,
188        username: &str,
189        email: &str,
190        name_first: &str,
191        name_last: &str,
192        password: &str,
193    ) -> Result<uuid::Uuid, crate::database::DatabaseError> {
194        let row = sqlx::query(
195            r#"
196            INSERT INTO users (username, email, name_first, name_last, password, admin)
197            VALUES ($1, $2, $3, $4, crypt($5, gen_salt('bf', 12)), (SELECT COUNT(*) = 0 FROM users))
198            RETURNING users.uuid
199            "#,
200        )
201        .bind(username)
202        .bind(email)
203        .bind(name_first)
204        .bind(name_last)
205        .bind(password)
206        .fetch_one(database.write())
207        .await?;
208
209        Ok(row.try_get("uuid")?)
210    }
211
212    pub async fn by_external_id(
213        database: &crate::database::Database,
214        external_id: &str,
215    ) -> Result<Option<Self>, crate::database::DatabaseError> {
216        let row = sqlx::query(&format!(
217            r#"
218            SELECT {}
219            FROM users
220            LEFT JOIN roles ON roles.uuid = users.role_uuid
221            JOIN user_security_keys ON user_security_keys.user_uuid = users.uuid
222            WHERE users.external_id = $1
223            "#,
224            Self::columns_sql(None)
225        ))
226        .bind(external_id)
227        .fetch_optional(database.read())
228        .await?;
229
230        row.try_map(|row| Self::map(None, &row))
231    }
232
233    /// Returns the user and session associated with the given session string, if valid.
234    ///
235    /// Cached for 5 seconds.
236    pub async fn by_session_cached(
237        database: &crate::database::Database,
238        session: &str,
239    ) -> Result<Option<(Self, super::user_session::UserSession)>, anyhow::Error> {
240        let (key_id, key) = match session.split_once(':') {
241            Some((key_id, key)) => (key_id, key),
242            None => return Ok(None),
243        };
244
245        database
246            .cache
247            .cached(&format!("user::session::{session}"), 5, || async {
248                let row = sqlx::query(&format!(
249                    r#"
250                    WITH user_sessions AS MATERIALIZED (
251                        SELECT * FROM user_sessions WHERE key_id = $1
252                    )
253                    SELECT {}, {}
254                    FROM users
255                    LEFT JOIN roles ON roles.uuid = users.role_uuid
256                    JOIN user_sessions ON user_sessions.user_uuid = users.uuid
257                    WHERE user_sessions.key = crypt($2, user_sessions.key)
258                    "#,
259                    Self::columns_sql(None),
260                    super::user_session::UserSession::columns_sql(Some("session_"))
261                ))
262                .bind(key_id)
263                .bind(key)
264                .fetch_optional(database.read())
265                .await?;
266
267                row.try_map(|row| {
268                    Ok::<_, anyhow::Error>((
269                        Self::map(None, &row)?,
270                        super::user_session::UserSession::map(Some("session_"), &row)?,
271                    ))
272                })
273            })
274            .await
275    }
276
277    /// Returns the user and API key associated with the given API key string, if valid.
278    ///
279    /// Cached for 5 seconds.
280    pub async fn by_api_key_cached(
281        database: &crate::database::Database,
282        key: &str,
283    ) -> Result<Option<(Self, super::user_api_key::UserApiKey)>, anyhow::Error> {
284        database
285            .cache
286            .cached(&format!("user::api_key::{key}"), 5, || async {
287                let row = sqlx::query(&format!(
288                    r#"
289                    WITH user_api_keys AS MATERIALIZED (
290                        SELECT * FROM user_api_keys 
291                        WHERE user_api_keys.key_start = $1 
292                        AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
293                    )
294                    SELECT {}, {}
295                    FROM users
296                    LEFT JOIN roles ON roles.uuid = users.role_uuid
297                    JOIN user_api_keys ON user_api_keys.user_uuid = users.uuid
298                    WHERE user_api_keys.key = crypt($2, user_api_keys.key)
299                    "#,
300                    Self::columns_sql(None),
301                    super::user_api_key::UserApiKey::columns_sql(Some("api_key_"))
302                ))
303                .bind(&key[0..16])
304                .bind(key)
305                .fetch_optional(database.read())
306                .await?;
307
308                row.try_map(|row| {
309                    Ok::<_, anyhow::Error>((
310                        Self::map(None, &row)?,
311                        super::user_api_key::UserApiKey::map(Some("api_key_"), &row)?,
312                    ))
313                })
314            })
315            .await
316    }
317
318    pub async fn by_credential_id(
319        database: &crate::database::Database,
320        credential_id: &CredentialID,
321    ) -> Result<
322        Option<(Self, super::user_security_key::UserSecurityKey)>,
323        crate::database::DatabaseError,
324    > {
325        let row = sqlx::query(&format!(
326            r#"
327            SELECT {}, {}
328            FROM users
329            LEFT JOIN roles ON roles.uuid = users.role_uuid
330            JOIN user_security_keys ON user_security_keys.user_uuid = users.uuid
331            WHERE user_security_keys.credential_id = $1
332            "#,
333            Self::columns_sql(None),
334            super::user_security_key::UserSecurityKey::columns_sql(Some("security_key_"))
335        ))
336        .bind(credential_id.to_vec())
337        .fetch_optional(database.read())
338        .await?;
339
340        row.try_map(|row| {
341            Ok((
342                Self::map(None, &row)?,
343                super::user_security_key::UserSecurityKey::map(Some("security_key_"), &row)?,
344            ))
345        })
346    }
347
348    pub async fn by_email(
349        database: &crate::database::Database,
350        email: &str,
351    ) -> Result<Option<Self>, crate::database::DatabaseError> {
352        let row = sqlx::query(&format!(
353            r#"
354            SELECT {}
355            FROM users
356            LEFT JOIN roles ON roles.uuid = users.role_uuid
357            WHERE lower(users.email) = lower($1)
358            "#,
359            Self::columns_sql(None)
360        ))
361        .bind(email)
362        .fetch_optional(database.read())
363        .await?;
364
365        row.try_map(|row| Self::map(None, &row))
366    }
367
368    pub async fn by_email_password(
369        database: &crate::database::Database,
370        email: &str,
371        password: &str,
372    ) -> Result<Option<Self>, crate::database::DatabaseError> {
373        let row = sqlx::query(&format!(
374            r#"
375            SELECT {}
376            FROM users
377            LEFT JOIN roles ON roles.uuid = users.role_uuid
378            WHERE lower(users.email) = lower($1) AND users.password IS NOT NULL AND users.password = crypt($2, users.password)
379            "#,
380            Self::columns_sql(None)
381        ))
382        .bind(email)
383        .bind(password)
384        .fetch_optional(database.read())
385        .await?;
386
387        row.try_map(|row| Self::map(None, &row))
388    }
389
390    pub async fn by_username(
391        database: &crate::database::Database,
392        username: &str,
393    ) -> Result<Option<Self>, crate::database::DatabaseError> {
394        let row = sqlx::query(&format!(
395            r#"
396            SELECT {}
397            FROM users
398            LEFT JOIN roles ON roles.uuid = users.role_uuid
399            WHERE lower(users.username) = lower($1)
400            "#,
401            Self::columns_sql(None)
402        ))
403        .bind(username)
404        .fetch_optional(database.read())
405        .await?;
406
407        row.try_map(|row| Self::map(None, &row))
408    }
409
410    pub async fn by_username_password(
411        database: &crate::database::Database,
412        username: &str,
413        password: &str,
414    ) -> Result<Option<Self>, crate::database::DatabaseError> {
415        let row = sqlx::query(&format!(
416            r#"
417            SELECT {}
418            FROM users
419            LEFT JOIN roles ON roles.uuid = users.role_uuid
420            WHERE lower(users.username) = lower($1) AND users.password IS NOT NULL AND users.password = crypt($2, users.password)
421            "#,
422            Self::columns_sql(None)
423        ))
424        .bind(username)
425        .bind(password)
426        .fetch_optional(database.read())
427        .await?;
428
429        row.try_map(|row| Self::map(None, &row))
430    }
431
432    pub async fn by_username_public_key(
433        database: &crate::database::Database,
434        username: &str,
435        public_key: russh::keys::PublicKey,
436    ) -> Result<Option<Self>, crate::database::DatabaseError> {
437        let row = sqlx::query(&format!(
438            r#"
439            SELECT {}
440            FROM users
441            LEFT JOIN roles ON roles.uuid = users.role_uuid
442            JOIN user_ssh_keys ON user_ssh_keys.user_uuid = users.uuid
443            WHERE lower(users.username) = lower($1) AND user_ssh_keys.fingerprint = $2
444            "#,
445            Self::columns_sql(None)
446        ))
447        .bind(username)
448        .bind(
449            public_key
450                .fingerprint(russh::keys::HashAlg::Sha256)
451                .to_string(),
452        )
453        .fetch_optional(database.read())
454        .await?;
455
456        row.try_map(|row| Self::map(None, &row))
457    }
458
459    pub async fn by_role_uuid_with_pagination(
460        database: &crate::database::Database,
461        role_uuid: uuid::Uuid,
462        page: i64,
463        per_page: i64,
464        search: Option<&str>,
465    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
466        let offset = (page - 1) * per_page;
467
468        let rows = sqlx::query(&format!(
469            r#"
470            SELECT {}, COUNT(*) OVER() AS total_count
471            FROM users
472            LEFT JOIN roles ON roles.uuid = users.role_uuid
473            WHERE users.role_uuid = $1 AND ($2 IS NULL OR users.username ILIKE '%' || $2 || '%' OR users.email ILIKE '%' || $2 || '%')
474            ORDER BY users.created
475            LIMIT $3 OFFSET $4
476            "#,
477            Self::columns_sql(None)
478        ))
479        .bind(role_uuid)
480        .bind(search)
481        .bind(per_page)
482        .bind(offset)
483        .fetch_all(database.read())
484        .await?;
485
486        Ok(super::Pagination {
487            total: rows
488                .first()
489                .map_or(Ok(0), |row| row.try_get("total_count"))?,
490            per_page,
491            page,
492            data: rows
493                .into_iter()
494                .map(|row| Self::map(None, &row))
495                .try_collect_vec()?,
496        })
497    }
498
499    pub async fn all_with_pagination(
500        database: &crate::database::Database,
501        page: i64,
502        per_page: i64,
503        search: Option<&str>,
504    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
505        let offset = (page - 1) * per_page;
506
507        let rows = sqlx::query(&format!(
508            r#"
509            SELECT {}, COUNT(*) OVER() AS total_count
510            FROM users
511            LEFT JOIN roles ON roles.uuid = users.role_uuid
512            WHERE $1 IS NULL OR users.username ILIKE '%' || $1 || '%' OR users.email ILIKE '%' || $1 || '%'
513            ORDER BY users.created
514            LIMIT $2 OFFSET $3
515            "#,
516            Self::columns_sql(None)
517        ))
518        .bind(search)
519        .bind(per_page)
520        .bind(offset)
521        .fetch_all(database.read())
522        .await?;
523
524        Ok(super::Pagination {
525            total: rows
526                .first()
527                .map_or(Ok(0), |row| row.try_get("total_count"))?,
528            per_page,
529            page,
530            data: rows
531                .into_iter()
532                .map(|row| Self::map(None, &row))
533                .try_collect_vec()?,
534        })
535    }
536
537    pub async fn count(database: &crate::database::Database) -> i64 {
538        sqlx::query_scalar(
539            r#"
540            SELECT COUNT(*)
541            FROM users
542            "#,
543        )
544        .fetch_one(database.read())
545        .await
546        .unwrap_or(0)
547    }
548
549    pub async fn validate_password(
550        &self,
551        database: &crate::database::Database,
552        password: &str,
553    ) -> Result<bool, crate::database::DatabaseError> {
554        if !self.has_password {
555            return Ok(true);
556        }
557
558        let row = sqlx::query(
559            r#"
560            SELECT 1
561            FROM users
562            WHERE users.uuid = $1 AND users.password = crypt($2, users.password)
563            "#,
564        )
565        .bind(self.uuid)
566        .bind(password)
567        .fetch_optional(database.read())
568        .await?;
569
570        Ok(row.is_some())
571    }
572
573    /// Update the User password, `None` will disallow password login and not require one when changing
574    pub async fn update_password(
575        &mut self,
576        database: &crate::database::Database,
577        password: Option<&str>,
578    ) -> Result<(), crate::database::DatabaseError> {
579        if let Some(password) = password {
580            sqlx::query(
581                r#"
582		            UPDATE users
583		            SET password = crypt($2, gen_salt('bf', 12))
584		            WHERE users.uuid = $1
585		            "#,
586            )
587            .bind(self.uuid)
588            .bind(password)
589            .execute(database.write())
590            .await?;
591
592            self.has_password = true;
593        } else {
594            sqlx::query(
595                r#"
596		            UPDATE users
597		            SET password = NULL
598		            WHERE users.uuid = $1
599		            "#,
600            )
601            .bind(self.uuid)
602            .bind(password)
603            .execute(database.write())
604            .await?;
605
606            self.has_password = false;
607        }
608
609        Ok(())
610    }
611
612    /// Update the User password, `None` will disallow password login and not require one when changing
613    pub async fn update_password_with_transaction(
614        &mut self,
615        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
616        password: Option<&str>,
617    ) -> Result<(), crate::database::DatabaseError> {
618        if let Some(password) = password {
619            sqlx::query(
620                r#"
621		            UPDATE users
622		            SET password = crypt($2, gen_salt('bf', 12))
623		            WHERE users.uuid = $1
624		            "#,
625            )
626            .bind(self.uuid)
627            .bind(password)
628            .execute(&mut **transaction)
629            .await?;
630
631            self.has_password = true;
632        } else {
633            sqlx::query(
634                r#"
635		            UPDATE users
636		            SET password = NULL
637		            WHERE users.uuid = $1
638		            "#,
639            )
640            .bind(self.uuid)
641            .bind(password)
642            .execute(&mut **transaction)
643            .await?;
644
645            self.has_password = false;
646        }
647
648        Ok(())
649    }
650
651    pub fn require_two_factor(&self, settings: &crate::settings::AppSettings) -> bool {
652        if let Some(role) = &self.role {
653            role.require_two_factor
654        } else {
655            match settings.app.two_factor_requirement {
656                crate::settings::app::TwoFactorRequirement::Admins => self.admin,
657                crate::settings::app::TwoFactorRequirement::AllUsers => true,
658                crate::settings::app::TwoFactorRequirement::None => false,
659            }
660        }
661    }
662
663    pub async fn into_api_full_object(
664        self,
665        state: &crate::State,
666        storage_url_retriever: &StorageUrlRetriever<'_>,
667    ) -> Result<ApiFullUser, crate::database::DatabaseError> {
668        let api_object = ApiFullUser::init_hooks(&self, state).await?;
669
670        let require_two_factor = self.require_two_factor(storage_url_retriever.get_settings());
671
672        let role = if let Some(r) = self.role {
673            Some(r.into_admin_api_object(state, ()).await?)
674        } else {
675            None
676        };
677
678        let api_object = finish_extendible!(
679            ApiFullUser {
680                uuid: self.uuid,
681                username: self.username,
682                role,
683                avatar: self
684                    .avatar
685                    .as_ref()
686                    .map(|a| storage_url_retriever.get_url(a)),
687                email: self.email,
688                name_first: self.name_first,
689                name_last: self.name_last,
690                admin: self.admin,
691                totp_enabled: self.totp_enabled,
692                totp_last_used: self.totp_last_used.map(|dt| dt.and_utc()),
693                require_two_factor,
694                language: self.language,
695                toast_position: self.toast_position,
696                start_on_grouped_servers: self.start_on_grouped_servers,
697                has_password: self.has_password,
698                created: self.created.and_utc(),
699            },
700            api_object,
701            state
702        )?;
703
704        Ok(api_object)
705    }
706}
707
708#[async_trait::async_trait]
709impl IntoApiObject for User {
710    type ApiObject = ApiUser;
711    type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
712
713    async fn into_api_object<'a>(
714        self,
715        state: &crate::State,
716        storage_url_retriever: Self::ExtraArgs<'a>,
717    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
718        let api_object = ApiUser::init_hooks(&self, state).await?;
719
720        let api_object = finish_extendible!(
721            ApiUser {
722                uuid: self.uuid,
723                username: self.username,
724                avatar: self
725                    .avatar
726                    .as_ref()
727                    .map(|a| storage_url_retriever.get_url(a)),
728                totp_enabled: self.totp_enabled,
729                created: self.created.and_utc(),
730            },
731            api_object,
732            state
733        )?;
734
735        Ok(api_object)
736    }
737}
738
739#[async_trait::async_trait]
740impl IntoAdminApiObject for User {
741    type AdminApiObject = AdminApiUser;
742    type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
743
744    async fn into_admin_api_object<'a>(
745        self,
746        state: &crate::State,
747        storage_url_retriever: Self::ExtraArgs<'a>,
748    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
749        let api_object = AdminApiUser::init_hooks(&self, state).await?;
750
751        let require_two_factor = self.require_two_factor(storage_url_retriever.get_settings());
752
753        let role = if let Some(r) = self.role {
754            Some(r.into_admin_api_object(state, ()).await?)
755        } else {
756            None
757        };
758
759        let api_object = finish_extendible!(
760            AdminApiUser {
761                uuid: self.uuid,
762                external_id: self.external_id,
763                username: self.username,
764                role,
765                avatar: self
766                    .avatar
767                    .as_ref()
768                    .map(|a| storage_url_retriever.get_url(a)),
769                email: self.email,
770                name_first: self.name_first,
771                name_last: self.name_last,
772                admin: self.admin,
773                totp_enabled: self.totp_enabled,
774                totp_last_used: self.totp_last_used.map(|dt| dt.and_utc()),
775                require_two_factor,
776                language: self.language,
777                toast_position: self.toast_position,
778                start_on_grouped_servers: self.start_on_grouped_servers,
779                has_password: self.has_password,
780                created: self.created.and_utc(),
781            },
782            api_object,
783            state
784        )?;
785
786        Ok(api_object)
787    }
788}
789
790#[derive(ToSchema, Deserialize, Validate)]
791pub struct CreateUserOptions {
792    #[garde(skip)]
793    pub role_uuid: Option<uuid::Uuid>,
794
795    #[garde(length(max = 255))]
796    #[schema(max_length = 255)]
797    pub external_id: Option<compact_str::CompactString>,
798
799    #[garde(length(chars, min = 3, max = 15), pattern("^[a-zA-Z0-9_]+$"))]
800    #[schema(min_length = 3, max_length = 15)]
801    #[schema(pattern = "^[a-zA-Z0-9_]+$")]
802    pub username: compact_str::CompactString,
803    #[garde(email, length(max = 255))]
804    #[schema(format = "email", max_length = 255)]
805    pub email: compact_str::CompactString,
806    #[garde(length(chars, min = 2, max = 255))]
807    #[schema(min_length = 2, max_length = 255)]
808    pub name_first: compact_str::CompactString,
809    #[garde(length(chars, min = 2, max = 255))]
810    #[schema(min_length = 2, max_length = 255)]
811    pub name_last: compact_str::CompactString,
812    #[garde(length(chars, min = 1, max = 512))]
813    #[schema(min_length = 1, max_length = 512)]
814    pub password: Option<String>,
815
816    #[garde(skip)]
817    pub admin: bool,
818
819    #[garde(
820        length(chars, min = 2, max = 15),
821        custom(crate::utils::validate_language)
822    )]
823    #[schema(min_length = 2, max_length = 15)]
824    pub language: compact_str::CompactString,
825}
826
827#[async_trait::async_trait]
828impl CreatableModel for User {
829    type CreateOptions<'a> = CreateUserOptions;
830    type CreateResult = Self;
831
832    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
833        static CREATE_LISTENERS: LazyLock<CreateListenerList<User>> =
834            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
835
836        &CREATE_LISTENERS
837    }
838
839    async fn create_with_transaction(
840        state: &crate::State,
841        mut options: Self::CreateOptions<'_>,
842        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
843    ) -> Result<Self, crate::database::DatabaseError> {
844        options.validate()?;
845
846        if let Some(role_uuid) = options.role_uuid {
847            super::role::Role::by_uuid_optional_cached(&state.database, role_uuid)
848                .await?
849                .ok_or(crate::database::InvalidRelationError("role"))?;
850        }
851
852        let mut query_builder = InsertQueryBuilder::new("users");
853
854        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
855
856        query_builder
857            .set("role_uuid", options.role_uuid)
858            .set("external_id", options.external_id.as_deref())
859            .set("username", &options.username)
860            .set("email", &options.email)
861            .set("name_first", &options.name_first)
862            .set("name_last", &options.name_last);
863
864        if let Some(password) = &options.password {
865            query_builder.set_expr("password", "crypt($1, gen_salt('bf', 12))", vec![password]);
866        }
867
868        query_builder
869            .set("admin", options.admin)
870            .set("language", &options.language);
871
872        let row = query_builder
873            .returning("uuid")
874            .fetch_one(&mut **transaction)
875            .await?;
876        let uuid: uuid::Uuid = row.get("uuid");
877
878        let mut result = Self::by_uuid_with_transaction(transaction, uuid).await?;
879
880        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
881
882        Ok(result)
883    }
884
885    async fn create(
886        state: &crate::State,
887        options: Self::CreateOptions<'_>,
888    ) -> Result<Self, crate::database::DatabaseError> {
889        let mut transaction = state.database.write().begin().await?;
890        let result = Self::create_with_transaction(state, options, &mut transaction).await?;
891        transaction.commit().await?;
892        Ok(result)
893    }
894}
895
896#[derive(Default, ToSchema, Serialize, Deserialize, Validate)]
897pub struct UpdateUserOptions {
898    #[garde(skip)]
899    #[serde(
900        default,
901        skip_serializing_if = "Option::is_none",
902        with = "::serde_with::rust::double_option"
903    )]
904    pub role_uuid: Option<Option<uuid::Uuid>>,
905
906    #[garde(length(chars, min = 1, max = 255))]
907    #[schema(min_length = 1, max_length = 255)]
908    #[serde(
909        default,
910        skip_serializing_if = "Option::is_none",
911        with = "::serde_with::rust::double_option"
912    )]
913    pub external_id: Option<Option<compact_str::CompactString>>,
914
915    #[garde(length(chars, min = 3, max = 15), pattern("^[a-zA-Z0-9_]+$"))]
916    #[schema(min_length = 3, max_length = 15)]
917    #[schema(pattern = "^[a-zA-Z0-9_]+$")]
918    pub username: Option<compact_str::CompactString>,
919    #[garde(email, length(max = 255))]
920    #[schema(format = "email", max_length = 255)]
921    pub email: Option<compact_str::CompactString>,
922    #[garde(length(chars, min = 2, max = 255))]
923    #[schema(min_length = 2, max_length = 255)]
924    pub name_first: Option<compact_str::CompactString>,
925    #[garde(length(chars, min = 2, max = 255))]
926    #[schema(min_length = 2, max_length = 255)]
927    pub name_last: Option<compact_str::CompactString>,
928    #[garde(length(chars, min = 8, max = 512))]
929    #[schema(min_length = 8, max_length = 512)]
930    pub password: Option<Option<compact_str::CompactString>>,
931
932    #[garde(skip)]
933    pub toast_position: Option<UserToastPosition>,
934    #[garde(skip)]
935    pub start_on_grouped_servers: Option<bool>,
936
937    #[garde(skip)]
938    pub admin: Option<bool>,
939
940    #[garde(
941        length(chars, min = 2, max = 15),
942        inner(custom(crate::utils::validate_language))
943    )]
944    #[schema(min_length = 2, max_length = 15)]
945    pub language: Option<compact_str::CompactString>,
946}
947
948#[async_trait::async_trait]
949impl UpdatableModel for User {
950    type UpdateOptions = UpdateUserOptions;
951
952    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
953        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<User>> =
954            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
955
956        &UPDATE_LISTENERS
957    }
958
959    async fn update_with_transaction(
960        &mut self,
961        state: &crate::State,
962        mut options: Self::UpdateOptions,
963        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
964    ) -> Result<(), crate::database::DatabaseError> {
965        options.validate()?;
966
967        let role = if let Some(role_uuid) = options.role_uuid {
968            if let Some(role_uuid) = role_uuid {
969                Some(Some(
970                    super::role::Role::by_uuid_optional_cached(&state.database, role_uuid)
971                        .await?
972                        .ok_or(crate::database::InvalidRelationError("role"))?,
973                ))
974            } else {
975                Some(None)
976            }
977        } else {
978            None
979        };
980
981        let mut query_builder = UpdateQueryBuilder::new("users");
982
983        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
984            .await?;
985
986        query_builder
987            .set("role_uuid", options.role_uuid.as_ref())
988            .set("external_id", options.external_id.as_ref())
989            .set("username", options.username.as_ref())
990            .set("email", options.email.as_ref())
991            .set("name_first", options.name_first.as_ref())
992            .set("name_last", options.name_last.as_ref())
993            .set("admin", options.admin)
994            .set("language", options.language.as_ref())
995            .set("toast_position", options.toast_position.as_ref())
996            .set("start_on_grouped_servers", options.start_on_grouped_servers)
997            .where_eq("uuid", self.uuid);
998
999        query_builder.execute(&mut **transaction).await?;
1000
1001        if let Some(role) = role {
1002            self.role = role;
1003        }
1004        if let Some(external_id) = options.external_id {
1005            self.external_id = external_id;
1006        }
1007        if let Some(username) = options.username {
1008            self.username = username;
1009        }
1010        if let Some(email) = options.email {
1011            self.email = email;
1012        }
1013        if let Some(name_first) = options.name_first {
1014            self.name_first = name_first;
1015        }
1016        if let Some(name_last) = options.name_last {
1017            self.name_last = name_last;
1018        }
1019        if let Some(toast_position) = options.toast_position {
1020            self.toast_position = toast_position;
1021        }
1022        if let Some(start_on_grouped_servers) = options.start_on_grouped_servers {
1023            self.start_on_grouped_servers = start_on_grouped_servers;
1024        }
1025        if let Some(admin) = options.admin {
1026            self.admin = admin;
1027        }
1028        if let Some(language) = options.language {
1029            self.language = language;
1030        }
1031
1032        if let Some(password) = options.password {
1033            self.update_password_with_transaction(transaction, password.as_deref())
1034                .await?;
1035        }
1036
1037        self.run_after_update_handlers(state, transaction).await?;
1038
1039        Ok(())
1040    }
1041}
1042
1043#[async_trait::async_trait]
1044impl DeletableModel for User {
1045    type DeleteOptions = ();
1046
1047    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
1048        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<User>> =
1049            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1050
1051        &DELETE_LISTENERS
1052    }
1053
1054    async fn delete_with_transaction(
1055        &self,
1056        state: &crate::State,
1057        options: Self::DeleteOptions,
1058        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1059    ) -> Result<(), anyhow::Error> {
1060        self.run_delete_handlers(&options, state, transaction)
1061            .await?;
1062
1063        sqlx::query(
1064            r#"
1065            DELETE FROM users
1066            WHERE users.uuid = $1
1067            "#,
1068        )
1069        .bind(self.uuid)
1070        .execute(&mut **transaction)
1071        .await?;
1072
1073        self.run_after_delete_handlers(&options, state, transaction)
1074            .await?;
1075
1076        Ok(())
1077    }
1078
1079    async fn delete(
1080        &self,
1081        state: &crate::State,
1082        options: Self::DeleteOptions,
1083    ) -> Result<(), anyhow::Error> {
1084        let mut transaction = state.database.write().begin().await?;
1085        self.delete_with_transaction(state, options, &mut transaction)
1086            .await?;
1087        transaction.commit().await?;
1088
1089        state.storage.remove(self.avatar.as_deref()).await?;
1090
1091        Ok(())
1092    }
1093}
1094
1095#[async_trait::async_trait]
1096impl ByUuid for User {
1097    async fn by_uuid(
1098        database: &crate::database::Database,
1099        uuid: uuid::Uuid,
1100    ) -> Result<Self, crate::database::DatabaseError> {
1101        let row = sqlx::query(&format!(
1102            r#"
1103            SELECT {}
1104            FROM users
1105            LEFT JOIN roles ON roles.uuid = users.role_uuid
1106            WHERE users.uuid = $1
1107            "#,
1108            Self::columns_sql(None)
1109        ))
1110        .bind(uuid)
1111        .fetch_one(database.read())
1112        .await?;
1113
1114        Self::map(None, &row)
1115    }
1116
1117    async fn by_uuid_with_transaction(
1118        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1119        uuid: uuid::Uuid,
1120    ) -> Result<Self, crate::database::DatabaseError> {
1121        let row = sqlx::query(&format!(
1122            r#"
1123            SELECT {}
1124            FROM users
1125            LEFT JOIN roles ON roles.uuid = users.role_uuid
1126            WHERE users.uuid = $1
1127            "#,
1128            Self::columns_sql(None)
1129        ))
1130        .bind(uuid)
1131        .fetch_one(&mut **transaction)
1132        .await?;
1133
1134        Self::map(None, &row)
1135    }
1136}
1137
1138#[schema_extension_derive::extendible]
1139#[init_args(User, crate::State)]
1140#[hook_args(crate::State)]
1141#[derive(ToSchema, Serialize)]
1142#[schema(title = "User")]
1143pub struct ApiUser {
1144    pub uuid: uuid::Uuid,
1145
1146    pub username: compact_str::CompactString,
1147    pub avatar: Option<String>,
1148
1149    pub totp_enabled: bool,
1150
1151    pub created: chrono::DateTime<chrono::Utc>,
1152}
1153
1154#[schema_extension_derive::extendible]
1155#[init_args(User, crate::State)]
1156#[hook_args(crate::State)]
1157#[derive(ToSchema, Serialize)]
1158#[schema(title = "FullUser")]
1159pub struct ApiFullUser {
1160    pub uuid: uuid::Uuid,
1161
1162    pub username: compact_str::CompactString,
1163    pub role: Option<super::role::AdminApiRole>,
1164    pub avatar: Option<String>,
1165    pub email: compact_str::CompactString,
1166
1167    pub name_first: compact_str::CompactString,
1168    pub name_last: compact_str::CompactString,
1169
1170    pub admin: bool,
1171    pub totp_enabled: bool,
1172    pub totp_last_used: Option<chrono::DateTime<chrono::Utc>>,
1173    pub require_two_factor: bool,
1174
1175    pub language: compact_str::CompactString,
1176    pub toast_position: UserToastPosition,
1177    pub start_on_grouped_servers: bool,
1178
1179    pub has_password: bool,
1180
1181    pub created: chrono::DateTime<chrono::Utc>,
1182}
1183
1184#[schema_extension_derive::extendible]
1185#[init_args(User, crate::State)]
1186#[hook_args(crate::State)]
1187#[derive(ToSchema, Serialize)]
1188#[schema(title = "AdminUser")]
1189pub struct AdminApiUser {
1190    pub uuid: uuid::Uuid,
1191    pub external_id: Option<compact_str::CompactString>,
1192
1193    pub username: compact_str::CompactString,
1194    pub role: Option<super::role::AdminApiRole>,
1195    pub avatar: Option<String>,
1196    pub email: compact_str::CompactString,
1197
1198    pub name_first: compact_str::CompactString,
1199    pub name_last: compact_str::CompactString,
1200
1201    pub admin: bool,
1202    pub totp_enabled: bool,
1203    pub totp_last_used: Option<chrono::DateTime<chrono::Utc>>,
1204    pub require_two_factor: bool,
1205
1206    pub language: compact_str::CompactString,
1207    pub toast_position: UserToastPosition,
1208    pub start_on_grouped_servers: bool,
1209
1210    pub has_password: bool,
1211
1212    pub created: chrono::DateTime<chrono::Utc>,
1213}