diff --git a/Cargo.lock b/Cargo.lock index b8b4667..00f6046 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1781,6 +1781,7 @@ dependencies = [ "getrandom", "gloo-net", "hex", + "http", "jsonwebtoken", "katex", "leptos", @@ -1803,6 +1804,7 @@ dependencies = [ "send_wrapper", "serde", "serde_json", + "serde_urlencoded", "sha2", "smart-default", "time", diff --git a/Cargo.toml b/Cargo.toml index 482d3f8..e50ddf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,12 +29,9 @@ ssr = [ "config", "tower", "tower-layer", + "reqwest","diesel-derive-newtype" ] -hydrate = [ - "leptos/hydrate", - "katex/wasm-js", -] -diesel-derive-newtype = ["dep:diesel-derive-newtype"] +hydrate = ["leptos/hydrate", "katex/wasm-js", "gloo-net"] # This profile significantly speeds up build time. If debug info is needed you can comment the line # out temporarily, but make sure to leave this in the main branch. @@ -73,7 +70,10 @@ sha2 = "0.10.8" uuid = { version = "1.11.0", features = ["serde"] } serde = { version = "1.0.215", features = ["derive"] } url = { version = "2.5.3", features = ["serde"] } -reqwest = { version = "0.12.9", features = ["json", "cookies"] } +reqwest = { version = "0.12.9", features = [ + "json", + "cookies", +], optional = true } log = "0.4" tracing = "0.1.40" once_cell = "1.20.2" @@ -121,8 +121,10 @@ tower = { version = "0.5.1", optional = true } tower-layer = { version = "0.3.3", optional = true } console_log = "1.0.0" send_wrapper = "0.6.0" -gloo-net = "0.6.0" +gloo-net = { version = "0.6.0", optional = true } web-sys = "0.3.72" +http = "1.1.0" +serde_urlencoded = "0.7.1" [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/src/common/mod.rs b/src/common/mod.rs index a7b9ae8..cb5f983 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -36,7 +36,7 @@ pub struct GetArticleForm { pub id: Option, } -#[derive(Deserialize, Serialize, Clone, Default)] +#[derive(Deserialize, Serialize, Clone, Default, Debug)] pub struct ListArticlesForm { pub only_local: Option, pub instance_id: Option, @@ -128,13 +128,13 @@ impl Default for EditVersion { } } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct RegisterUserForm { pub username: String, pub password: String, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct LoginUserForm { pub username: String, pub password: String, @@ -188,7 +188,7 @@ impl DbPerson { } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct CreateArticleForm { pub title: String, pub text: String, @@ -217,19 +217,19 @@ pub struct ProtectArticleForm { pub protected: bool, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct ForkArticleForm { pub article_id: ArticleId, pub new_title: String, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct ApproveArticleForm { pub article_id: ArticleId, pub approve: bool, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct DeleteConflictForm { pub conflict_id: ConflictId, } @@ -244,12 +244,12 @@ pub struct FollowInstance { pub id: InstanceId, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct SearchArticleForm { pub query: String, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct ResolveObject { pub id: Url, } diff --git a/src/frontend/api.rs b/src/frontend/api.rs index 6e0e21b..4093748 100644 --- a/src/frontend/api.rs +++ b/src/frontend/api.rs @@ -11,37 +11,40 @@ use crate::{ frontend::error::MyResult, }; use anyhow::anyhow; -use reqwest::{Client, RequestBuilder, StatusCode}; +use http::*; use serde::{Deserialize, Serialize}; +use std::fmt::Debug; use std::sync::LazyLock; use url::Url; -pub static CLIENT: LazyLock = LazyLock::new(|| ApiClient::new(Client::new(), None)); +pub static CLIENT: LazyLock = LazyLock::new(|| { + #[cfg(feature = "ssr")] + { + ApiClient::new(reqwest::Client::new(), None) + } + #[cfg(not(feature = "ssr"))] + { + ApiClient::new() + } +}); #[derive(Clone)] pub struct ApiClient { - client: Client, + #[cfg(feature = "ssr")] + client: reqwest::Client, pub hostname: String, ssl: bool, } impl ApiClient { - pub fn new(client: Client, hostname_: Option) -> Self { + #[cfg(feature = "ssr")] + pub fn new(client: reqwest::Client, hostname_: Option) -> Self { let mut hostname; let ssl; - #[cfg(not(feature = "ssr"))] - { - use leptos_use::use_document; - hostname = use_document().location().unwrap().host().unwrap(); - ssl = !cfg!(debug_assertions); - } - #[cfg(feature = "ssr")] - { - use leptos::config::get_config_from_str; - let leptos_options = get_config_from_str(include_str!("../../Cargo.toml")).unwrap(); - hostname = leptos_options.site_addr.to_string(); - ssl = false; - } + use leptos::config::get_config_from_str; + let leptos_options = get_config_from_str(include_str!("../../Cargo.toml")).unwrap(); + hostname = leptos_options.site_addr.to_string(); + ssl = false; // required for tests if let Some(hostname_) = hostname_ { hostname = hostname_; @@ -52,17 +55,12 @@ impl ApiClient { ssl, } } - - async fn get(&self, endpoint: &str, query: Option) -> MyResult - where - T: for<'de> Deserialize<'de>, - R: Serialize, - { - let mut req = self.client.get(self.request_endpoint(endpoint)); - if let Some(query) = query { - req = req.query(&query); - } - self.send::(req).await + #[cfg(not(feature = "ssr"))] + pub fn new() -> Self { + use leptos_use::use_document; + let hostname = use_document().location().unwrap().host().unwrap(); + let ssl = !cfg!(debug_assertions); + Self { hostname, ssl } } pub async fn get_article(&self, data: GetArticleForm) -> MyResult { @@ -74,38 +72,24 @@ impl ApiClient { } pub async fn register(&self, register_form: RegisterUserForm) -> MyResult { - let req = self - .client - .post(self.request_endpoint("/api/v1/account/register")) - .form(®ister_form); - self.send::(req).await + self.post("/api/v1/account/register", Some(®ister_form)) + .await } pub async fn login(&self, login_form: LoginUserForm) -> MyResult { - let req = self - .client - .post(self.request_endpoint("/api/v1/account/login")) - .form(&login_form); - self.send::(req).await + self.post("/api/v1/account/login", Some(&login_form)).await } pub async fn create_article(&self, data: &CreateArticleForm) -> MyResult { - let req = self - .client - .post(self.request_endpoint("/api/v1/article")) - .form(data); - self.send(req).await + self.send(Method::POST, "/api/v1/article", Some(&data)) + .await } pub async fn edit_article_with_conflict( &self, edit_form: &EditArticleForm, ) -> MyResult> { - let req = self - .client - .patch(self.request_endpoint("/api/v1/article")) - .form(edit_form); - self.send(req).await + self.get("/api/v1/article", Some(&edit_form)).await } pub async fn edit_article(&self, edit_form: &EditArticleForm) -> MyResult { @@ -121,17 +105,13 @@ impl ApiClient { } pub async fn notifications_list(&self) -> MyResult> { - let req = self - .client - .get(self.request_endpoint("/api/v1/user/notifications/list")); - self.send(req).await + self.get("/api/v1/user/notifications/list", None::<()>) + .await } pub async fn notifications_count(&self) -> MyResult { - let req = self - .client - .get(self.request_endpoint("/api/v1/user/notifications/count")); - self.send(req).await + self.get("/api/v1/user/notifications/count", None::<()>) + .await } pub async fn approve_article(&self, article_id: ArticleId, approve: bool) -> MyResult<()> { @@ -139,20 +119,13 @@ impl ApiClient { article_id, approve, }; - let req = self - .client - .post(self.request_endpoint("/api/v1/article/approve")) - .form(&form); - self.send(req).await + self.post("/api/v1/article/approve", Some(&form)).await } pub async fn delete_conflict(&self, conflict_id: ConflictId) -> MyResult<()> { let form = DeleteConflictForm { conflict_id }; - let req = self - .client - .delete(self.request_endpoint("/api/v1/conflict")) - .form(&form); - self.send(req).await + self.send(Method::DELETE, "/api/v1/conflict", Some(form)) + .await } pub async fn search(&self, search_form: &SearchArticleForm) -> MyResult> { @@ -164,7 +137,7 @@ impl ApiClient { } pub async fn get_instance(&self, get_form: &GetInstance) -> MyResult { - self.get("/api/v1/instance", Some(get_form)).await + self.get("/api/v1/instance", Some(&get_form)).await } pub async fn list_instances(&self) -> MyResult> { @@ -210,31 +183,22 @@ impl ApiClient { } pub async fn site(&self) -> MyResult { - let req = self.client.get(self.request_endpoint("/api/v1/site")); - self.send(req).await + self.get("/api/v1/site", None::<()>).await } pub async fn logout(&self) -> MyResult<()> { - let req = self - .client - .get(self.request_endpoint("/api/v1/account/logout")); - Ok(self.send(req).await.unwrap()) + Ok(self + .get("/api/v1/account/logout", None::<()>) + .await + .unwrap()) } pub async fn fork_article(&self, form: &ForkArticleForm) -> MyResult { - let req = self - .client - .post(self.request_endpoint("/api/v1/article/fork")) - .form(form); - Ok(self.send(req).await.unwrap()) + Ok(self.post("/api/v1/article/fork", Some(form)).await.unwrap()) } pub async fn protect_article(&self, params: &ProtectArticleForm) -> MyResult { - let req = self - .client - .post(self.request_endpoint("/api/v1/article/protect")) - .form(params); - self.send(req).await + self.post("/api/v1/article/protect", Some(params)).await } pub async fn resolve_article(&self, id: Url) -> MyResult { @@ -245,47 +209,67 @@ impl ApiClient { pub async fn resolve_instance(&self, id: Url) -> MyResult { let resolve_object = ResolveObject { id }; - self.get("/api/v1/instance/resolve", Some(resolve_object)) - .await + self.get("/api/v1/user", Some(resolve_object)).await } pub async fn get_user(&self, data: GetUserForm) -> MyResult { self.get("/api/v1/user", Some(data)).await } - #[cfg(feature = "ssr")] - async fn send(&self, mut req: RequestBuilder) -> MyResult + + async fn get(&self, endpoint: &str, query: Option) -> MyResult where T: for<'de> Deserialize<'de>, + R: Serialize + Debug, + { + self.send(Method::GET, endpoint, query).await + } + + async fn post(&self, endpoint: &str, query: Option) -> MyResult + where + T: for<'de> Deserialize<'de>, + R: Serialize + Debug, + { + self.send(Method::POST, endpoint, query).await + } + + #[cfg(feature = "ssr")] + async fn send(&self, method: Method, path: &str, params: Option

) -> MyResult + where + P: Serialize + Debug, + T: for<'de> Deserialize<'de>, { use crate::common::{Auth, AUTH_COOKIE}; use leptos::prelude::use_context; use reqwest::header::HeaderName; - + let mut req = self + .client + .request(method, self.request_endpoint(path)) + .query(¶ms); let auth = use_context::(); if let Some(Auth(Some(auth))) = auth { req = req.header(HeaderName::from_static(AUTH_COOKIE), auth); } let res = req.send().await?; let status = res.status(); - let text = res.text().await?; - if status == StatusCode::OK { - Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?) - } else { - Err(anyhow!("API error: {text}").into()) - } + let text = res.text().await?.to_string(); + Self::response(status.into(), text) } - #[cfg(feature = "hydrate")] - fn send( - &self, - mut req: RequestBuilder, - ) -> impl std::future::Future> + Send + #[cfg(not(feature = "ssr"))] + fn send<'a, P, T>( + &'a self, + method: Method, + path: &'a str, + params: Option

