diff --git a/src/auth/component.rs b/src/auth/component.rs new file mode 100644 index 0000000..bea5c7f --- /dev/null +++ b/src/auth/component.rs @@ -0,0 +1,61 @@ +use crate::auth::core::basic_checker; +use askama::Template; +use poem::{ + error::InternalServerError, + handler, + http::StatusCode, + session::Session, + web::{Form, Html}, + IntoResponse, Request, Response, +}; +use poem_openapi::auth::Basic; +use serde::Deserialize; + +/// Template for Signin form +#[derive(Template)] +#[template(path = "auth/component/signin_form.html")] +pub struct SigninForm { + pub error: Option, +} + +#[derive(Deserialize)] +struct SigninParams { + username: String, + password: String, +} + +/// Signin form for the UI +#[handler] +pub async fn signin_form( + Form(params): Form, + session: &Session, + req: &Request, +) -> Result { + let basic = Basic { + username: params.username, + password: params.password, + }; + // Do the creds match what we are expecting? + match basic_checker(req, basic).await { + Some(user) => { + // Save the username if auth is good + session.set("username", user.username); + + // Redirect back to home page + Ok(Response::builder() + .status(StatusCode::FOUND) + .header("HX-Redirect", "/") + .finish()) + } + None => { + // Well, looks like user auth failed + let signin_form: String = SigninForm { + error: Some("User authentiation failed".to_string()), + } + .render() + .map_err(InternalServerError)?; + + Ok(Html(signin_form).into_response()) + } + } +} diff --git a/src/auth/core.rs b/src/auth/core.rs index 2211c9e..9321d9f 100644 --- a/src/auth/core.rs +++ b/src/auth/core.rs @@ -113,18 +113,39 @@ async fn token_checker(req: &Request, api_key: ApiKey) -> Option { // Pull jwt data let token_data = decode::(&api_key.key, decoding_key, &Validation::default()).ok()?; + // Params to valid the user + let allowed_users: &Vec = req.data::>()?; + let username: &str = &token_data.claims.sub; + // Make sure the user in the token is still valid. - let user_creds = req.data::>()?; - user_creds + validate_user(allowed_users, username) +} + +/// Is the username still valid? +fn validate_user(allowed_users: &[UserCred], username: &str) -> Option { + // Make sure the user in the token is still valid. + let found_user: &UserCred = allowed_users .iter() - .find(|user_cred| user_cred.username == token_data.claims.sub)?; + .find(|allowed_user| allowed_user.username == username)?; - // Return User from inside the token + // Return found User Some(User { - username: token_data.claims.sub, + username: found_user.username.to_string(), }) } +/// Is the user allowed access? +pub fn has_ui_access(username: &str, req: &Request) -> bool { + // Params to valid the user + let allowed_users: Option<&Vec> = req.data::>(); + + // Make sure the user in the token is still valid. + match allowed_users { + Some(allowed_users) => validate_user(allowed_users, username).is_some(), + None => false, + } +} + /// Key or Basic Auth #[derive(SecurityScheme)] pub enum TokenOrBasicAuth { diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 7157ab1..b4f5d23 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,9 +1,11 @@ mod api; +mod component; mod core; mod page; +mod route; pub use crate::auth::{ api::AuthApi, - core::{Auth, TokenAuth, UserCred}, - page::route, + core::{has_ui_access, Auth, TokenAuth, UserCred}, + route::route, }; diff --git a/src/auth/page.rs b/src/auth/page.rs index e8af367..1c5baf3 100644 --- a/src/auth/page.rs +++ b/src/auth/page.rs @@ -1,98 +1,53 @@ -use crate::{auth::core::basic_checker, index::Navbar}; +use crate::{auth::component::SigninForm, index::Navbar}; use askama::Template; use poem::{ error::InternalServerError, - get, handler, + handler, http::{header, StatusCode}, session::Session, - web::{Form, Html}, - IntoResponse, Request, Response, Route, + web::Html, + IntoResponse, Response, }; -use poem_openapi::auth::Basic; -use serde::Deserialize; -/// Tempate for Signin page +/// Template for Sign In page #[derive(Template)] #[template(path = "auth/page/signin.html")] -struct Signin<'a> { +struct Signin { navbar: Navbar, - signin_form: SigninForm<'a>, + signin_form: SigninForm, } /// Sign in page #[handler] -fn signin(session: &Session) -> Result { +pub fn signin(session: &Session) -> Result { let username: Option = session.get("username"); // Are we already signed in? - if username.is_some() { - // Redirect back to home page - Ok(Response::builder() - .status(StatusCode::FOUND) - .header(header::LOCATION, "/") - .finish()) - } else { - // Ok, we are not signed in - let signin_html: String = Signin { - navbar: Navbar { username: None }, - signin_form: SigninForm { error: None }, + match username { + Some(_) => { + // Redirect back to home page + Ok(Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/") + .finish()) } - .render() - .map_err(InternalServerError)?; - - Ok(Html(signin_html).into_response()) - } -} - -/// Tempate for Signin form -#[derive(Template)] -#[template(path = "auth/component/signin_form.html")] -struct SigninForm<'a> { - error: Option<&'a str>, -} - -#[derive(Deserialize)] -struct SigninParams { - username: String, - password: String, -} - -/// Signin form for the UI -#[handler] -async fn signin_form( - Form(params): Form, - session: &Session, - req: &Request, -) -> Result { - let basic = Basic { - username: params.username, - password: params.password, - }; - // Do the creds match what we are expecting? - if let Some(user) = basic_checker(req, basic).await { - // Save the username if auth is good - session.set("username", user.username); - - // Redirect back to home page - Ok(Response::builder() - .status(StatusCode::FOUND) - .header("HX-Redirect", "/") - .finish()) - } else { - // Well, looks like user auth failed - let signin_form_html: String = SigninForm { - error: Some("User authentiation failed"), + None => { + // Ok, we are not signed in + let signin: String = Signin { + navbar: Navbar { username: None }, + signin_form: SigninForm { error: None }, + } + .render() + .map_err(InternalServerError)?; + + Ok(Html(signin).into_response()) } - .render() - .map_err(InternalServerError)?; - - Ok(Html(signin_form_html).into_response()) } } /// Logout and purge some cookies #[handler] -fn logout(session: &Session) -> Response { +pub fn logout(session: &Session) -> Response { session.clear(); // Redirect back to home page @@ -101,10 +56,3 @@ fn logout(session: &Session) -> Response { .header(header::LOCATION, "/") .finish() } - -/// Provide routs for the API endpoints -pub fn route() -> Route { - Route::new() - .at("/signin", get(signin).post(signin_form)) - .at("/logout", get(logout)) -} diff --git a/src/auth/route.rs b/src/auth/route.rs new file mode 100644 index 0000000..2984610 --- /dev/null +++ b/src/auth/route.rs @@ -0,0 +1,12 @@ +use crate::auth::{ + component::signin_form, + page::{logout, signin}, +}; +use poem::{get, Route}; + +/// Provide routs for the API endpoints +pub fn route() -> Route { + Route::new() + .at("/signin", get(signin).post(signin_form)) + .at("/logout", get(logout)) +} diff --git a/src/domain/api.rs b/src/domain/api.rs index 0b47893..8baed04 100644 --- a/src/domain/api.rs +++ b/src/domain/api.rs @@ -126,23 +126,27 @@ impl DomainApi { Query(domain_name): Query>, Query(owner): Query>, Query(extra): Query>, + Query(ascending): Query>, Query(page): Query>, ) -> Result, poem::Error> { - // Default no page to 0 - let page = page.unwrap_or(0); + // Defaults + let page: u64 = page.unwrap_or(0); + let ascending: bool = ascending.unwrap_or(true); // Search Params let search_param = SearchDomainParam { domain_name, owner, extra, + ascending, + page, }; // Start Transaction let mut tx = pool.begin().await.map_err(InternalServerError)?; // Pull domain - let search_domain = search_domain_read(&mut tx, &search_param, &page).await?; + let search_domain = search_domain_read(&mut tx, &search_param).await?; Ok(Json(search_domain)) } @@ -151,10 +155,13 @@ impl DomainApi { #[cfg(test)] mod tests { use super::*; - use crate::util::test_utils::{ - gen_jwt_encode_decode_token, gen_test_domain_json, gen_test_model_json, gen_test_pack_json, - gen_test_schema_json, gen_test_user_creds, post_test_domain, post_test_model, - post_test_pack, post_test_schema, + use crate::util::{ + test_utils::{ + gen_jwt_encode_decode_token, gen_test_domain_json, gen_test_model_json, + gen_test_pack_json, gen_test_schema_json, gen_test_user_creds, post_test_domain, + post_test_model, post_test_pack, post_test_schema, + }, + PAGE_SIZE, }; use poem::{ http::StatusCode, @@ -866,7 +873,11 @@ mod tests { let test_json = response.json().await; let json_value = test_json.value(); - json_value.object().get("domains").array().assert_len(50); + json_value + .object() + .get("domains") + .array() + .assert_len(PAGE_SIZE as usize); json_value.object().get("page").assert_i64(0); json_value.object().get("more").assert_bool(true); } diff --git a/src/domain/component.rs b/src/domain/component.rs new file mode 100644 index 0000000..70125e9 --- /dev/null +++ b/src/domain/component.rs @@ -0,0 +1,148 @@ +use crate::{ + auth::has_ui_access, + domain::core::{ + domain_add, search_domain_read, Domain, DomainParam, SearchDomain, SearchDomainParam, + }, +}; +use askama::Template; +use poem::{ + error::InternalServerError, + handler, + session::Session, + web::{Data, Form, Html, Query}, + Request, +}; +use serde::Deserialize; +use sqlx::PgPool; + +/// Tempate to add a Domain +#[derive(Template)] +#[template(path = "domain/component/domain_form.html")] +pub struct DomainForm { + pub error: Option, + pub has_access: bool, +} + +/// Add a Domain via the UI +#[handler] +pub async fn domain_form( + Data(pool): Data<&PgPool>, + Form(params): Form, + session: &Session, + req: &Request, +) -> Result, poem::Error> { + // Pull username from cookies + let username: Option = session.get("username"); + + // Do we have a user signed in? + let user: &str = match &username { + Some(user) => user, + None => { + // Render HTML for the UI + let domain_form: String = DomainForm { + error: Some("User is not signed in".to_string()), + has_access: false, + } + .render() + .map_err(InternalServerError)?; + + return Ok(Html(domain_form)); + } + }; + + // Does the user have access? + let has_access: bool = has_ui_access(user, req); + if !has_access { + // Render HTML for the UI + let domain_form: String = DomainForm { + error: Some("User does not have access".to_string()), + has_access, + } + .render() + .map_err(InternalServerError)?; + + return Ok(Html(domain_form)); + } + + // Start transaction + let mut tx = pool.begin().await.map_err(InternalServerError)?; + + // Write new Domain to the DB + let domain: Result = domain_add(&mut tx, ¶ms, user).await; + + // Did we get any errors writing to the DB? + let error = match domain { + Ok(_) => { + tx.commit().await.map_err(InternalServerError)?; + None + } + Err(err) => { + tx.rollback().await.map_err(InternalServerError)?; + Some(err.to_string()) + } + }; + + // Render HTML for the UI + let domain_form: String = DomainForm { error, has_access } + .render() + .map_err(InternalServerError)?; + + Ok(Html(domain_form)) +} + +/// Tempate for rows od Domains +#[derive(Template)] +#[template(path = "domain/component/domain_rows.html")] +pub struct DomainRows { + pub domains: Vec, + pub params: SearchDomainParam, + pub next_page: Option, +} + +/// Params for searching for domains +#[derive(Deserialize)] +struct SearchDomainUserParam { + domain_name: Option, + owner: Option, + extra: Option, + ascending: Option, + page: Option, +} + +/// Results of searching for a domain +#[handler] +pub async fn domain_rows( + Data(pool): Data<&PgPool>, + Query(params): Query, +) -> Result, poem::Error> { + // Search Params + let params = SearchDomainParam { + domain_name: params.domain_name, + owner: params.owner, + extra: params.extra, + ascending: params.ascending.unwrap_or(true), + page: params.page.unwrap_or(0), + }; + + // Start transaction + let mut tx = pool.begin().await.map_err(InternalServerError)?; + + // Default rows of the domain search + let domain_search: SearchDomain = search_domain_read(&mut tx, ¶ms).await?; + + // For pogination, here are the next page number + let next_page: Option = match &domain_search.more { + true => Some(params.page + 1), + false => None, + }; + + let rows: String = DomainRows { + domains: domain_search.domains, + params, + next_page, + } + .render() + .map_err(InternalServerError)?; + + Ok(Html(rows)) +} diff --git a/src/domain/core.rs b/src/domain/core.rs index ccc784b..1b575ef 100644 --- a/src/domain/core.rs +++ b/src/domain/core.rs @@ -13,7 +13,7 @@ use poem::{ http::StatusCode, }; use poem_openapi::Object; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Postgres, Transaction}; use validator::Validate; @@ -31,7 +31,7 @@ pub struct Domain { } /// How to create a new domain -#[derive(Debug, Object, Serialize, Validate)] +#[derive(Debug, Deserialize, Object, Serialize, Validate)] pub struct DomainParam { #[validate(custom(function = dbx_validater))] pub name: String, @@ -51,9 +51,9 @@ pub struct DomainChildren { /// Domain Search Results #[derive(Object)] pub struct SearchDomain { - domains: Vec, - page: u64, - more: bool, + pub domains: Vec, + pub page: u64, + pub more: bool, } /// Params for searching for domains @@ -61,6 +61,8 @@ pub struct SearchDomainParam { pub domain_name: Option, pub owner: Option, pub extra: Option, + pub ascending: bool, + pub page: u64, } /// Add a domain @@ -160,20 +162,19 @@ pub async fn domain_read_with_children( /// Read details of many domains pub async fn search_domain_read( tx: &mut Transaction<'_, Postgres>, - search_param: &SearchDomainParam, - page: &u64, + params: &SearchDomainParam, ) -> Result { // Compute offset - let offset = page * PAGE_SIZE; - let next_offset = (page + 1) * PAGE_SIZE; + let offset = params.page * PAGE_SIZE; + let next_offset = (params.page + 1) * PAGE_SIZE; // Pull the Domains - let domains = search_domain_select(tx, search_param, &Some(PAGE_SIZE), &Some(offset)) + let domains = search_domain_select(tx, params, &PAGE_SIZE, &offset) .await .map_err(InternalServerError)?; // More domains present? - let next_domain = search_domain_select(tx, search_param, &Some(PAGE_SIZE), &Some(next_offset)) + let next_domain = search_domain_select(tx, params, &1, &next_offset) .await .map_err(InternalServerError)?; @@ -181,7 +182,7 @@ pub async fn search_domain_read( Ok(SearchDomain { domains, - page: *page, + page: params.page, more, }) } @@ -595,13 +596,13 @@ mod tests { domain_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let search = search_domain_read(&mut tx, &search_param, &0) - .await - .unwrap(); + let search = search_domain_read(&mut tx, &search_param).await.unwrap(); - assert_eq!(search.domains.len(), 50); + assert_eq!(search.domains.len(), PAGE_SIZE as usize); assert_eq!(search.page, 0); assert_eq!(search.more, true); } @@ -613,11 +614,11 @@ mod tests { domain_name: None, owner: None, extra: None, + ascending: true, + page: 1, }; - let search = search_domain_read(&mut tx, &search_param, &1) - .await - .unwrap(); + let search = search_domain_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.domains.len(), 1); assert_eq!(search.page, 1); @@ -631,11 +632,11 @@ mod tests { domain_name: Some("abcdef".to_string()), owner: None, extra: None, + ascending: true, + page: 0, }; - let search = search_domain_read(&mut tx, &search_param, &0) - .await - .unwrap(); + let search = search_domain_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.domains.len(), 0); assert_eq!(search.page, 0); @@ -649,11 +650,11 @@ mod tests { domain_name: Some("foobar".to_string()), owner: None, extra: None, + ascending: true, + page: 0, }; - let search = search_domain_read(&mut tx, &search_param, &0) - .await - .unwrap(); + let search = search_domain_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.domains.len(), 1); assert_eq!(search.domains[0].name, "foobar_domain"); @@ -666,11 +667,11 @@ mod tests { domain_name: Some("foobar".to_string()), owner: Some("test.com".to_string()), extra: None, + ascending: true, + page: 0, }; - let search = search_domain_read(&mut tx, &search_param, &0) - .await - .unwrap(); + let search = search_domain_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.domains.len(), 1); assert_eq!(search.domains[0].name, "foobar_domain"); @@ -685,11 +686,11 @@ mod tests { domain_name: Some("foobar".to_string()), owner: Some("test.com".to_string()), extra: Some("abc".to_string()), + ascending: true, + page: 0, }; - let search = search_domain_read(&mut tx, &search_param, &0) - .await - .unwrap(); + let search = search_domain_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.domains.len(), 1); assert_eq!(search.domains[0].name, "foobar_domain"); diff --git a/src/domain/db.rs b/src/domain/db.rs index 1ffbac3..3236830 100644 --- a/src/domain/db.rs +++ b/src/domain/db.rs @@ -230,9 +230,9 @@ pub async fn pack_select_by_domain( /// Pull multiple domains that match by criteria pub async fn search_domain_select( tx: &mut Transaction<'_, Postgres>, - search_param: &SearchDomainParam, - limit: &Option, - offset: &Option, + params: &SearchDomainParam, + limit: &u64, + offset: &u64, ) -> Result, sqlx::Error> { // Query we will be modifying let mut query = QueryBuilder::<'_, Postgres>::new( @@ -250,39 +250,32 @@ pub async fn search_domain_select( ); // Should we add a WHERE statement? - if search_param.domain_name.is_some() - || search_param.owner.is_some() - || search_param.extra.is_some() - { + if params.domain_name.is_some() || params.owner.is_some() || params.extra.is_some() { query.push(" WHERE "); // Start building the WHERE statement with the "AND" separating the condition. let mut separated = query.separated(" AND "); // Fuzzy search - if let Some(domain_name) = &search_param.domain_name { + if let Some(domain_name) = ¶ms.domain_name { separated.push(format!("name ILIKE '%{domain_name}%'")); } - if let Some(owner) = &search_param.owner { + if let Some(owner) = ¶ms.owner { separated.push(format!("owner ILIKE '%{owner}%'")); } - if let Some(extra) = &search_param.extra { + if let Some(extra) = ¶ms.extra { separated.push(format!("extra::text ILIKE '%{extra}%'")); } } // Add ORDER BY - query.push(" ORDER BY id "); - - // Add LIMIT - if let Some(limit) = limit { - query.push(format!(" LIMIT {limit} ")); + match ¶ms.ascending { + true => query.push(" ORDER BY id "), + false => query.push(" ORDER BY id DESC"), + }; - // Add OFFSET - if let Some(offset) = offset { - query.push(format!(" OFFSET {offset} ")); - } - } + // Add LIMIT and OFFSET + query.push(format!(" LIMIT {limit} OFFSET {offset} ")); // Run our generated SQL statement let domain = query @@ -298,9 +291,12 @@ mod tests { use super::*; use crate::{ domain::util::test_utils::gen_test_domain_param, - util::test_utils::{ - gen_test_domain_json, gen_test_model_json, gen_test_schema_json, post_test_domain, - post_test_model, post_test_schema, + util::{ + test_utils::{ + gen_test_domain_json, gen_test_model_json, gen_test_schema_json, post_test_domain, + post_test_model, post_test_schema, + }, + PAGE_SIZE, }, }; use pretty_assertions::assert_eq; @@ -634,9 +630,11 @@ mod tests { domain_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let domains = search_domain_select(&mut tx, &search_param, &None, &None) + let domains = search_domain_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -650,9 +648,11 @@ mod tests { domain_name: Some("abcdef".to_string()), owner: None, extra: None, + ascending: true, + page: 0, }; - let domains = search_domain_select(&mut tx, &search_param, &None, &None) + let domains = search_domain_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -666,9 +666,11 @@ mod tests { domain_name: Some("test_domain".to_string()), owner: None, extra: None, + ascending: true, + page: 0, }; - let domains = search_domain_select(&mut tx, &search_param, &None, &None) + let domains = search_domain_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -683,9 +685,11 @@ mod tests { domain_name: Some("test_domain".to_string()), owner: Some("test.com".to_string()), extra: None, + ascending: true, + page: 0, }; - let domains = search_domain_select(&mut tx, &search_param, &None, &None) + let domains = search_domain_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -700,9 +704,11 @@ mod tests { domain_name: Some("test_domain".to_string()), owner: Some("test.com".to_string()), extra: Some("abc".to_string()), + ascending: true, + page: 0, }; - let domains = search_domain_select(&mut tx, &search_param, &None, &None) + let domains = search_domain_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -717,9 +723,11 @@ mod tests { domain_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let domains = search_domain_select(&mut tx, &search_param, &Some(1), &None) + let domains = search_domain_select(&mut tx, &search_param, &1, &0) .await .unwrap(); @@ -734,9 +742,11 @@ mod tests { domain_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let domains = search_domain_select(&mut tx, &search_param, &Some(1), &Some(1)) + let domains = search_domain_select(&mut tx, &search_param, &1, &1) .await .unwrap(); diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 6b06224..446d2f5 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -1,6 +1,9 @@ mod api; +mod component; mod core; mod db; +mod page; +mod route; mod util; -pub use crate::domain::{api::DomainApi, db::domain_select}; +pub use crate::domain::{api::DomainApi, db::domain_select, route::route}; diff --git a/src/domain/page.rs b/src/domain/page.rs new file mode 100644 index 0000000..8863eb4 --- /dev/null +++ b/src/domain/page.rs @@ -0,0 +1,91 @@ +use crate::{ + auth::has_ui_access, + domain::{ + component::{DomainForm, DomainRows}, + core::{search_domain_read, SearchDomain, SearchDomainParam}, + }, + index::Navbar, +}; +use askama::Template; +use poem::{ + error::InternalServerError, + handler, + session::Session, + web::{Data, Html}, + Request, +}; +use sqlx::PgPool; + +/// Template for Domain Search Page +#[derive(Template)] +#[template(path = "domain/page/domain_search.html")] +struct DomainSearch { + navbar: Navbar, + domain_add: DomainForm, + rows: DomainRows, +} + +/// Sign in page +#[handler] +pub async fn domain_search( + Data(pool): Data<&PgPool>, + session: &Session, + req: &Request, +) -> Result, poem::Error> { + // If we have the username from the cookies, do they have access? + let username: Option = session.get("username"); + let has_access: bool = match &username { + Some(username) => has_ui_access(username, req), + None => false, + }; + + // Start transaction + let mut tx = pool.begin().await.map_err(InternalServerError)?; + + // Defaults + let page: u64 = 0; + let ascending: bool = true; + + // Default rows of the domain search + let domain_search: SearchDomain = search_domain_read( + &mut tx, + &SearchDomainParam { + domain_name: None, + owner: None, + extra: None, + ascending, + page, + }, + ) + .await?; + + // For pagination, here are the next page number + let next_page: Option = match &domain_search.more { + true => Some(page + 1), + false => None, + }; + + // Render HTML + let domain_search: String = DomainSearch { + navbar: Navbar { username }, + domain_add: DomainForm { + error: None, + has_access, + }, + rows: DomainRows { + domains: domain_search.domains, + params: SearchDomainParam { + domain_name: None, + owner: None, + extra: None, + ascending, + page, + }, + next_page, + }, + } + .render() + .map_err(InternalServerError)?; + + Ok(Html(domain_search)) +} diff --git a/src/domain/route.rs b/src/domain/route.rs new file mode 100644 index 0000000..7817f33 --- /dev/null +++ b/src/domain/route.rs @@ -0,0 +1,13 @@ +use crate::domain::{ + component::{domain_form, domain_rows}, + page::domain_search, +}; +use poem::{get, post, Route}; + +/// Provide routs for the domain pages and components +pub fn route() -> Route { + Route::new() + .at("/domain", post(domain_form)) + .at("/search", get(domain_search)) + .at("/search/rows", get(domain_rows)) +} diff --git a/src/index.rs b/src/index.rs index 33b49fb..653c6c5 100644 --- a/src/index.rs +++ b/src/index.rs @@ -24,7 +24,7 @@ async fn index(session: &Session) -> Result, poem::Error> { let username: Option = session.get("username"); // Render landing page - let index = Index { + let index: String = Index { navbar: Navbar { username }, } .render() diff --git a/src/main.rs b/src/main.rs index 2979aa3..9aeb966 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,6 +70,7 @@ async fn main() -> Result<(), eyre::Error> { // User friendly locations .at("/", index::route()) .nest("/auth", auth::route()) + .nest("/domain", domain::route()) // Global context to be shared .data(pool) .data(user_creds) diff --git a/src/model/api.rs b/src/model/api.rs index 36600ac..d92e59f 100644 --- a/src/model/api.rs +++ b/src/model/api.rs @@ -129,10 +129,12 @@ impl ModelApi { Query(schema_name): Query>, Query(owner): Query>, Query(extra): Query>, + Query(ascending): Query>, Query(page): Query>, ) -> Result, poem::Error> { - // Default no page to 0 - let page = page.unwrap_or(0); + // Defaults + let page: u64 = page.unwrap_or(0); + let ascending: bool = ascending.unwrap_or(true); // Search Params let search_param = SearchModelParam { @@ -141,13 +143,15 @@ impl ModelApi { schema_name, owner, extra, + ascending, + page, }; // Start Transaction let mut tx = pool.begin().await.map_err(InternalServerError)?; // Pull models - let search_model = search_model_read(&mut tx, &search_param, &page).await?; + let search_model = search_model_read(&mut tx, &search_param).await?; Ok(Json(search_model)) } diff --git a/src/model/core.rs b/src/model/core.rs index 390daca..a1cb901 100644 --- a/src/model/core.rs +++ b/src/model/core.rs @@ -66,6 +66,8 @@ pub struct SearchModelParam { pub schema_name: Option, pub owner: Option, pub extra: Option, + pub ascending: bool, + pub page: u64, } /// Add a model @@ -161,30 +163,26 @@ pub async fn model_read_with_children( /// Read details of many models pub async fn search_model_read( tx: &mut Transaction<'_, Postgres>, - search_param: &SearchModelParam, - page: &u64, + params: &SearchModelParam, ) -> Result { + let page: u64 = params.page; // Compute offset let offset = page * PAGE_SIZE; let next_offset = (page + 1) * PAGE_SIZE; // Pull the Models - let models = search_model_select(tx, search_param, &Some(PAGE_SIZE), &Some(offset)) + let models = search_model_select(tx, params, &PAGE_SIZE, &offset) .await .map_err(InternalServerError)?; // More models present? - let next_model = search_model_select(tx, search_param, &Some(PAGE_SIZE), &Some(next_offset)) + let next_model = search_model_select(tx, params, &PAGE_SIZE, &next_offset) .await .map_err(InternalServerError)?; let more = !next_model.is_empty(); - Ok(SearchModel { - models, - page: *page, - more, - }) + Ok(SearchModel { models, page, more }) } #[cfg(test)] @@ -561,9 +559,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let search = search_model_read(&mut tx, &search_param, &0).await.unwrap(); + let search = search_model_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.models.len(), 50); assert_eq!(search.page, 0); @@ -579,9 +579,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 1, }; - let search = search_model_read(&mut tx, &search_param, &1).await.unwrap(); + let search = search_model_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.models.len(), 1); assert_eq!(search.page, 1); @@ -597,9 +599,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let search = search_model_read(&mut tx, &search_param, &0).await.unwrap(); + let search = search_model_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.models.len(), 50); assert_eq!(search.page, 0); @@ -615,9 +619,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let search = search_model_read(&mut tx, &search_param, &0).await.unwrap(); + let search = search_model_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.models.len(), 0); assert_eq!(search.page, 0); @@ -633,9 +639,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let search = search_model_read(&mut tx, &search_param, &0).await.unwrap(); + let search = search_model_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.models.len(), 1); assert_eq!(search.models[0].name, "foobar_model"); @@ -650,9 +658,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let search = search_model_read(&mut tx, &search_param, &0).await.unwrap(); + let search = search_model_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.models.len(), 50); assert_eq!(search.page, 0); @@ -668,9 +678,11 @@ mod tests { schema_name: None, owner: Some("test.com".to_string()), extra: None, + ascending: true, + page: 0, }; - let search = search_model_read(&mut tx, &search_param, &0).await.unwrap(); + let search = search_model_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.models.len(), 1); assert_eq!(search.models[0].name, "foobar_model"); @@ -687,9 +699,11 @@ mod tests { schema_name: None, owner: Some("test.com".to_string()), extra: Some("abc".to_string()), + ascending: true, + page: 0, }; - let search = search_model_read(&mut tx, &search_param, &0).await.unwrap(); + let search = search_model_read(&mut tx, &search_param).await.unwrap(); assert_eq!(search.models.len(), 1); assert_eq!(search.models[0].name, "foobar_model"); diff --git a/src/model/db.rs b/src/model/db.rs index e5d525b..ba481d2 100644 --- a/src/model/db.rs +++ b/src/model/db.rs @@ -169,9 +169,9 @@ pub async fn model_drop( /// Pull multiple models that match the criteria pub async fn search_model_select( tx: &mut Transaction<'_, Postgres>, - search_param: &SearchModelParam, - limit: &Option, - offset: &Option, + params: &SearchModelParam, + limit: &u64, + offset: &u64, ) -> Result, sqlx::Error> { // Query we will be modifying let mut query = QueryBuilder::<'_, Postgres>::new( @@ -201,11 +201,11 @@ pub async fn search_model_select( ); // Should we add a WHERE statement? - if search_param.model_name.is_some() - || search_param.domain_name.is_some() - || search_param.schema_name.is_some() - || search_param.owner.is_some() - || search_param.extra.is_some() + if params.model_name.is_some() + || params.domain_name.is_some() + || params.schema_name.is_some() + || params.owner.is_some() + || params.extra.is_some() { query.push(" WHERE "); @@ -213,35 +213,31 @@ pub async fn search_model_select( let mut separated = query.separated(" AND "); // Fuzzy search - if let Some(model_name) = &search_param.model_name { + if let Some(model_name) = ¶ms.model_name { separated.push(format!("model.name ILIKE '%{model_name}%'")); } - if let Some(domain_name) = &search_param.domain_name { + if let Some(domain_name) = ¶ms.domain_name { separated.push(format!("domain.name ILIKE '%{domain_name}%'")); } - if let Some(schema_name) = &search_param.schema_name { + if let Some(schema_name) = ¶ms.schema_name { separated.push(format!("schema.name ILIKE '%{schema_name}%'")); } - if let Some(owner) = &search_param.owner { + if let Some(owner) = ¶ms.owner { separated.push(format!("model.owner ILIKE '%{owner}%'")); } - if let Some(extra) = &search_param.extra { + if let Some(extra) = ¶ms.extra { separated.push(format!("model.extra::text ILIKE '%{extra}%'")); } } // Add ORDER BY - query.push(" ORDER BY model.id "); - - // Add LIMIT - if let Some(limit) = limit { - query.push(format!(" LIMIT {limit} ")); + match ¶ms.ascending { + true => query.push(" ORDER BY id "), + false => query.push(" ORDER BY id DESC"), + }; - // Add OFFSET - if let Some(offset) = offset { - query.push(format!(" OFFSET {offset} ")); - } - } + // Add LIMIT and OFFSET + query.push(format!(" LIMIT {limit} OFFSET {offset} ")); // Run our generated SQL statement let model = query.build_query_as::().fetch_all(&mut **tx).await?; @@ -254,9 +250,12 @@ mod tests { use super::*; use crate::{ model::util::test_utils::gen_test_model_param, - util::test_utils::{ - gen_test_domain_json, gen_test_model_json, gen_test_schema_json, post_test_domain, - post_test_model, post_test_schema, + util::{ + test_utils::{ + gen_test_domain_json, gen_test_model_json, gen_test_schema_json, post_test_domain, + post_test_model, post_test_schema, + }, + PAGE_SIZE, }, }; use pretty_assertions::assert_eq; @@ -627,9 +626,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &None, &None) + let models = search_model_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -648,9 +649,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &None, &None) + let models = search_model_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -666,9 +669,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &None, &None) + let models = search_model_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -687,9 +692,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &None, &None) + let models = search_model_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -706,9 +713,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &None, &None) + let models = search_model_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -726,9 +735,11 @@ mod tests { schema_name: None, owner: Some("test_model%@test.com".to_string()), extra: None, + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &None, &None) + let models = search_model_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -746,9 +757,11 @@ mod tests { schema_name: None, owner: None, extra: Some("abc".to_string()), + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &None, &None) + let models = search_model_select(&mut tx, &search_param, &PAGE_SIZE, &0) .await .unwrap(); @@ -767,9 +780,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &Some(1), &None) + let models = search_model_select(&mut tx, &search_param, &1, &0) .await .unwrap(); @@ -786,9 +801,11 @@ mod tests { schema_name: None, owner: None, extra: None, + ascending: true, + page: 0, }; - let models = search_model_select(&mut tx, &search_param, &Some(1), &Some(1)) + let models = search_model_select(&mut tx, &search_param, &1, &1) .await .unwrap(); diff --git a/src/pack/api.rs b/src/pack/api.rs index a5d7103..723e4c4 100644 --- a/src/pack/api.rs +++ b/src/pack/api.rs @@ -131,10 +131,12 @@ impl PackApi { Query(repo): Query>, Query(owner): Query>, Query(extra): Query>, + Query(ascending): Query>, Query(page): Query>, ) -> Result, poem::Error> { - // Default no page to 0 - let page = page.unwrap_or(0); + // Defaults + let page: u64 = page.unwrap_or(0); + let ascending: bool = ascending.unwrap_or(true); // Search Params let search_param = SearchPackParam { @@ -145,13 +147,15 @@ impl PackApi { repo, owner, extra, + ascending, + page, }; // Start Transaction let mut tx = pool.begin().await.map_err(InternalServerError)?; // Pull packs - let search_pack = search_pack_read(&mut tx, &search_param, &page).await?; + let search_pack = search_pack_read(&mut tx, &search_param).await?; Ok(Json(search_pack)) } diff --git a/src/pack/core.rs b/src/pack/core.rs index ba60f9d..12681eb 100644 --- a/src/pack/core.rs +++ b/src/pack/core.rs @@ -114,6 +114,8 @@ pub struct SearchPackParam { pub repo: Option, pub owner: Option, pub extra: Option, + pub ascending: bool, + pub page: u64, } /// Add a pack @@ -213,30 +215,27 @@ pub async fn pack_read_with_children( /// Read details of many packs pub async fn search_pack_read( tx: &mut Transaction<'_, Postgres>, - search_param: &SearchPackParam, - page: &u64, + params: &SearchPackParam, ) -> Result { + let page: u64 = params.page; + // Compute offset let offset = page * PAGE_SIZE; let next_offset = (page + 1) * PAGE_SIZE; // Pull the Pack - let packs = search_pack_select(tx, search_param, &Some(PAGE_SIZE), &Some(offset)) + let packs = search_pack_select(tx, params, &PAGE_SIZE, &offset) .await .map_err(InternalServerError)?; // More packs present? - let next_pack = search_pack_select(tx, search_param, &Some(PAGE_SIZE), &Some(next_offset)) + let next_pack = search_pack_select(tx, params, &PAGE_SIZE, &next_offset) .await .map_err(InternalServerError)?; let more = !next_pack.is_empty(); - Ok(SearchPack { - packs, - page: *page, - more, - }) + Ok(SearchPack { packs, page, more }) } #[cfg(test)] diff --git a/src/pack/db.rs b/src/pack/db.rs index 048dd56..bf9d718 100644 --- a/src/pack/db.rs +++ b/src/pack/db.rs @@ -173,9 +173,9 @@ pub async fn pack_drop( /// Pull multiple packs that match the criteria pub async fn search_pack_select( tx: &mut Transaction<'_, Postgres>, - search_param: &SearchPackParam, - limit: &Option, - offset: &Option, + params: &SearchPackParam, + limit: &u64, + offset: &u64, ) -> Result, sqlx::Error> { // Query we will be modifying let mut query = QueryBuilder::<'_, Postgres>::new( @@ -202,13 +202,13 @@ pub async fn search_pack_select( ); // Should we add a WHERE statement? - if search_param.pack_name.is_some() - || search_param.domain_name.is_some() - || search_param.runtime.is_some() - || search_param.compute.is_some() - || search_param.repo.is_some() - || search_param.owner.is_some() - || search_param.extra.is_some() + if params.pack_name.is_some() + || params.domain_name.is_some() + || params.runtime.is_some() + || params.compute.is_some() + || params.repo.is_some() + || params.owner.is_some() + || params.extra.is_some() { query.push(" WHERE "); @@ -216,41 +216,37 @@ pub async fn search_pack_select( let mut separated = query.separated(" AND "); // Fuzzy search - if let Some(pack_name) = &search_param.pack_name { + if let Some(pack_name) = ¶ms.pack_name { separated.push(format!("pack.name ILIKE '%{pack_name}%'")); } - if let Some(domain_name) = &search_param.domain_name { + if let Some(domain_name) = ¶ms.domain_name { separated.push(format!("domain.name ILIKE '%{domain_name}%'")); } - if let Some(runtime) = search_param.runtime { + if let Some(runtime) = params.runtime { separated.push(format!("pack.runtime = '{runtime}'")); } - if let Some(compute) = search_param.compute { + if let Some(compute) = params.compute { separated.push(format!("pack.compute = '{compute}'")); } - if let Some(repo) = &search_param.repo { + if let Some(repo) = ¶ms.repo { separated.push(format!("pack.repo ILIKE '%{repo}%'")); } - if let Some(owner) = &search_param.owner { + if let Some(owner) = ¶ms.owner { separated.push(format!("pack.owner ILIKE '%{owner}%'")); } - if let Some(extra) = &search_param.extra { + if let Some(extra) = ¶ms.extra { separated.push(format!("pack.extra::text ILIKE '%{extra}%'")); } } // Add ORDER BY - query.push(" ORDER BY pack.id "); + match ¶ms.ascending { + true => query.push(" ORDER BY id "), + false => query.push(" ORDER BY id DESC"), + }; - // Add LIMIT - if let Some(limit) = limit { - query.push(format!(" LIMIT {limit} ")); - - // Add OFFSET - if let Some(offset) = offset { - query.push(format!(" OFFSET {offset} ")); - } - } + // Add LIMIT and OFFSET + query.push(format!(" LIMIT {limit} OFFSET {offset} ")); // Run our generated SQL statement let pack = query.build_query_as::().fetch_all(&mut **tx).await?; diff --git a/src/search/mod.rs b/src/search/mod.rs new file mode 100644 index 0000000..fd2befd --- /dev/null +++ b/src/search/mod.rs @@ -0,0 +1,6 @@ +mod api; +mod core; +mod db; +mod page; + +pub use crate::search::{api::SearchApi, page::route}; diff --git a/src/search/page.rs b/src/search/page.rs new file mode 100644 index 0000000..c5b5473 --- /dev/null +++ b/src/search/page.rs @@ -0,0 +1,43 @@ +use crate::{auth::has_ui_access, domain::DomainForm, index::Navbar}; +use askama::Template; +use poem::{ + error::InternalServerError, get, handler, session::Session, web::Html, IntoResponse, Request, + Response, Route, +}; + +/// Tempate for Domain Search Page +#[derive(Template)] +#[template(path = "search/page/domain_search.html")] +struct DomainSearch { + navbar: Navbar, + domain_add: DomainForm, +} + +/// Sign in page +#[handler] +fn domain_search(session: &Session, req: &Request) -> Result { + // If we have the username from the cookies, do they have access? + let username: Option = session.get("username"); + let has_access: bool = match &username { + Some(username) => has_ui_access(username, req), + None => false, + }; + + // Render HTML + let domain_search: String = DomainSearch { + navbar: Navbar { username }, + domain_add: DomainForm { + error: None, + has_access, + }, + } + .render() + .map_err(InternalServerError)?; + + Ok(Html(domain_search).into_response()) +} + +/// Provide routs for the API endpoints +pub fn route() -> Route { + Route::new().at("/domain", get(domain_search)) +} diff --git a/templates/auth/component/signin_form.html b/templates/auth/component/signin_form.html index 20a4a14..05ba414 100644 --- a/templates/auth/component/signin_form.html +++ b/templates/auth/component/signin_form.html @@ -1,33 +1,25 @@ -
+
+ Login + {% if let Some(error) = error %} -

