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