, + ) -> impl std::future::Future> + Send + 'a where + P: Serialize + Debug + 'a, T: for<'de> Deserialize<'de>, { use gloo_net::http::*; use leptos::prelude::on_cleanup; use send_wrapper::SendWrapper; - use web_sys::RequestCredentials; + use std::collections::HashMap; + use web_sys::{RequestCredentials, UrlSearchParams}; SendWrapper::new(async move { let abort_controller = SendWrapper::new(web_sys::AbortController::new().ok()); @@ -298,26 +282,42 @@ impl ApiClient { } }); - /* - if status == StatusCode::OK { - Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?) + let path_with_endpoint = self.request_endpoint(path); + let path = if method == Method::GET { + let query = serde_urlencoded::to_string(¶ms).unwrap(); + format!("{path_with_endpoint}?{query}") } else { - Err(anyhow!("API error: {text}").into()) - } - */ - Ok(RequestBuilder::new("/api/v1/site") - .method(Method::GET) + path_with_endpoint + }; + log::info!("{path}"); + let builder = RequestBuilder::new(&path) + .method(method.clone()) .abort_signal(abort_signal.as_ref()) - .credentials(RequestCredentials::Include) - .send() - .await - .unwrap() - .json() - .await - .unwrap()) + .credentials(RequestCredentials::Include); + let req = if method == Method::POST { + builder.json(¶ms) + } else { + builder.build() + } + .unwrap(); + let res = req.send().await.unwrap(); + let status = res.status(); + let text = res.text().await.unwrap(); + Self::response(status, text) }) } + fn response(status: u16, text: String) -> MyResult + where + T: for<'de> Deserialize<'de>, + { + if status == StatusCode::OK { + Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?) + } else { + Err(anyhow!("API error: {text}").into()) + } + } + fn request_endpoint(&self, path: &str) -> String { let protocol = if self.ssl { "https" } else { "http" }; format!("{protocol}://{}{path}", &self.hostname) diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index abd9abd..5558956 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -16,7 +16,7 @@ pub fn hydrate() { use crate::frontend::app::App; console_log::init_with_level(log::Level::Debug).expect("error initializing logger"); console_error_panic_hook::set_once(); - mount_to_body(App); + leptos::mount::hydrate_body(App); } fn article_link(article: &DbArticle) -> String {