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