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 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 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 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 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}