From 0006b1f037db325be29bd387f4234d5d462bb4b5 Mon Sep 17 00:00:00 2001 From: delgado Date: Sun, 23 Jun 2024 22:25:37 -0400 Subject: [PATCH 1/7] Add form to enter new domains --- src/auth/core.rs | 31 +++++-- src/auth/mod.rs | 2 +- src/auth/page.rs | 72 ++++++++-------- src/domain/core.rs | 4 +- src/domain/mod.rs | 8 +- src/domain/page.rs | 93 +++++++++++++++++++++ src/index.rs | 2 +- src/main.rs | 2 + src/search/mod.rs | 3 +- src/search/page.rs | 43 ++++++++++ templates/auth/component/signin_form.html | 38 ++++----- templates/domain/component/domain_form.html | 53 ++++++++++++ templates/search/page/domain_search.html | 19 +++++ 13 files changed, 303 insertions(+), 67 deletions(-) create mode 100644 src/domain/page.rs create mode 100644 src/search/page.rs create mode 100644 templates/domain/component/domain_form.html create mode 100644 templates/search/page/domain_search.html diff --git a/src/auth/core.rs b/src/auth/core.rs index 149a349..0fc4cdb 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..141acef 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -4,6 +4,6 @@ mod page; pub use crate::auth::{ api::AuthApi, - core::{Auth, TokenAuth, UserCred}, + core::{has_ui_access, Auth, TokenAuth, UserCred}, page::route, }; diff --git a/src/auth/page.rs b/src/auth/page.rs index e8af367..e4fd037 100644 --- a/src/auth/page.rs +++ b/src/auth/page.rs @@ -14,9 +14,9 @@ use serde::Deserialize; /// Tempate for Signin 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 @@ -25,30 +25,33 @@ 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)?; + 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_html).into_response()) + Ok(Html(signin).into_response()) + } } } /// Tempate for Signin form #[derive(Template)] #[template(path = "auth/component/signin_form.html")] -struct SigninForm<'a> { - error: Option<&'a str>, +struct SigninForm { + error: Option, } #[derive(Deserialize)] @@ -69,24 +72,27 @@ async fn signin_form( 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); + 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()) - } else { - // Well, looks like user auth failed - let signin_form_html: String = SigninForm { - error: Some("User authentiation failed"), + // Redirect back to home page + Ok(Response::builder() + .status(StatusCode::FOUND) + .header("HX-Redirect", "/") + .finish()) } - .render() - .map_err(InternalServerError)?; + 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_html).into_response()) + Ok(Html(signin_form).into_response()) + } } } diff --git a/src/domain/core.rs b/src/domain/core.rs index 52bb254..328c0c8 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, diff --git a/src/domain/mod.rs b/src/domain/mod.rs index f68145d..516885c 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -1,6 +1,12 @@ mod api; mod core; mod db; +mod page; mod util; -pub use crate::domain::{api::DomainApi, core::Domain, db::domain_select}; +pub use crate::domain::{ + api::DomainApi, + core::Domain, + db::domain_select, + page::{route, DomainForm}, +}; diff --git a/src/domain/page.rs b/src/domain/page.rs new file mode 100644 index 0000000..ddb7342 --- /dev/null +++ b/src/domain/page.rs @@ -0,0 +1,93 @@ +use crate::{ + auth::has_ui_access, + domain::core::{domain_add, Domain, DomainParam}, +}; +use askama::Template; +use poem::{ + error::InternalServerError, + handler, post, + session::Session, + web::{Data, Form, Html}, + IntoResponse, Request, Response, Route, +}; +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 { + // Pull username from cookies + let username: Option = session.get("username"); + + // Do we have a user signed in? + let user: &String = 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).into_response()); + } + }; + + // 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).into_response()); + } + + // 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).into_response()) +} + +/// Provide routs for the API endpoints +pub fn route() -> Route { + Route::new().at("/", post(domain_form)) +} 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 5290266..74c7fd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,6 +70,8 @@ async fn main() -> Result<(), eyre::Error> { // User friendly locations .at("/", index::route()) .nest("/auth", auth::route()) + .nest("/domain", domain::route()) + .nest("/search", search::route()) // Global context to be shared .data(pool) .data(user_creds) diff --git a/src/search/mod.rs b/src/search/mod.rs index 74c664a..fd2befd 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -1,5 +1,6 @@ mod api; mod core; mod db; +mod page; -pub use crate::search::api::SearchApi; +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..3df67a7 --- /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/search/page/domain_search.html b/templates/search/page/domain_search.html new file mode 100644 index 0000000..af958e6 --- /dev/null +++ b/templates/search/page/domain_search.html @@ -0,0 +1,19 @@ + +{% extends "shared/layout/base.html" %} + +{% block body %} + + +{{ navbar|safe }} + + +
+

