use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; #[derive(Debug, thiserror::Error)] pub enum ApiError { #[error("{1}")] Http(u16, String), #[error("network error: {0}")] Net(String), } #[derive(Deserialize, Clone)] pub struct Me { pub first_boot: bool, #[serde(default)] pub user: Option, #[serde(default)] pub roots: Vec, } #[derive(Deserialize, Clone)] pub struct UserInfo { #[allow(dead_code)] // used from milestone 2 onwards pub id: i64, pub name: String, pub is_admin: bool, } #[derive(Deserialize, Clone)] pub struct RootInfo { #[allow(dead_code)] // used from milestone 2 onwards pub id: i64, pub name: String, pub path: String, pub mode: String, } /// Acknowledges a successful 2xx response whose body we don't care about. /// Accepts any JSON value (the server sends `{"ok": true}`). pub struct OkResp; impl<'de> Deserialize<'de> for OkResp { fn deserialize>(d: D) -> Result { serde::de::IgnoredAny::deserialize(d)?; Ok(OkResp) } } #[derive(Serialize)] struct CredentialsBody { name: String, password: String, } pub fn me() -> impl std::future::Future> { request("GET", "/api/auth/me", None::<()>) } pub fn login( name: String, password: String, ) -> impl std::future::Future> { request( "POST", "/api/auth/login", Some(CredentialsBody { name, password }), ) } pub fn setup( name: String, password: String, ) -> impl std::future::Future> { request( "POST", "/api/auth/setup", Some(CredentialsBody { name, password }), ) } pub fn logout() -> impl std::future::Future> { request("POST", "/api/auth/logout", Some(())) } async fn request( method: &str, url: &str, body: Option, ) -> Result { let window = web_sys::window().ok_or_else(|| ApiError::Net("no window available".to_string()))?; let opts = web_sys::RequestInit::new(); opts.set_method(method); opts.set_mode(web_sys::RequestMode::SameOrigin); if let Some(body) = body { let json = serde_json_to_string(&body) .map_err(|e| ApiError::Net(e.to_string()))?; opts.set_body_opt_str(Some(&json)); let headers = web_sys::Headers::new() .expect("Headers constructor failed"); let _ = headers.set("Content-Type", "application/json"); opts.set_headers_headers(&headers); } let req = web_sys::Request::new_with_str_and_init(url, &opts) .map_err(|e| ApiError::Net(format!("{e:?}")))?; let promise = window.fetch_with_request(&req); let resp_val = JsFuture::from(promise) .await .map_err(|e| ApiError::Net(format!("{e:?}")))?; let resp: web_sys::Response = resp_val .dyn_into() .map_err(|_| ApiError::Net("fetch did not return a Response".to_string()))?; let status = resp.status(); if !(200..300).contains(&status) { return Err(ApiError::Http(status, error_message(&resp).await)); } let json_promise = resp .json() .map_err(|e| ApiError::Net(format!("{e:?}")))?; let js: JsValue = JsFuture::from(json_promise) .await .map_err(|e| ApiError::Net(format!("response is not JSON: {e:?}")))?; serde_wasm_bindgen::from_value(js).map_err(|e| ApiError::Net(e.to_string())) } /// Best-effort extraction of the server's `{"error": "..."}` message. async fn error_message(resp: &web_sys::Response) -> String { let Ok(promise) = resp.text() else { return "request failed".to_string(); }; let Ok(js) = JsFuture::from(promise).await else { return "request failed".to_string(); }; let Some(text) = js.as_string() else { return "request failed".to_string(); }; const MARKER: &str = "\"error\":\""; if let Some(start) = text.find(MARKER) { let rest = &text[start + MARKER.len()..]; if let Some(end) = rest.find('"') { return rest[..end].replace("\\\"", "\""); } } "request failed".to_string() } /// Read a form input's value by element id. pub fn input_value(id: &str) -> String { web_sys::window() .and_then(|w| w.document()) .and_then(|d| d.get_element_by_id(id).and_then(|el| el.dyn_into::().ok())) .map(|i| i.value()) .unwrap_or_default() } fn serde_json_to_string(v: &impl Serialize) -> Result { // Reuse the wasm-bindgen JSON serializer; the body must be a plain string // so we convert via JSON text. let value = serde_wasm_bindgen::to_value(v)?; let s = js_sys::JSON::stringify(&value).map_err(|e| { serde_wasm_bindgen::Error::new(format!("JSON.stringify failed: {e:?}")) })?; s.as_string().ok_or_else(|| { serde_wasm_bindgen::Error::new("stringify returned a non-string") }) }