You've already forked authentication-service
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:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user