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