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,
 | 
				
			||||||
 | 
					        page: gpodder::Page,
 | 
				
			||||||
 | 
					        filter: &gpodder::UserFilter,
 | 
				
			||||||
 | 
					    ) -> Result<Vec<gpodder::User>, AuthErr> {
 | 
				
			||||||
 | 
					        (|| {
 | 
				
			||||||
 | 
					            let mut query = users::table
 | 
				
			||||||
                .select(User::as_select())
 | 
					                .select(User::as_select())
 | 
				
			||||||
                .order(users::username.asc())
 | 
					                .order(users::username.asc())
 | 
				
			||||||
                .offset((page.page * page.per_page) as i64)
 | 
					                .offset((page.page * page.per_page) as i64)
 | 
				
			||||||
                .limit(page.per_page as i64)
 | 
					                .limit(page.per_page as i64)
 | 
				
			||||||
            .get_results(&mut self.pool.get().map_err(DbError::from)?)
 | 
					                .into_boxed();
 | 
				
			||||||
            .map_err(DbError::from)?
 | 
					
 | 
				
			||||||
            .into_iter()
 | 
					            if let Some(username) = &filter.username {
 | 
				
			||||||
            .map(gpodder::User::from)
 | 
					                // Case insensitive by default for SQLite
 | 
				
			||||||
            .collect())
 | 
					                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