api.rs
⎇
Raw
1use serde::de::DeserializeOwned;
2use serde::{Deserialize, Serialize};
3use wasm_bindgen::JsCast;
4use wasm_bindgen::JsValue;
5use wasm_bindgen_futures::JsFuture;
6
7#[derive(Debug, thiserror::Error)]
8pub enum ApiError {
9 #[error("{1}")]
10 Http(u16, String),
11 #[error("network error: {0}")]
12 Net(String),
13}
14
15#[derive(Deserialize, Clone)]
16pub struct Me {
17 pub first_boot: bool,
18 #[serde(default)]
19 pub user: Option<UserInfo>,
20 #[serde(default)]
21 pub roots: Vec<RootInfo>,
22}
23
24#[derive(Deserialize, Clone)]
25pub struct UserInfo {
26 #[allow(dead_code)] // used from milestone 2 onwards
27 pub id: i64,
28 pub name: String,
29 pub is_admin: bool,
30}
31
32#[derive(Deserialize, Clone)]
33pub struct RootInfo {
34 #[allow(dead_code)] // used from milestone 2 onwards
35 pub id: i64,
36 pub name: String,
37 pub path: String,
38 pub mode: String,
39}
40
41#[derive(Deserialize, Clone, Debug, PartialEq)]
42pub struct Entry {
43 pub name: String,
44 pub is_dir: bool,
45 pub size: u64,
46 pub mtime: String,
47}
48
49#[derive(Deserialize)]
50pub struct FilesResp {
51 pub entries: Vec<Entry>,
52}
53
54/// Acknowledges a successful 2xx response whose body we don't care about.
55/// Accepts any JSON value (the server sends `{"ok": true}`).
56pub struct OkResp;
57
58impl<'de> Deserialize<'de> for OkResp {
59 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
60 serde::de::IgnoredAny::deserialize(d)?;
61 Ok(OkResp)
62 }
63}
64
65#[derive(Serialize)]
66struct CredentialsBody {
67 name: String,
68 password: String,
69}
70
71pub fn me() -> impl std::future::Future<Output = Result<Me, ApiError>> {
72 request("GET", "/api/auth/me".to_string(), None::<()>)
73}
74
75pub fn login(
76 name: String,
77 password: String,
78) -> impl std::future::Future<Output = Result<OkResp, ApiError>> {
79 request(
80 "POST",
81 "/api/auth/login".to_string(),
82 Some(CredentialsBody { name, password }),
83 )
84}
85
86pub fn setup(
87 name: String,
88 password: String,
89) -> impl std::future::Future<Output = Result<OkResp, ApiError>> {
90 request(
91 "POST",
92 "/api/auth/setup".to_string(),
93 Some(CredentialsBody { name, password }),
94 )
95}
96
97pub fn logout() -> impl std::future::Future<Output = Result<OkResp, ApiError>> {
98 request("POST", "/api/auth/logout".to_string(), Some(()))
99}
100
101pub fn list_files(
102 root_id: i64,
103 path: &str,
104) -> impl std::future::Future<Output = Result<FilesResp, ApiError>> {
105 let url = if path.is_empty() {
106 format!("/api/files/{root_id}")
107 } else {
108 let encoded: Vec<String> = path
109 .split('/')
110 .map(|s| js_sys::encode_uri_component(s).into())
111 .collect();
112 format!("/api/files/{root_id}/{}", encoded.join("/"))
113 };
114 request("GET", url, None::<()>)
115}
116
117async fn request<T: DeserializeOwned>(
118 method: &str,
119 url: String,
120 body: Option<impl Serialize>,
121) -> Result<T, ApiError> {
122 let window =
123 web_sys::window().ok_or_else(|| ApiError::Net("no window available".to_string()))?;
124
125 let opts = web_sys::RequestInit::new();
126 opts.set_method(method);
127 opts.set_mode(web_sys::RequestMode::SameOrigin);
128 if let Some(body) = body {
129 let json = serde_json_to_string(&body)
130 .map_err(|e| ApiError::Net(e.to_string()))?;
131 opts.set_body_opt_str(Some(&json));
132 let headers = web_sys::Headers::new()
133 .expect("Headers constructor failed");
134 let _ = headers.set("Content-Type", "application/json");
135 opts.set_headers_headers(&headers);
136 }
137
138 let req =
139 web_sys::Request::new_with_str_and_init(&url, &opts)
140 .map_err(|e| ApiError::Net(format!("{e:?}")))?;
141 let promise = window.fetch_with_request(&req);
142 let resp_val = JsFuture::from(promise)
143 .await
144 .map_err(|e| ApiError::Net(format!("{e:?}")))?;
145 let resp: web_sys::Response = resp_val
146 .dyn_into()
147 .map_err(|_| ApiError::Net("fetch did not return a Response".to_string()))?;
148
149 let status = resp.status();
150 if !(200..300).contains(&status) {
151 return Err(ApiError::Http(status, error_message(&resp).await));
152 }
153
154 let json_promise = resp
155 .json()
156 .map_err(|e| ApiError::Net(format!("{e:?}")))?;
157 let js: JsValue = JsFuture::from(json_promise)
158 .await
159 .map_err(|e| ApiError::Net(format!("response is not JSON: {e:?}")))?;
160
161 serde_wasm_bindgen::from_value(js).map_err(|e| ApiError::Net(e.to_string()))
162}
163
164/// Best-effort extraction of the server's `{"error": "..."}` message.
165async fn error_message(resp: &web_sys::Response) -> String {
166 let Ok(promise) = resp.text() else {
167 return "request failed".to_string();
168 };
169 let Ok(js) = JsFuture::from(promise).await else {
170 return "request failed".to_string();
171 };
172 let Some(text) = js.as_string() else {
173 return "request failed".to_string();
174 };
175 const MARKER: &str = "\"error\":\"";
176 if let Some(start) = text.find(MARKER) {
177 let rest = &text[start + MARKER.len()..];
178 if let Some(end) = rest.find('"') {
179 return rest[..end].replace("\\\"", "\"");
180 }
181 }
182 "request failed".to_string()
183}
184
185/// Read a form input's value by element id.
186pub fn input_value(id: &str) -> String {
187 web_sys::window()
188 .and_then(|w| w.document())
189 .and_then(|d| d.get_element_by_id(id).and_then(|el| el.dyn_into::<web_sys::HtmlInputElement>().ok()))
190 .map(|i| i.value())
191 .unwrap_or_default()
192}
193
194fn serde_json_to_string(v: &impl Serialize) -> Result<String, serde_wasm_bindgen::Error> {
195 // Reuse the wasm-bindgen JSON serializer; the body must be a plain string
196 // so we convert via JSON text.
197 let value = serde_wasm_bindgen::to_value(v)?;
198 let s = js_sys::JSON::stringify(&value).map_err(|e| {
199 serde_wasm_bindgen::Error::new(format!("JSON.stringify failed: {e:?}"))
200 })?;
201 s.as_string().ok_or_else(|| {
202 serde_wasm_bindgen::Error::new("stringify returned a non-string")
203 })
204}
205