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};
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub enum Response<T> {
Success(T),
Err { status: u16, message: String },
@ -103,7 +103,7 @@ pub type CategoryResponse = Response<String>;
pub type EmptyResponse = Response<()>;
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct UserData {
pub user_id: String,
}

View File

@ -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"

View File

@ -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};

View File

@ -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;

View File

@ -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<S: std::fmt::Display>(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::<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]
pub async fn get_categories(&self) -> Result<Option<String>, Error> {
let mut path = self.v1_path();

View File

@ -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<BTreeMap<String, String>>,
pub filtered_ingredients: RcSignal<BTreeSet<IngredientKey>>,
pub modified_amts: RcSignal<BTreeMap<IngredientKey, RcSignal<String>>>,
pub auth: RcSignal<Option<UserData>>,
}
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),
}
}

View File

@ -14,15 +14,26 @@
use sycamore::prelude::*;
use crate::app_state;
#[component]
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,
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" } }
}
}

View File

@ -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<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;
}
use crate::app_state;
#[component]
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();
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);
});
}
});