Domain Search

+
+ +
+ + {{ domain_add|safe }} +
+ +{% endblock %} From 956a8d8e3023f2ba674861944c29a02881b6f873 Mon Sep 17 00:00:00 2001 From: delgado Date: Wed, 26 Jun 2024 00:52:21 -0400 Subject: [PATCH 2/7] Address more merge conflicts --- src/domain/mod.rs | 2 +- src/main.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 81b1882..eed7108 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -4,4 +4,4 @@ mod db; mod page; mod util; -pub use crate::domain::{api::DomainApi, db::domain_select}; +pub use crate::domain::{api::DomainApi, db::domain_select, page::route}; diff --git a/src/main.rs b/src/main.rs index 000ae3c..35c627f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,7 +70,6 @@ async fn main() -> Result<(), eyre::Error> { .at("/", index::route()) .nest("/auth", auth::route()) .nest("/domain", domain::route()) - .nest("/search", search::route()) // Global context to be shared .data(pool) .data(user_creds) From 96f2b6319698e3eb5670df3a031083e27d6c32b7 Mon Sep 17 00:00:00 2001 From: delgado Date: Wed, 26 Jun 2024 01:05:11 -0400 Subject: [PATCH 3/7] Update routes to address move of search into its base moduels --- src/domain/page.rs | 39 ++++++++++++++++++- templates/domain/component/domain_form.html | 2 +- .../page/domain_search.html | 0 templates/index/page/index.html | 6 +-- templates/shared/component/navbar.html | 6 +-- 5 files changed, 44 insertions(+), 9 deletions(-) rename templates/{search => domain}/page/domain_search.html (100%) diff --git a/src/domain/page.rs b/src/domain/page.rs index ddb7342..002cf10 100644 --- a/src/domain/page.rs +++ b/src/domain/page.rs @@ -1,11 +1,12 @@ use crate::{ auth::has_ui_access, domain::core::{domain_add, Domain, DomainParam}, + index::Navbar, }; use askama::Template; use poem::{ error::InternalServerError, - handler, post, + get, handler, post, session::Session, web::{Data, Form, Html}, IntoResponse, Request, Response, Route, @@ -87,7 +88,41 @@ pub async fn domain_form( Ok(Html(domain_form).into_response()) } +/// Tempate for Domain Search Page +#[derive(Template)] +#[template(path = "domain/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("/", post(domain_form)) + Route::new() + .at("/domain", post(domain_form)) + .at("/search", get(domain_search)) } diff --git a/templates/domain/component/domain_form.html b/templates/domain/component/domain_form.html index 3df67a7..b6b7478 100644 --- a/templates/domain/component/domain_form.html +++ b/templates/domain/component/domain_form.html @@ -42,7 +42,7 @@
- + - +
diff --git a/templates/shared/component/navbar.html b/templates/shared/component/navbar.html index a58f589..964107d 100644 --- a/templates/shared/component/navbar.html +++ b/templates/shared/component/navbar.html @@ -1,9 +1,9 @@