From 7343c77a04ab7c4495ce4e8a5abacba30b8bc9d9 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 22 Dec 2022 10:49:50 -0500 Subject: [PATCH] User user_data response to show the user id in the header --- api/src/lib.rs | 4 +-- kitchen/Cargo.toml | 2 +- kitchen/src/web/auth.rs | 2 +- kitchen/src/web/mod.rs | 2 +- web/src/api.rs | 51 ++++++++++++++++++++++++++++++++++++ web/src/app_state.rs | 3 +++ web/src/components/header.rs | 13 ++++++++- web/src/pages/login.rs | 41 ++++------------------------- 8 files changed, 76 insertions(+), 42 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index e02715a..294ae2b 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; use recipes::{IngredientKey, RecipeEntry}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub enum Response { Success(T), Err { status: u16, message: String }, @@ -103,7 +103,7 @@ pub type CategoryResponse = Response; pub type EmptyResponse = Response<()>; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct UserData { pub user_id: String, } diff --git a/kitchen/Cargo.toml b/kitchen/Cargo.toml index 501ef19..1fa0a61 100644 --- a/kitchen/Cargo.toml +++ b/kitchen/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" tracing = "0.1.35" tracing-subscriber = "0.3.14" recipes = { path = "../recipes" } -api = { path = "../api" } +client-api = { path = "../api", features = ["server"], package = "api" } csv = "1.1.1" rust-embed="6.4.0" mime_guess = "2.0.4" diff --git a/kitchen/src/web/auth.rs b/kitchen/src/web/auth.rs index cc67c03..aa29f66 100644 --- a/kitchen/src/web/auth.rs +++ b/kitchen/src/web/auth.rs @@ -14,13 +14,13 @@ use std::str::FromStr; use std::sync::Arc; -use api; use async_session::{Session, SessionStore}; use axum::{ extract::Extension, http::{header, HeaderMap, StatusCode}, }; use axum_auth::AuthBasic; +use client_api as api; use cookie::{Cookie, SameSite}; use secrecy::Secret; use tracing::{debug, error, info, instrument}; diff --git a/kitchen/src/web/mod.rs b/kitchen/src/web/mod.rs index 69a5c24..a55e3e2 100644 --- a/kitchen/src/web/mod.rs +++ b/kitchen/src/web/mod.rs @@ -30,7 +30,7 @@ use tower::ServiceBuilder; use tower_http::trace::TraceLayer; use tracing::{debug, info, instrument}; -use api; +use client_api as api; use storage::{APIStore, AuthStore}; mod auth; diff --git a/web/src/api.rs b/web/src/api.rs index 753da77..754970d 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::collections::{BTreeMap, BTreeSet}; +use base64; use reqwasm; use serde_json::{from_str, to_string}; use sycamore::prelude::*; @@ -79,6 +80,16 @@ pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Res } } } + info!("Checking for user_data in local storage"); + let storage = js_lib::get_storage(); + let user_data = storage + .get("user_data") + .expect("Couldn't read from storage"); + if let Some(data) = user_data { + if let Ok(user_data) = from_str(&data) { + state.auth.set(user_data) + } + } info!("Synchronizing categories"); match store.get_categories().await { Ok(Some(categories_content)) => { @@ -164,6 +175,10 @@ fn recipe_key(id: S) -> String { format!("recipe:{}", id) } +fn token68(user: String, pass: String) -> String { + base64::encode(format!("{}:{}", user, pass)) +} + #[derive(Clone, Debug)] pub struct HttpStore { root: String, @@ -194,6 +209,42 @@ impl HttpStore { use_context::>(cx).clone() } + // NOTE(jwall): We do **not** want to record the password in our logs. + #[instrument(skip_all, fields(?self, user))] + pub async fn authenticate(&self, user: String, pass: String) -> Option { + debug!("attempting login request against api."); + let mut path = self.v1_path(); + path.push_str("/auth"); + let storage = js_lib::get_storage(); + let result = reqwasm::http::Request::get(&path) + .header( + "Authorization", + format!("Basic {}", token68(user, pass)).as_str(), + ) + .send() + .await; + if let Ok(resp) = &result { + if resp.status() == 200 { + let user_data = resp + .json::() + .await + .expect("Unparseable authentication response") + .as_success(); + storage + .set( + "user_data", + &to_string(&user_data).expect("Unable to serialize user_data"), + ) + .unwrap(); + return user_data; + } + error!(status = resp.status(), "Login was unsuccessful") + } else { + error!(err=?result.unwrap_err(), "Failed to send auth request"); + } + return None; + } + //#[instrument] pub async fn get_categories(&self) -> Result, Error> { let mut path = self.v1_path(); diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 46093dc..7d77fe5 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -16,6 +16,7 @@ use std::collections::{BTreeMap, BTreeSet}; use sycamore::prelude::*; use tracing::{debug, instrument, warn}; +use client_api::UserData; use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe}; #[derive(Debug)] @@ -27,6 +28,7 @@ pub struct State { pub category_map: RcSignal>, pub filtered_ingredients: RcSignal>, pub modified_amts: RcSignal>>, + pub auth: RcSignal>, } impl State { @@ -39,6 +41,7 @@ impl State { category_map: create_rc_signal(BTreeMap::new()), filtered_ingredients: create_rc_signal(BTreeSet::new()), modified_amts: create_rc_signal(BTreeMap::new()), + auth: create_rc_signal(None), } } diff --git a/web/src/components/header.rs b/web/src/components/header.rs index d06904d..1b9ed21 100644 --- a/web/src/components/header.rs +++ b/web/src/components/header.rs @@ -14,15 +14,26 @@ use sycamore::prelude::*; +use crate::app_state; + #[component] pub fn Header(cx: Scope) -> View { + let state = app_state::State::get_from_context(cx); + let login = create_memo(cx, move || { + let user_id = state.auth.get(); + match user_id.as_ref() { + Some(user_data) => format!("{}", user_data.user_id), + None => "Login".to_owned(), + } + }); view! {cx, nav(class="no-print") { h1(class="title") { "Kitchen" } ul { li { a(href="/ui/planning/plan") { "MealPlan" } } li { a(href="/ui/manage/categories") { "Manage" } } - li { a(href="/ui/login") { "Login" } } + li { a(href="/ui/login") { (login.get()) } } + // TODO(jwall): Move to footer? li { a(href="https://github.com/zaphar/kitchen") { "Github" } } } } diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 2f25f56..bd0a8c3 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -11,42 +11,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use base64; -use client_api::AccountResponse; -use reqwasm::http; use sycamore::{futures::spawn_local_scoped, prelude::*}; -use tracing::{debug, error, info}; +use tracing::{debug, info}; -fn token68(user: String, pass: String) -> String { - base64::encode(format!("{}:{}", user, pass)) -} - -async fn authenticate(user: String, pass: String) -> Option { - debug!( - username = user, - password = pass, - "attempting login request against api." - ); - let result = http::Request::get("/api/v1/auth") - .header( - "Authorization", - format!("Basic {}", token68(user, pass)).as_str(), - ) - .send() - .await; - if let Ok(resp) = &result { - if resp.status() == 200 { - return resp - .json() - .await - .expect("Unparseable authentication response"); - } - error!(status = resp.status(), "Login was unsuccessful") - } else { - error!(err=?result.unwrap_err(), "Failed to send auth request"); - } - return None; -} +use crate::app_state; #[component] pub fn LoginForm(cx: Scope) -> View { @@ -57,10 +25,11 @@ pub fn LoginForm(cx: Scope) -> View { let (username, password) = (*clicked.get()).clone(); if username != "" && password != "" { spawn_local_scoped(cx, async move { + let state = app_state::State::get_from_context(cx); + let store = crate::api::HttpStore::get_from_context(cx); debug!("authenticating against ui"); // TODO(jwall): Navigate to plan if the below is successful. - // TODO(jwall): Store account data in our app_state. - authenticate(username, password).await; + state.auth.set(store.authenticate(username, password).await); }); } });