mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
User user_data response to show the user id in the header
This commit is contained in:
parent
997d95e201
commit
7343c77a04
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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};
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user