Compare commits
2 Commits
c7c5cf889c
...
6c8183c1e3
Author | SHA1 | Date |
---|---|---|
|
6c8183c1e3 | |
|
fce301080c |
|
@ -72,3 +72,8 @@ pub struct Page {
|
||||||
pub page: u32,
|
pub page: u32,
|
||||||
pub per_page: u32,
|
pub per_page: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub struct UserFilter {
|
||||||
|
pub username: Option<String>,
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,11 @@ pub struct AdminRepository<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AdminRepository<'a> {
|
impl<'a> AdminRepository<'a> {
|
||||||
pub fn paginated_users(&self, page: Page) -> Result<Vec<models::User>, AuthErr> {
|
pub fn paginated_users(
|
||||||
self.store.paginated_users(page)
|
&self,
|
||||||
|
page: Page,
|
||||||
|
filter: &models::UserFilter,
|
||||||
|
) -> Result<Vec<models::User>, AuthErr> {
|
||||||
|
self.store.paginated_users(page, filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ pub trait GpodderAuthStore {
|
||||||
fn remove_old_sessions(&self, min_last_seen: DateTime<Utc>) -> Result<usize, AuthErr>;
|
fn remove_old_sessions(&self, min_last_seen: DateTime<Utc>) -> Result<usize, AuthErr>;
|
||||||
|
|
||||||
/// Return the given page of users, ordered by username
|
/// Return the given page of users, ordered by username
|
||||||
fn paginated_users(&self, page: Page) -> Result<Vec<User>, AuthErr>;
|
fn paginated_users(&self, page: Page, filter: &UserFilter) -> Result<Vec<User>, AuthErr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait GpodderDeviceStore {
|
pub trait GpodderDeviceStore {
|
||||||
|
|
|
@ -143,16 +143,31 @@ impl gpodder::GpodderAuthStore for SqliteRepository {
|
||||||
.map_err(AuthErr::from)
|
.map_err(AuthErr::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paginated_users(&self, page: gpodder::Page) -> Result<Vec<gpodder::User>, AuthErr> {
|
fn paginated_users(
|
||||||
Ok(users::table
|
&self,
|
||||||
.select(User::as_select())
|
page: gpodder::Page,
|
||||||
.order(users::username.asc())
|
filter: &gpodder::UserFilter,
|
||||||
.offset((page.page * page.per_page) as i64)
|
) -> Result<Vec<gpodder::User>, AuthErr> {
|
||||||
.limit(page.per_page as i64)
|
(|| {
|
||||||
.get_results(&mut self.pool.get().map_err(DbError::from)?)
|
let mut query = users::table
|
||||||
.map_err(DbError::from)?
|
.select(User::as_select())
|
||||||
.into_iter()
|
.order(users::username.asc())
|
||||||
.map(gpodder::User::from)
|
.offset((page.page * page.per_page) as i64)
|
||||||
.collect())
|
.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::<Result<Vec<_>, _>>()?,
|
||||||
|
)
|
||||||
|
})()
|
||||||
|
.map_err(AuthErr::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use axum::{
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
routing::{delete, get},
|
routing::{delete, get},
|
||||||
};
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
server::{
|
server::{
|
||||||
|
@ -22,22 +23,46 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_users(
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct UserFilter {
|
||||||
|
username: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UserFilter> 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<Context>,
|
State(ctx): State<Context>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(session): Extension<gpodder::Session>,
|
Extension(session): Extension<gpodder::Session>,
|
||||||
Query(page): Query<super::Pagination>,
|
Query(page): Query<super::Pagination>,
|
||||||
|
Query(filter): Query<UserFilter>,
|
||||||
) -> AppResult<TemplateResponse<Page<View>>> {
|
) -> AppResult<TemplateResponse<Page<View>>> {
|
||||||
let next_page = page.next_page();
|
let next_page = page.next_page();
|
||||||
|
let filter_clone = filter.clone();
|
||||||
|
|
||||||
let user_id = session.user.id;
|
let user_id = session.user.id;
|
||||||
let users = tokio::task::spawn_blocking(move || {
|
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
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
|
||||||
let next_page_query =
|
let next_page_query = (users.len() == next_page.per_page as usize)
|
||||||
(users.len() == next_page.per_page as usize).then_some(next_page.to_query());
|
.then_some(next_page.to_query().join(filter_clone));
|
||||||
|
|
||||||
Ok(View::Users(users, user_id, next_page_query)
|
Ok(View::Users(users, user_id, next_page_query)
|
||||||
.page(&headers)
|
.page(&headers)
|
||||||
|
|
|
@ -23,6 +23,15 @@ impl Query {
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convenience method for adding possibly empty parameter values from options
|
||||||
|
pub fn opt_parameter(self, key: impl ToString, value: Option<impl ToString>) -> Self {
|
||||||
|
if let Some(value) = value {
|
||||||
|
self.parameter(key, value)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allows objects to be converted into queries
|
/// Allows objects to be converted into queries
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
<h1>Users</h1>
|
<h1>Users</h1>
|
||||||
|
|
||||||
<table>
|
<input
|
||||||
|
type="text" id="username" name="username"
|
||||||
|
hx-get="/users"
|
||||||
|
hx-target="#users > tbody"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-select="table > tbody > tr"
|
||||||
|
hx-trigger="input changed delay:500ms"
|
||||||
|
placeholder="Search..."
|
||||||
|
/>
|
||||||
|
<table id="users">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in users %}
|
{%- for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ user.username }}</th>
|
<th>{{ user.username }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor -%}
|
||||||
|
|
||||||
{%- if next_page_query %}
|
{%- if next_page_query %}
|
||||||
<tr
|
<tr
|
||||||
|
|
Loading…
Reference in New Issue