diff --git a/gpodder/src/models.rs b/gpodder/src/models.rs index fb9339b..c37f97f 100644 --- a/gpodder/src/models.rs +++ b/gpodder/src/models.rs @@ -72,3 +72,8 @@ pub struct Page { pub page: u32, pub per_page: u32, } + +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct UserFilter { + pub username: Option, +} diff --git a/gpodder/src/repository/admin.rs b/gpodder/src/repository/admin.rs index 8a3e845..2123026 100644 --- a/gpodder/src/repository/admin.rs +++ b/gpodder/src/repository/admin.rs @@ -7,7 +7,11 @@ pub struct AdminRepository<'a> { } impl<'a> AdminRepository<'a> { - pub fn paginated_users(&self, page: Page) -> Result, AuthErr> { - self.store.paginated_users(page) + pub fn paginated_users( + &self, + page: Page, + filter: &models::UserFilter, + ) -> Result, AuthErr> { + self.store.paginated_users(page, filter) } } diff --git a/gpodder/src/store.rs b/gpodder/src/store.rs index 1daa3c2..7f32374 100644 --- a/gpodder/src/store.rs +++ b/gpodder/src/store.rs @@ -66,7 +66,7 @@ pub trait GpodderAuthStore { fn remove_old_sessions(&self, min_last_seen: DateTime) -> Result; /// Return the given page of users, ordered by username - fn paginated_users(&self, page: Page) -> Result, AuthErr>; + fn paginated_users(&self, page: Page, filter: &UserFilter) -> Result, AuthErr>; } pub trait GpodderDeviceStore { diff --git a/gpodder_sqlite/src/repository/auth.rs b/gpodder_sqlite/src/repository/auth.rs index ced2606..38af31e 100644 --- a/gpodder_sqlite/src/repository/auth.rs +++ b/gpodder_sqlite/src/repository/auth.rs @@ -143,16 +143,31 @@ impl gpodder::GpodderAuthStore for SqliteRepository { .map_err(AuthErr::from) } - fn paginated_users(&self, page: gpodder::Page) -> Result, AuthErr> { - Ok(users::table - .select(User::as_select()) - .order(users::username.asc()) - .offset((page.page * page.per_page) as i64) - .limit(page.per_page as i64) - .get_results(&mut self.pool.get().map_err(DbError::from)?) - .map_err(DbError::from)? - .into_iter() - .map(gpodder::User::from) - .collect()) + fn paginated_users( + &self, + page: gpodder::Page, + filter: &gpodder::UserFilter, + ) -> Result, AuthErr> { + (|| { + let mut query = users::table + .select(User::as_select()) + .order(users::username.asc()) + .offset((page.page * page.per_page) as i64) + .limit(page.per_page as i64) + .into_boxed(); + + if let Some(username) = &filter.username { + // Case insensitive by default for SQLite + query = query.filter(users::username.like(format!("%{username}%"))); + } + + Ok::<_, DbError>( + query + .load_iter(&mut self.pool.get()?)? + .map(|res| res.map(gpodder::User::from)) + .collect::, _>>()?, + ) + })() + .map_err(AuthErr::from) } } diff --git a/otter/src/server/web/users.rs b/otter/src/server/web/users.rs index f5d3467..dd54c0e 100644 --- a/otter/src/server/web/users.rs +++ b/otter/src/server/web/users.rs @@ -4,6 +4,7 @@ use axum::{ http::HeaderMap, routing::{delete, get}, }; +use serde::Deserialize; use crate::{ server::{ @@ -22,22 +23,46 @@ pub fn router(ctx: Context) -> Router { )) } -pub async fn get_users( +#[derive(Deserialize, Clone)] +struct UserFilter { + username: Option, +} + +impl From for gpodder::UserFilter { + fn from(value: UserFilter) -> Self { + Self { + username: value.username, + } + } +} + +impl ToQuery for UserFilter { + fn to_query(self) -> crate::web::Query { + crate::web::Query::default().opt_parameter("username", self.username) + } +} + +async fn get_users( State(ctx): State, headers: HeaderMap, Extension(session): Extension, Query(page): Query, + Query(filter): Query, ) -> AppResult>> { let next_page = page.next_page(); + let filter_clone = filter.clone(); + let user_id = session.user.id; let users = tokio::task::spawn_blocking(move || { - ctx.store.admin(&session.user)?.paginated_users(page.into()) + ctx.store + .admin(&session.user)? + .paginated_users(page.into(), &filter.into()) }) .await .unwrap()?; - let next_page_query = - (users.len() == next_page.per_page as usize).then_some(next_page.to_query()); + let next_page_query = (users.len() == next_page.per_page as usize) + .then_some(next_page.to_query().join(filter_clone)); Ok(View::Users(users, user_id, next_page_query) .page(&headers) diff --git a/otter/src/web/query.rs b/otter/src/web/query.rs index 409afd5..e8585ea 100644 --- a/otter/src/web/query.rs +++ b/otter/src/web/query.rs @@ -23,6 +23,15 @@ impl Query { self } + + /// Convenience method for adding possibly empty parameter values from options + pub fn opt_parameter(self, key: impl ToString, value: Option) -> Self { + if let Some(value) = value { + self.parameter(key, value) + } else { + self + } + } } /// Allows objects to be converted into queries diff --git a/otter/src/web/templates/views/users.html b/otter/src/web/templates/views/users.html index 22dee43..f66f3fe 100644 --- a/otter/src/web/templates/views/users.html +++ b/otter/src/web/templates/views/users.html @@ -1,15 +1,24 @@

Users

- + +
- {% for user in users %} + {%- for user in users %} - {% endfor %} + {% endfor -%} {%- if next_page_query %}
Username
{{ user.username }}