working login and logout

This commit is contained in:
Felix Ableitner 2024-01-18 15:31:32 +01:00
parent f41cc01ad3
commit 469ca6f718
9 changed files with 35 additions and 55 deletions

View File

@ -57,7 +57,7 @@ uuid = { version = "1.6.1", features = ["serde"] }
tower-http = { version = "0.4.0", features = ["cors", "fs"], optional = true } tower-http = { version = "0.4.0", features = ["cors", "fs"], optional = true }
serde = { version = "1.0.192", features = ["derive"] } serde = { version = "1.0.192", features = ["derive"] }
url = { version = "2.4.1", features = ["serde"] } url = { version = "2.4.1", features = ["serde"] }
reqwest = { version = "0.11.22", features = ["json"] } reqwest = { version = "0.11.22", features = ["json", "cookies"] }
log = "0.4" log = "0.4"
tracing = "0.1.40" tracing = "0.1.40"
once_cell = "1.18.0" once_cell = "1.18.0"
@ -68,7 +68,6 @@ time = "0.3.31"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
reqwest = { version = "0.11.22", features = ["cookies"] }
[package.metadata.leptos] [package.metadata.leptos]
output-name = "ibis" output-name = "ibis"

View File

@ -3,4 +3,4 @@ filehash = false
target = "assets/index.html" target = "assets/index.html"
[[proxy]] [[proxy]]
backend = "http://[::1]:8131" backend = "http://127.0.0.1:8131"

View File

@ -91,6 +91,7 @@ fn create_cookie(jwt: String, data: &Data<MyDataHandle>) -> Cookie<'static> {
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.path("/") .path("/")
.http_only(true) .http_only(true)
.secure(true)
.expires(Expiration::DateTime( .expires(Expiration::DateTime(
OffsetDateTime::now_utc() + Duration::weeks(52), OffsetDateTime::now_utc() + Duration::weeks(52),
)) ))

View File

