User user_data response to show the user id in the header

This commit is contained in:
Jeremy Wall 2022-12-22 10:49:50 -05:00
parent 997d95e201
commit 7343c77a04
8 changed files with 76 additions and 42 deletions

View File

@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize};
use recipes::{IngredientKey, RecipeEntry}; use recipes::{IngredientKey, RecipeEntry};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
pub enum Response<T> { pub enum Response<T> {
Success(T), Success(T),
Err { status: u16, message: String }, Err { status: u16, message: String },
@ -103,7 +103,7 @@ pub type CategoryResponse = Response<String>;
pub type EmptyResponse = Response<()>; pub type EmptyResponse = Response<()>;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
pub struct UserData { pub struct UserData {
pub user_id: String, pub user_id: String,
} }

View File

@ -10,7 +10,7 @@ edition = "2021"
tracing = "0.1.35" tracing = "0.1.35"
tracing-subscriber = "0.3.14" tracing-subscriber = "0.3.14"
recipes = { path = "../recipes" } recipes = { path = "../recipes" }
api = { path = "../api" } client-api = { path = "../api", features = ["server"], package = "api" }
csv = "1.1.1" csv = "1.1.1"
rust-embed="6.4.0" rust-embed="6.4.0"
mime_guess = "2.0.4" mime_guess = "2.0.4"

View File

@ -14,13 +14,13 @@
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use api;
use async_session::{Session, SessionStore}; use async_session::{Session, SessionStore};
use axum::{ use axum::{
extract::Extension, extract::Extension,
http::{header, HeaderMap, StatusCode}, http::{header, HeaderMap, StatusCode},
}; };
use axum_auth::AuthBasic; use axum_auth::AuthBasic;
use client_api as api;
use cookie::{Cookie, SameSite}; use cookie::{Cookie, SameSite};
use secrecy::Secret; use secrecy::Secret;
use tracing::{debug, error, info, instrument}; use tracing::{debug, error, info, instrument};

View File

@ -30,7 +30,7 @@ use tower::ServiceBuilder;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::{debug, info, instrument}; use tracing::{debug, info, instrument};
use api; use client_api as api;
use storage::{APIStore, AuthStore}; use storage::{APIStore, AuthStore};
mod auth; mod auth;

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use base64;
use reqwasm; use reqwasm;
use serde_json::{from_str, to_string}; use serde_json::{from_str, to_string};
use sycamore::prelude::*; 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"); info!("Synchronizing categories");
match store.get_categories().await { match store.get_categories().await {
Ok(Some(categories_content)) => { Ok(Some(categories_content)) => {
@ -164,6 +175,10 @@ fn recipe_key<S: std::fmt::Display>(id: S) -> String {
format!("recipe:{}", id) format!("recipe:{}", id)
} }
fn token68(user: String, pass: String) -> String {
base64::encode(format!("{}:{}", user, pass))
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct HttpStore { pub struct HttpStore {
root: String, root: String,
@ -194,6 +209,42 @@ impl HttpStore {
use_context::<std::rc::Rc<Self>>(cx).clone() use_context::<std::rc::Rc<Self>>(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<UserData> {
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::<AccountResponse>()
.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] //#[instrument]
pub async fn get_categories(&self) -> Result<Option<String>, Error> { pub async fn get_categories(&self) -> Result<Option<String>, Error> {
let mut path = self.v1_path(); let mut path = self.v1_path();

View File

@ -16,6 +16,7 @@ use std::collections::{BTreeMap, BTreeSet};
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use client_api::UserData;
use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe}; use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe};
#[derive(Debug)] #[derive(Debug)]
@ -27,6 +28,7 @@ pub struct State {
pub category_map: RcSignal<BTreeMap<String, String>>, pub category_map: RcSignal<BTreeMap<String, String>>,
pub filtered_ingredients: RcSignal<BTreeSet<IngredientKey>>, pub filtered_ingredients: RcSignal<BTreeSet<IngredientKey>>,
pub modified_amts: RcSignal<BTreeMap<IngredientKey, RcSignal<String>>>, pub modified_amts: RcSignal<BTreeMap<IngredientKey, RcSignal<String>>>,
pub auth: RcSignal<Option<UserData>>,
} }
impl State { impl State {
@ -39,6 +41,7 @@ impl State {
category_map: create_rc_signal(BTreeMap::new()), category_map: create_rc_signal(BTreeMap::new()),
filtered_ingredients: create_rc_signal(BTreeSet::new()), filtered_ingredients: create_rc_signal(BTreeSet::new()),
modified_amts: create_rc_signal(BTreeMap::new()), modified_amts: create_rc_signal(BTreeMap::new()),
auth: create_rc_signal(None),
} }
} }

View File

@ -14,15 +14,26 @@
use sycamore::prelude::*; use sycamore::prelude::*;
use crate::app_state;
#[component] #[component]
pub fn Header<G: Html>(cx: Scope) -> View<G> { pub fn Header<G: Html>(cx: Scope) -> View<G> {
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, view! {cx,
nav(class="no-print") { nav(class="no-print") {
h1(class="title") { "Kitchen" } h1(class="title") { "Kitchen" }
ul { ul {
li { a(href="/ui/planning/plan") { "MealPlan" } } li { a(href="/ui/planning/plan") { "MealPlan" } }
li { a(href="/ui/manage/categories") { "Manage" } } 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" } } li { a(href="https://github.com/zaphar/kitchen") { "Github" } }
} }
} }

View File

@ -11,42 +11,10 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use base64;
use client_api::AccountResponse;
use reqwasm::http;
use sycamore::{futures::spawn_local_scoped, prelude::*}; use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{debug, error, info}; use tracing::{debug, info};
fn token68(user: String, pass: String) -> String { use crate::app_state;
base64::encode(format!("{}:{}", user, pass))
}
async fn authenticate(user: String, pass: String) -> Option<AccountResponse> {
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;
}
#[component] #[component]
pub fn LoginForm<G: Html>(cx: Scope) -> View<G> { pub fn LoginForm<G: Html>(cx: Scope) -> View<G> {
@ -57,10 +25,11 @@ pub fn LoginForm<G: Html>(cx: Scope) -> View<G> {
let (username, password) = (*clicked.get()).clone(); let (username, password) = (*clicked.get()).clone();
if username != "" && password != "" { if username != "" && password != "" {
spawn_local_scoped(cx, async move { 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"); debug!("authenticating against ui");
// TODO(jwall): Navigate to plan if the below is successful. // TODO(jwall): Navigate to plan if the below is successful.
// TODO(jwall): Store account data in our app_state. state.auth.set(store.authenticate(username, password).await);
authenticate(username, password).await;
}); });
} }
}); });