1use async_trait::async_trait;
12use mas_data_model::{Clock, UlidExt as _, User};
13use mas_storage::user::{UserFilter, UserRepository};
14use rand::Rng;
15use sea_query::{
16 Expr, ExprTrait, PostgresQueryBuilder, Query, SimpleExpr, extension::postgres::PgExpr as _,
17};
18use sea_query_sqlx::SqlxBinder;
19use sqlx::PgConnection;
20use ulid::Ulid;
21use uuid::Uuid;
22
23use crate::{
24 DatabaseError,
25 filter::{Filter, StatementExt},
26 iden::{CompatSessions, OAuth2Sessions, Users},
27 pagination::QueryBuilderExt,
28 tracing::ExecuteExt,
29};
30
31mod email;
32mod password;
33mod recovery;
34mod registration;
35mod registration_token;
36mod session;
37mod terms;
38
39#[cfg(test)]
40mod tests;
41
42pub use self::{
43 email::PgUserEmailRepository, password::PgUserPasswordRepository,
44 recovery::PgUserRecoveryRepository, registration::PgUserRegistrationRepository,
45 registration_token::PgUserRegistrationTokenRepository, session::PgBrowserSessionRepository,
46 terms::PgUserTermsRepository,
47};
48
49pub struct PgUserRepository<'c> {
51 conn: &'c mut PgConnection,
52}
53
54impl<'c> PgUserRepository<'c> {
55 pub fn new(conn: &'c mut PgConnection) -> Self {
57 Self { conn }
58 }
59}
60
61mod priv_ {
62 #![allow(missing_docs)]
65
66 use chrono::{DateTime, Utc};
67 use mas_storage::pagination::Node;
68 use sea_query::enum_def;
69 use ulid::Ulid;
70 use uuid::Uuid;
71
72 #[derive(Debug, Clone, sqlx::FromRow)]
73 #[enum_def]
74 pub(super) struct UserLookup {
75 pub(super) user_id: Uuid,
76 pub(super) username: String,
77 pub(super) created_at: DateTime<Utc>,
78 pub(super) locked_at: Option<DateTime<Utc>>,
79 pub(super) deactivated_at: Option<DateTime<Utc>>,
80 pub(super) can_request_admin: bool,
81 pub(super) is_guest: bool,
82 }
83
84 impl Node<Ulid> for UserLookup {
85 fn cursor(&self) -> Ulid {
86 self.user_id.into()
87 }
88 }
89}
90
91use priv_::{UserLookup, UserLookupIden};
92
93impl From<UserLookup> for User {
94 fn from(value: UserLookup) -> Self {
95 let id = value.user_id.into();
96 Self {
97 id,
98 username: value.username,
99 sub: id.to_string(),
100 created_at: value.created_at,
101 locked_at: value.locked_at,
102 deactivated_at: value.deactivated_at,
103 can_request_admin: value.can_request_admin,
104 is_guest: value.is_guest,
105 }
106 }
107}
108
109impl Filter for UserFilter<'_> {
110 fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition {
111 sea_query::Condition::all()
112 .add_option(self.state().map(|state| {
113 match state {
114 mas_storage::user::UserState::Deactivated => {
115 Expr::col((Users::Table, Users::DeactivatedAt)).is_not_null()
116 }
117 mas_storage::user::UserState::Locked => {
118 Expr::col((Users::Table, Users::LockedAt)).is_not_null()
119 }
120 mas_storage::user::UserState::Active => {
121 Expr::col((Users::Table, Users::LockedAt))
122 .is_null()
123 .and(Expr::col((Users::Table, Users::DeactivatedAt)).is_null())
124 }
125 }
126 }))
127 .add_option(self.can_request_admin().map(|can_request_admin| {
128 Expr::col((Users::Table, Users::CanRequestAdmin)).eq(can_request_admin)
129 }))
130 .add_option(
131 self.is_guest()
132 .map(|is_guest| Expr::col((Users::Table, Users::IsGuest)).eq(is_guest)),
133 )
134 .add_option(self.search().map(|search| {
135 Expr::col((Users::Table, Users::Username)).ilike(format!("%{search}%"))
136 }))
137 .add_option(self.active_oauth2_session_for_any_of_clients().map(
138 |clients| -> SimpleExpr {
139 let client_ids: Vec<SimpleExpr> =
140 clients.iter().map(|c| Expr::val(Uuid::from(*c))).collect();
141 Expr::exists(
142 Query::select()
143 .expr(Expr::cust("1"))
144 .from(OAuth2Sessions::Table)
145 .and_where(
146 Expr::col((OAuth2Sessions::Table, OAuth2Sessions::UserId))
147 .equals((Users::Table, Users::UserId)),
148 )
149 .and_where(
150 Expr::col((OAuth2Sessions::Table, OAuth2Sessions::FinishedAt))
151 .is_null(),
152 )
153 .and_where(
154 Expr::col((OAuth2Sessions::Table, OAuth2Sessions::OAuth2ClientId))
155 .is_in(client_ids),
156 )
157 .take(),
158 )
159 },
160 ))
161 .add_option(self.has_active_oauth2_session().map(|has| -> SimpleExpr {
162 let exists = Expr::exists(
163 Query::select()
164 .expr(Expr::cust("1"))
165 .from(OAuth2Sessions::Table)
166 .and_where(
167 Expr::col((OAuth2Sessions::Table, OAuth2Sessions::UserId))
168 .equals((Users::Table, Users::UserId)),
169 )
170 .and_where(
171 Expr::col((OAuth2Sessions::Table, OAuth2Sessions::FinishedAt))
172 .is_null(),
173 )
174 .take(),
175 );
176 if has { exists } else { exists.not() }
177 }))
178 .add_option(self.has_active_compat_session().map(|has| -> SimpleExpr {
179 let exists = Expr::exists(
180 Query::select()
181 .expr(Expr::cust("1"))
182 .from(CompatSessions::Table)
183 .and_where(
184 Expr::col((CompatSessions::Table, CompatSessions::UserId))
185 .equals((Users::Table, Users::UserId)),
186 )
187 .and_where(
188 Expr::col((CompatSessions::Table, CompatSessions::FinishedAt))
189 .is_null(),
190 )
191 .take(),
192 );
193 if has { exists } else { exists.not() }
194 }))
195 }
196}
197
198#[async_trait]
199impl UserRepository for PgUserRepository<'_> {
200 type Error = DatabaseError;
201
202 #[tracing::instrument(
203 name = "db.user.lookup",
204 skip_all,
205 fields(
206 db.query.text,
207 user.id = %id,
208 ),
209 )]
210 async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error> {
211 let res = sqlx::query_as!(
212 UserLookup,
213 r#"
214 SELECT user_id
215 , username
216 , created_at
217 , locked_at
218 , deactivated_at
219 , can_request_admin
220 , is_guest
221 FROM users
222 WHERE user_id = $1
223 "#,
224 Uuid::from(id),
225 )
226 .traced()
227 .fetch_optional(&mut *self.conn)
228 .await?;
229
230 let Some(res) = res else { return Ok(None) };
231
232 Ok(Some(res.into()))
233 }
234
235 #[tracing::instrument(
236 name = "db.user.find_by_username",
237 skip_all,
238 fields(
239 db.query.text,
240 user.username = username,
241 ),
242 )]
243 async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error> {
244 let res = sqlx::query_as!(
248 UserLookup,
249 r#"
250 SELECT user_id
251 , username
252 , created_at
253 , locked_at
254 , deactivated_at
255 , can_request_admin
256 , is_guest
257 FROM users
258 WHERE LOWER(username) = LOWER($1)
259 "#,
260 username,
261 )
262 .traced()
263 .fetch_all(&mut *self.conn)
264 .await?;
265
266 match &res[..] {
267 [user] => Ok(Some(user.clone().into())),
269 [] => Ok(None),
271 list => {
272 if let Some(user) = list.iter().find(|user| user.username == username) {
275 Ok(Some(user.clone().into()))
276 } else {
277 Ok(None)
279 }
280 }
281 }
282 }
283
284 #[tracing::instrument(
285 name = "db.user.add",
286 skip_all,
287 fields(
288 db.query.text,
289 user.username = username,
290 user.id,
291 ),
292 )]
293 async fn add(
294 &mut self,
295 rng: &mut (dyn Rng + Send),
296 clock: &dyn Clock,
297 username: String,
298 ) -> Result<User, Self::Error> {
299 let created_at = clock.now();
300 let id = Ulid::from_datetime_with_rng(created_at, rng);
301 tracing::Span::current().record("user.id", tracing::field::display(id));
302
303 let res = sqlx::query!(
304 r#"
305 INSERT INTO users (user_id, username, created_at)
306 VALUES ($1, $2, $3)
307 ON CONFLICT (username) DO NOTHING
308 "#,
309 Uuid::from(id),
310 username,
311 created_at,
312 )
313 .traced()
314 .execute(&mut *self.conn)
315 .await?;
316
317 DatabaseError::ensure_affected_rows(&res, 1)?;
320
321 Ok(User {
322 id,
323 username,
324 sub: id.to_string(),
325 created_at,
326 locked_at: None,
327 deactivated_at: None,
328 can_request_admin: false,
329 is_guest: false,
330 })
331 }
332
333 #[tracing::instrument(
334 name = "db.user.exists",
335 skip_all,
336 fields(
337 db.query.text,
338 user.username = username,
339 ),
340 )]
341 async fn exists(&mut self, username: &str) -> Result<bool, Self::Error> {
342 let exists = sqlx::query_scalar!(
343 r#"
344 SELECT EXISTS(
345 SELECT 1 FROM users WHERE LOWER(username) = LOWER($1)
346 ) AS "exists!"
347 "#,
348 username
349 )
350 .traced()
351 .fetch_one(&mut *self.conn)
352 .await?;
353
354 Ok(exists)
355 }
356
357 #[tracing::instrument(
358 name = "db.user.lock",
359 skip_all,
360 fields(
361 db.query.text,
362 %user.id,
363 ),
364 )]
365 async fn lock(&mut self, clock: &dyn Clock, mut user: User) -> Result<User, Self::Error> {
366 if user.locked_at.is_some() {
367 return Ok(user);
368 }
369
370 let locked_at = clock.now();
371 let res = sqlx::query!(
372 r#"
373 UPDATE users
374 SET locked_at = $1
375 WHERE user_id = $2
376 "#,
377 locked_at,
378 Uuid::from(user.id),
379 )
380 .traced()
381 .execute(&mut *self.conn)
382 .await?;
383
384 DatabaseError::ensure_affected_rows(&res, 1)?;
385
386 user.locked_at = Some(locked_at);
387
388 Ok(user)
389 }
390
391 #[tracing::instrument(
392 name = "db.user.unlock",
393 skip_all,
394 fields(
395 db.query.text,
396 %user.id,
397 ),
398 )]
399 async fn unlock(&mut self, mut user: User) -> Result<User, Self::Error> {
400 if user.locked_at.is_none() {
401 return Ok(user);
402 }
403
404 let res = sqlx::query!(
405 r#"
406 UPDATE users
407 SET locked_at = NULL
408 WHERE user_id = $1
409 "#,
410 Uuid::from(user.id),
411 )
412 .traced()
413 .execute(&mut *self.conn)
414 .await?;
415
416 DatabaseError::ensure_affected_rows(&res, 1)?;
417
418 user.locked_at = None;
419
420 Ok(user)
421 }
422
423 #[tracing::instrument(
424 name = "db.user.deactivate",
425 skip_all,
426 fields(
427 db.query.text,
428 %user.id,
429 ),
430 )]
431 async fn deactivate(&mut self, clock: &dyn Clock, mut user: User) -> Result<User, Self::Error> {
432 if user.deactivated_at.is_some() {
433 return Ok(user);
434 }
435
436 let deactivated_at = clock.now();
437 let res = sqlx::query!(
438 r#"
439 UPDATE users
440 SET deactivated_at = $2
441 WHERE user_id = $1
442 AND deactivated_at IS NULL
443 "#,
444 Uuid::from(user.id),
445 deactivated_at,
446 )
447 .traced()
448 .execute(&mut *self.conn)
449 .await?;
450
451 DatabaseError::ensure_affected_rows(&res, 1)?;
452
453 user.deactivated_at = Some(deactivated_at);
454
455 Ok(user)
456 }
457
458 #[tracing::instrument(
459 name = "db.user.reactivate",
460 skip_all,
461 fields(
462 db.query.text,
463 %user.id,
464 ),
465 )]
466 async fn reactivate(&mut self, mut user: User) -> Result<User, Self::Error> {
467 if user.deactivated_at.is_none() {
468 return Ok(user);
469 }
470
471 let res = sqlx::query!(
472 r#"
473 UPDATE users
474 SET deactivated_at = NULL
475 WHERE user_id = $1
476 "#,
477 Uuid::from(user.id),
478 )
479 .traced()
480 .execute(&mut *self.conn)
481 .await?;
482
483 DatabaseError::ensure_affected_rows(&res, 1)?;
484
485 user.deactivated_at = None;
486
487 Ok(user)
488 }
489
490 #[tracing::instrument(
491 name = "db.user.delete_unsupported_threepids",
492 skip_all,
493 fields(
494 db.query.text,
495 %user.id,
496 ),
497 )]
498 async fn delete_unsupported_threepids(&mut self, user: &User) -> Result<usize, Self::Error> {
499 let res = sqlx::query!(
500 r#"
501 DELETE FROM user_unsupported_third_party_ids
502 WHERE user_id = $1
503 "#,
504 Uuid::from(user.id),
505 )
506 .traced()
507 .execute(&mut *self.conn)
508 .await?;
509
510 Ok(res.rows_affected().try_into().unwrap_or(usize::MAX))
511 }
512
513 #[tracing::instrument(
514 name = "db.user.set_can_request_admin",
515 skip_all,
516 fields(
517 db.query.text,
518 %user.id,
519 user.can_request_admin = can_request_admin,
520 ),
521 )]
522 async fn set_can_request_admin(
523 &mut self,
524 mut user: User,
525 can_request_admin: bool,
526 ) -> Result<User, Self::Error> {
527 let res = sqlx::query!(
528 r#"
529 UPDATE users
530 SET can_request_admin = $2
531 WHERE user_id = $1
532 "#,
533 Uuid::from(user.id),
534 can_request_admin,
535 )
536 .traced()
537 .execute(&mut *self.conn)
538 .await?;
539
540 DatabaseError::ensure_affected_rows(&res, 1)?;
541
542 user.can_request_admin = can_request_admin;
543
544 Ok(user)
545 }
546
547 #[tracing::instrument(
548 name = "db.user.list",
549 skip_all,
550 fields(
551 db.query.text,
552 ),
553 )]
554 async fn list(
555 &mut self,
556 filter: UserFilter<'_>,
557 pagination: mas_storage::Pagination,
558 ) -> Result<mas_storage::Page<User>, Self::Error> {
559 let (sql, arguments) = Query::select()
560 .expr_as(
561 Expr::col((Users::Table, Users::UserId)),
562 UserLookupIden::UserId,
563 )
564 .expr_as(
565 Expr::col((Users::Table, Users::Username)),
566 UserLookupIden::Username,
567 )
568 .expr_as(
569 Expr::col((Users::Table, Users::CreatedAt)),
570 UserLookupIden::CreatedAt,
571 )
572 .expr_as(
573 Expr::col((Users::Table, Users::LockedAt)),
574 UserLookupIden::LockedAt,
575 )
576 .expr_as(
577 Expr::col((Users::Table, Users::DeactivatedAt)),
578 UserLookupIden::DeactivatedAt,
579 )
580 .expr_as(
581 Expr::col((Users::Table, Users::CanRequestAdmin)),
582 UserLookupIden::CanRequestAdmin,
583 )
584 .expr_as(
585 Expr::col((Users::Table, Users::IsGuest)),
586 UserLookupIden::IsGuest,
587 )
588 .from(Users::Table)
589 .apply_filter(filter)
590 .generate_pagination((Users::Table, Users::UserId), pagination)
591 .build_sqlx(PostgresQueryBuilder);
592
593 let edges: Vec<UserLookup> = sqlx::query_as_with(&sql, arguments)
594 .traced()
595 .fetch_all(&mut *self.conn)
596 .await?;
597
598 let page = pagination.process(edges).map(User::from);
599
600 Ok(page)
601 }
602
603 #[tracing::instrument(
604 name = "db.user.count",
605 skip_all,
606 fields(
607 db.query.text,
608 ),
609 )]
610 async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error> {
611 let (sql, arguments) = Query::select()
612 .expr(Expr::col((Users::Table, Users::UserId)).count())
613 .from(Users::Table)
614 .apply_filter(filter)
615 .build_sqlx(PostgresQueryBuilder);
616
617 let count: i64 = sqlx::query_scalar_with(&sql, arguments)
618 .traced()
619 .fetch_one(&mut *self.conn)
620 .await?;
621
622 count
623 .try_into()
624 .map_err(DatabaseError::to_invalid_operation)
625 }
626
627 #[tracing::instrument(
628 name = "db.user.acquire_lock_for_sync",
629 skip_all,
630 fields(
631 db.query.text,
632 user.id = %user.id,
633 ),
634 )]
635 async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error> {
636 let lock_id = (u128::from(user.id) & 0xffff_ffff_ffff_ffff) as i64;
644
645 sqlx::query!(
648 r#"
649 SELECT pg_advisory_xact_lock($1)
650 "#,
651 lock_id,
652 )
653 .traced()
654 .execute(&mut *self.conn)
655 .await?;
656
657 Ok(())
658 }
659}