@ -6,13 +6,10 @@ use crate::common::{DbArticle, GetArticleData};
use crate::common::{DbInstance, FollowInstance, InstanceView, SearchArticleData}; use crate::common::{DbInstance, FollowInstance, InstanceView, SearchArticleData};
use crate::frontend::error::MyResult; use crate::frontend::error::MyResult;
use anyhow::anyhow; use anyhow::anyhow;
use once_cell::sync::Lazy;
use reqwest::{Client, RequestBuilder, StatusCode}; use reqwest::{Client, RequestBuilder, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
#[derive(Clone)] #[derive(Clone)]
pub struct ApiClient { pub struct ApiClient {
client: Client, client: Client,
@ -116,8 +113,9 @@ impl ApiClient {
let resolve_form = ResolveObject { let resolve_form = ResolveObject {
id: Url::parse(&format!("http://{}", follow_instance))?, id: Url::parse(&format!("http://{}", follow_instance))?,
}; };
let instance_resolved: DbInstance = let instance_resolved: DbInstance = self
get_query(&self.hostname, "instance/resolve", Some(resolve_form)).await?; .get_query("instance/resolve", Some(resolve_form))
.await?;
// send follow // send follow
let follow_form = FollowInstance { let follow_form = FollowInstance {
@ -170,35 +168,29 @@ impl ApiClient {
pub async fn resolve_article(&self, id: Url) -> MyResult<ArticleView> { pub async fn resolve_article(&self, id: Url) -> MyResult<ArticleView> {
let resolve_object = ResolveObject { id }; let resolve_object = ResolveObject { id };
get_query(&self.hostname, "article/resolve", Some(resolve_object)).await self.get_query("article/resolve", Some(resolve_object))
.await
} }
pub async fn resolve_instance(&self, id: Url) -> MyResult<DbInstance> { pub async fn resolve_instance(&self, id: Url) -> MyResult<DbInstance> {
let resolve_object = ResolveObject { id }; let resolve_object = ResolveObject { id };
get_query(&self.hostname, "instance/resolve", Some(resolve_object)).await self.get_query("instance/resolve", Some(resolve_object))
.await
} }
} }
async fn get_query<T, R>(hostname: &str, endpoint: &str, query: Option<R>) -> MyResult<T> async fn handle_json_res<T>(#[allow(unused_mut)] mut req: RequestBuilder) -> MyResult<T>
where where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
R: Serialize,
{ {
let mut req = CLIENT.get(format!("http://{}/api/v1/{}", hostname, endpoint)); #[cfg(not(feature = "ssr"))]
if let Some(query) = query { {
req = req.query(&query); req = req.fetch_credentials_include();
} }
handle_json_res::<T>(req).await
}
async fn handle_json_res<T>(req: RequestBuilder) -> MyResult<T>
where
T: for<'de> Deserialize<'de>,
{
let res = req.send().await?; let res = req.send().await?;
let status = res.status(); let status = res.status();
let text = res.text().await?; let text = res.text().await?;
if status == reqwest::StatusCode::OK { if status == StatusCode::OK {
Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?) Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?)
} else { } else {
Err(anyhow!("API error: {text}").into()) Err(anyhow!("API error: {text}").into())

View File

@ -43,10 +43,9 @@ impl GlobalState {
create_local_resource( create_local_resource(
move || (), move || (),
|_| async move { |_| async move {
if let Ok(my_profile) = GlobalState::api_client().my_profile().await { let my_profile = GlobalState::api_client().my_profile().await.ok();
expect_context::<RwSignal<GlobalState>>() expect_context::<RwSignal<GlobalState>>()
.update(|state| state.my_profile = Some(my_profile.clone())) .update(|state| state.my_profile = my_profile.clone());
};
}, },
); );
} }
@ -54,7 +53,7 @@ impl GlobalState {
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
let backend_hostname = "localhost:8080".to_string(); let backend_hostname = "127.0.0.1:8080".to_string();
provide_meta_context(); provide_meta_context();
let backend_hostname = GlobalState { let backend_hostname = GlobalState {

View File

@ -6,6 +6,12 @@ use leptos_router::*;
#[component] #[component]
pub fn Nav() -> impl IntoView { pub fn Nav() -> impl IntoView {
let global_state = use_context::<RwSignal<GlobalState>>().unwrap(); let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
let logout_action = create_action(move |_| async move {
GlobalState::api_client().logout().await.unwrap();
expect_context::<RwSignal<GlobalState>>()
.get_untracked()
.update_my_profile();
});
view! { view! {
<nav class="inner"> <nav class="inner">
<li> <li>
@ -19,11 +25,7 @@ pub fn Nav() -> impl IntoView {
{ {
move || global_state.with(|state| state.my_profile.clone().unwrap().person.username) move || global_state.with(|state| state.my_profile.clone().unwrap().person.username)
} }
<button on:click=move |_| { <button on:click=move |_| logout_action.dispatch(())>
// TODO: not executed
dbg!(1);
do_logout()
}>
Logout Logout
</button> </button>
</p> </p>
@ -40,14 +42,3 @@ pub fn Nav() -> impl IntoView {
</nav> </nav>
} }
} }
fn do_logout() {
dbg!("do logout");
create_action(move |()| async move {
dbg!("run logout action");
GlobalState::api_client().logout().await.unwrap();
expect_context::<RwSignal<GlobalState>>()
.get()
.update_my_profile();
});
}

View File

@ -26,12 +26,19 @@ pub fn Article() -> impl IntoView {
}, },
); );
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
let (count, set_count) = create_signal(0);
view! { view! {
<Suspense fallback=|| view! { "Loading..." }> <Suspense fallback=|| view! { "Loading..." }>
{move || article.get().map(|article| {move || article.get().map(|article|
view! { view! {
<div class="item-view"> <div class="item-view">
<h1>{article.article.title}</h1> <h1>{article.article.title}</h1>
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
<button on:click=move |_| {
set_count.update(|n| *n += 1);
}>Edit {move || count.get()}</button>
</Show>
<div>{article.article.text}</div> <div>{article.article.text}</div>
</div> </div>
})} })}

View File

@ -8,7 +8,7 @@ pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
.filter_module("ibis", LevelFilter::Info) .filter_module("ibis", LevelFilter::Info)
.init(); .init();
let database_url = "postgres://ibis:password@localhost:5432/ibis"; let database_url = "postgres://ibis:password@localhost:5432/ibis";
ibis_lib::backend::start("localhost:8131", database_url).await?; ibis_lib::backend::start("127.0.0.1:8131", database_url).await?;
Ok(()) Ok(())
} }

View File

@ -115,16 +115,7 @@ impl IbisInstance {
username: username.to_string(), username: username.to_string(),
password: "hunter2".to_string(), password: "hunter2".to_string(),
}; };
// use a separate http client for each backend instance, with cookie store for auth let client = ClientBuilder::new().cookie_store(true).build().unwrap();
// how to pass the client/hostname to api client methods?
// probably create a struct ApiClient(hostname, client) with all api methods in impl
// TODO: seems that cookie isnt being stored? or maybe wrong hostname?
let jar = Arc::new(Jar::default());
let client = ClientBuilder::new()
.cookie_store(true)
.cookie_provider(jar.clone())
.build()
.unwrap();
let api_client = ApiClient::new(client, hostname.clone()); let api_client = ApiClient::new(client, hostname.clone());
api_client.register(form).await.unwrap(); api_client.register(form).await.unwrap();
Self { Self {