shared/models/
user.rs

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