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