1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-21 23:00:50 +03:00

Better user emails pagination and filtering

This commit is contained in:
Quentin Gliech
2023-07-21 15:31:55 +02:00
parent 12ad572db8
commit a75a53cc24
9 changed files with 266 additions and 74 deletions

View File

@@ -31,6 +31,16 @@ pub enum Users {
PrimaryUserEmailId,
}
#[derive(sea_query::Iden)]
pub enum UserEmails {
Table,
UserEmailId,
UserId,
Email,
CreatedAt,
ConfirmedAt,
}
#[derive(sea_query::Iden)]
pub enum CompatSessions {
Table,

View File

@@ -15,15 +15,20 @@
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use mas_data_model::{User, UserEmail, UserEmailVerification, UserEmailVerificationState};
use mas_storage::{user::UserEmailRepository, Clock, Page, Pagination};
use mas_storage::{
user::{UserEmailFilter, UserEmailRepository},
Clock, Page, Pagination,
};
use rand::RngCore;
use sqlx::{PgConnection, QueryBuilder};
use sea_query::{enum_def, Expr, IntoColumnRef, PostgresQueryBuilder, Query};
use sqlx::PgConnection;
use tracing::{info_span, Instrument};
use ulid::Ulid;
use uuid::Uuid;
use crate::{
pagination::QueryBuilderExt, tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError,
iden::UserEmails, pagination::QueryBuilderExt, sea_query_sqlx::map_values, tracing::ExecuteExt,
DatabaseError, DatabaseInconsistencyError,
};
/// An implementation of [`UserEmailRepository`] for a PostgreSQL connection
@@ -40,6 +45,7 @@ impl<'c> PgUserEmailRepository<'c> {
}
#[derive(Debug, Clone, sqlx::FromRow)]
#[enum_def]
struct UserEmailLookup {
user_email_id: Uuid,
user_id: Uuid,
@@ -225,42 +231,65 @@ impl<'c> UserEmailRepository for PgUserEmailRepository<'c> {
}
#[tracing::instrument(
name = "db.user_email.list_paginated",
name = "db.user_email.list",
skip_all,
fields(
db.statement,
%user.id,
),
err,
)]
async fn list_paginated(
async fn list(
&mut self,
user: &User,
filter: UserEmailFilter<'_>,
pagination: Pagination,
) -> Result<Page<UserEmail>, DatabaseError> {
let mut query = QueryBuilder::new(
r#"
SELECT user_email_id
, user_id
, email
, created_at
, confirmed_at
FROM user_emails
"#,
);
let (sql, values) = Query::select()
.expr_as(
Expr::col((UserEmails::Table, UserEmails::UserEmailId)),
UserEmailLookupIden::UserEmailId,
)
.expr_as(
Expr::col((UserEmails::Table, UserEmails::UserId)),
UserEmailLookupIden::UserId,
)
.expr_as(
Expr::col((UserEmails::Table, UserEmails::Email)),
UserEmailLookupIden::Email,
)
.expr_as(
Expr::col((UserEmails::Table, UserEmails::CreatedAt)),
UserEmailLookupIden::CreatedAt,
)
.expr_as(
Expr::col((UserEmails::Table, UserEmails::ConfirmedAt)),
UserEmailLookupIden::ConfirmedAt,
)
.from(UserEmails::Table)
.and_where_option(filter.user().map(|user| {
Expr::col((UserEmails::Table, UserEmails::UserId)).eq(Uuid::from(user.id))
}))
.and_where_option(filter.state().map(|state| {
if state.is_verified() {
Expr::col((UserEmails::Table, UserEmails::ConfirmedAt)).is_not_null()
} else {
Expr::col((UserEmails::Table, UserEmails::ConfirmedAt)).is_null()
}
}))
.generate_pagination(
(UserEmails::Table, UserEmails::UserEmailId).into_column_ref(),
pagination,
)
.build(PostgresQueryBuilder);
query
.push(" WHERE user_id = ")
.push_bind(Uuid::from(user.id))
.generate_pagination("user_email_id", pagination);
let arguments = map_values(values);
let edges: Vec<UserEmailLookup> = query
.build_query_as()
let edges: Vec<UserEmailLookup> = sqlx::query_as_with(&sql, arguments)
.traced()
.fetch_all(&mut *self.conn)
.await?;
let page = pagination.process(edges).map(UserEmail::from);
Ok(page)
}
@@ -269,28 +298,35 @@ impl<'c> UserEmailRepository for PgUserEmailRepository<'c> {
skip_all,
fields(
db.statement,
%user.id,
),
err,
)]
async fn count(&mut self, user: &User) -> Result<usize, Self::Error> {
let res = sqlx::query_scalar!(
r#"
SELECT COUNT(*)
FROM user_emails
WHERE user_id = $1
"#,
Uuid::from(user.id),
)
.traced()
.fetch_one(&mut *self.conn)
.await?;
async fn count(&mut self, filter: UserEmailFilter<'_>) -> Result<usize, Self::Error> {
let (sql, values) = Query::select()
.expr(Expr::col((UserEmails::Table, UserEmails::UserEmailId)).count())
.from(UserEmails::Table)
.and_where_option(filter.user().map(|user| {
Expr::col((UserEmails::Table, UserEmails::UserId)).eq(Uuid::from(user.id))
}))
.and_where_option(filter.state().map(|state| {
if state.is_verified() {
Expr::col((UserEmails::Table, UserEmails::ConfirmedAt)).is_not_null()
} else {
Expr::col((UserEmails::Table, UserEmails::ConfirmedAt)).is_null()
}
}))
.build(PostgresQueryBuilder);
let res = res.unwrap_or_default();
let arguments = map_values(values);
Ok(res
let count: i64 = sqlx::query_scalar_with(&sql, arguments)
.traced()
.fetch_one(&mut *self.conn)
.await?;
count
.try_into()
.map_err(DatabaseError::to_invalid_operation)?)
.map_err(DatabaseError::to_invalid_operation)
}
#[tracing::instrument(

View File

@@ -16,7 +16,7 @@ use chrono::Duration;
use mas_storage::{
clock::MockClock,
user::{
BrowserSessionFilter, BrowserSessionRepository, UserEmailRepository,
BrowserSessionFilter, BrowserSessionRepository, UserEmailFilter, UserEmailRepository,
UserPasswordRepository, UserRepository,
},
Pagination, Repository, RepositoryAccess,
@@ -98,7 +98,14 @@ async fn test_user_email_repo(pool: PgPool) {
.unwrap()
.is_none());
assert_eq!(repo.user_email().count(&user).await.unwrap(), 0);
let all = UserEmailFilter::new().for_user(&user);
let pending = all.pending_only();
let verified = all.verified_only();
// Check the counts
assert_eq!(repo.user_email().count(all).await.unwrap(), 0);
assert_eq!(repo.user_email().count(pending).await.unwrap(), 0);
assert_eq!(repo.user_email().count(verified).await.unwrap(), 0);
let user_email = repo
.user_email()
@@ -110,7 +117,10 @@ async fn test_user_email_repo(pool: PgPool) {
assert_eq!(user_email.email, EMAIL);
assert!(user_email.confirmed_at.is_none());
assert_eq!(repo.user_email().count(&user).await.unwrap(), 1);
// Check the counts
assert_eq!(repo.user_email().count(all).await.unwrap(), 1);
assert_eq!(repo.user_email().count(pending).await.unwrap(), 1);
assert_eq!(repo.user_email().count(verified).await.unwrap(), 0);
assert!(repo
.user_email()
@@ -181,6 +191,11 @@ async fn test_user_email_repo(pool: PgPool) {
.await
.unwrap();
// Check the counts
assert_eq!(repo.user_email().count(all).await.unwrap(), 1);
assert_eq!(repo.user_email().count(pending).await.unwrap(), 0);
assert_eq!(repo.user_email().count(verified).await.unwrap(), 1);
// Reload the user_email
let user_email = repo
.user_email()
@@ -236,16 +251,35 @@ async fn test_user_email_repo(pool: PgPool) {
// Listing the user emails should work
let emails = repo
.user_email()
.list_paginated(&user, Pagination::first(10))
.list(all, Pagination::first(10))
.await
.unwrap();
assert!(!emails.has_next_page);
assert_eq!(emails.edges.len(), 1);
assert_eq!(emails.edges[0], user_email);
let emails = repo
.user_email()
.list(verified, Pagination::first(10))
.await
.unwrap();
assert!(!emails.has_next_page);
assert_eq!(emails.edges.len(), 1);
assert_eq!(emails.edges[0], user_email);
let emails = repo
.user_email()
.list(pending, Pagination::first(10))
.await
.unwrap();
assert!(!emails.has_next_page);
assert!(emails.edges.is_empty());
// Deleting the user email should work
repo.user_email().remove(user_email).await.unwrap();
assert_eq!(repo.user_email().count(&user).await.unwrap(), 0);
assert_eq!(repo.user_email().count(all).await.unwrap(), 0);
assert_eq!(repo.user_email().count(pending).await.unwrap(), 0);
assert_eq!(repo.user_email().count(verified).await.unwrap(), 0);
// Reload the user
let user = repo