Login failed: {{ error }}

+ Login failed: {{ error }} {% endif %}
-
- -
-
- -
+ +
+ +
-
+ diff --git a/templates/domain/component/domain_form.html b/templates/domain/component/domain_form.html new file mode 100644 index 0000000..b6b7478 --- /dev/null +++ b/templates/domain/component/domain_form.html @@ -0,0 +1,53 @@ +
+ Add Domain + {% if let Some(error) = error %} + + Adding Domain Failed: {{ error }} + {% endif %} + + +
+ +
+ +
+ +
+ + +
+
diff --git a/templates/domain/component/domain_rows.html b/templates/domain/component/domain_rows.html new file mode 100644 index 0000000..60c707f --- /dev/null +++ b/templates/domain/component/domain_rows.html @@ -0,0 +1,30 @@ +{% for domain in domains %} + + {{ domain.id }} + {{ domain.name }} + {{ domain.owner }} + {{ domain.extra }} + +{% endfor %} + + +{% if let Some(next_page) = next_page %} + + +{% endif %} diff --git a/templates/domain/page/domain_search.html b/templates/domain/page/domain_search.html new file mode 100644 index 0000000..e0b9e75 --- /dev/null +++ b/templates/domain/page/domain_search.html @@ -0,0 +1,109 @@ + +{% extends "shared/layout/base.html" %} + +{% block body %} + + +{{ navbar|safe }} + + +
+

Domain Search

+
+ +
+ + {{ domain_add|safe }} + + +
+ Domain Filters + + + + +
+ + + + +
+ + + + +
+ + + +
+ + + + + + + + + + + + + {{ rows|safe }} + +
IDDomain NameOwnerExtra
+
+ +{% endblock %} diff --git a/templates/index/page/index.html b/templates/index/page/index.html index ee8822d..56e2c2d 100644 --- a/templates/index/page/index.html +++ b/templates/index/page/index.html @@ -15,13 +15,13 @@

a Catalog that is trying to suck a little less

How would you like to search the Catalog?
- + - + - +
diff --git a/templates/shared/component/navbar.html b/templates/shared/component/navbar.html index a58f589..7853247 100644 --- a/templates/shared/component/navbar.html +++ b/templates/shared/component/navbar.html @@ -1,15 +1,17 @@