1use std::path::PathBuf;
2
3fn config_dir() -> PathBuf {
4 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
5 home.join(".lean-ctx").join("cloud")
6}
7
8fn credentials_path() -> PathBuf {
9 config_dir().join("credentials.json")
10}
11
12pub fn api_url() -> String {
13 std::env::var("LEAN_CTX_API_URL").unwrap_or_else(|_| "https://api.leanctx.com".to_string())
14}
15
16#[derive(serde::Serialize, serde::Deserialize)]
17struct Credentials {
18 api_key: String,
19 user_id: String,
20 email: String,
21}
22
23pub fn save_credentials(api_key: &str, user_id: &str, email: &str) -> std::io::Result<()> {
24 let dir = config_dir();
25 std::fs::create_dir_all(&dir)?;
26 let creds = Credentials {
27 api_key: api_key.to_string(),
28 user_id: user_id.to_string(),
29 email: email.to_string(),
30 };
31 let json = serde_json::to_string_pretty(&creds).map_err(std::io::Error::other)?;
32 std::fs::write(credentials_path(), json)
33}
34
35pub fn load_api_key() -> Option<String> {
36 let data = std::fs::read_to_string(credentials_path()).ok()?;
37 let creds: Credentials = serde_json::from_str(&data).ok()?;
38 Some(creds.api_key)
39}
40
41pub fn is_logged_in() -> bool {
42 load_api_key().is_some()
43}
44
45pub struct RegisterResult {
46 pub api_key: String,
47 pub user_id: String,
48 pub email_verified: bool,
49 pub verification_sent: bool,
50}
51
52pub fn register(email: &str, password: Option<&str>) -> Result<RegisterResult, String> {
53 let url = format!("{}/api/auth/register", api_url());
54 let mut body = serde_json::json!({ "email": email });
55 if let Some(pw) = password {
56 body["password"] = serde_json::Value::String(pw.to_string());
57 }
58
59 let resp = ureq::post(&url)
60 .header("Content-Type", "application/json")
61 .send(serde_json::to_vec(&body).unwrap().as_slice())
62 .map_err(|e| format!("Request failed: {e}"))?;
63
64 let resp_body = resp
65 .into_body()
66 .read_to_string()
67 .map_err(|e| format!("Failed to read response: {e}"))?;
68
69 let json: serde_json::Value =
70 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
71
72 Ok(RegisterResult {
73 api_key: json["api_key"]
74 .as_str()
75 .ok_or("Missing api_key in response")?
76 .to_string(),
77 user_id: json["user_id"]
78 .as_str()
79 .ok_or("Missing user_id in response")?
80 .to_string(),
81 email_verified: json["email_verified"].as_bool().unwrap_or(false),
82 verification_sent: json["verification_sent"].as_bool().unwrap_or(false),
83 })
84}
85
86pub fn forgot_password(email: &str) -> Result<String, String> {
87 let url = format!("{}/api/auth/forgot-password", api_url());
88 let body = serde_json::json!({ "email": email });
89
90 let resp = ureq::post(&url)
91 .header("Content-Type", "application/json")
92 .send(serde_json::to_vec(&body).unwrap().as_slice())
93 .map_err(|e| format!("Request failed: {e}"))?;
94
95 let resp_body = resp
96 .into_body()
97 .read_to_string()
98 .map_err(|e| format!("Failed to read response: {e}"))?;
99
100 let json: serde_json::Value =
101 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
102
103 Ok(json["message"]
104 .as_str()
105 .unwrap_or("If an account exists, a reset email has been sent.")
106 .to_string())
107}
108
109pub fn login(email: &str, password: &str) -> Result<RegisterResult, String> {
110 let url = format!("{}/api/auth/login", api_url());
111 let body = serde_json::json!({ "email": email, "password": password });
112
113 let resp = ureq::post(&url)
114 .header("Content-Type", "application/json")
115 .send(serde_json::to_vec(&body).unwrap().as_slice())
116 .map_err(|e| {
117 let msg = e.to_string();
118 if msg.contains("401") {
119 "Invalid email or password".to_string()
120 } else {
121 format!("Request failed: {e}")
122 }
123 })?;
124
125 let resp_body = resp
126 .into_body()
127 .read_to_string()
128 .map_err(|e| format!("Failed to read response: {e}"))?;
129
130 let json: serde_json::Value =
131 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
132
133 Ok(RegisterResult {
134 api_key: json["api_key"]
135 .as_str()
136 .ok_or("Missing api_key in response")?
137 .to_string(),
138 user_id: json["user_id"]
139 .as_str()
140 .ok_or("Missing user_id in response")?
141 .to_string(),
142 email_verified: json["email_verified"].as_bool().unwrap_or(false),
143 verification_sent: false,
144 })
145}
146
147pub fn sync_stats(stats: &[serde_json::Value]) -> Result<String, String> {
148 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
149 let url = format!("{}/api/stats", api_url());
150
151 let body = serde_json::json!({ "stats": stats });
152
153 let resp = ureq::post(&url)
154 .header("Authorization", &format!("Bearer {api_key}"))
155 .header("Content-Type", "application/json")
156 .send(serde_json::to_vec(&body).unwrap().as_slice())
157 .map_err(|e| format!("Sync failed: {e}"))?;
158
159 let resp_body = resp
160 .into_body()
161 .read_to_string()
162 .map_err(|e| format!("Failed to read response: {e}"))?;
163
164 let json: serde_json::Value =
165 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
166
167 Ok(json["message"].as_str().unwrap_or("Synced").to_string())
168}
169
170pub fn contribute(entries: &[serde_json::Value]) -> Result<String, String> {
171 let url = format!("{}/api/contribute", api_url());
172
173 let body = serde_json::json!({ "entries": entries });
174
175 let resp = ureq::post(&url)
176 .header("Content-Type", "application/json")
177 .send(serde_json::to_vec(&body).unwrap().as_slice())
178 .map_err(|e| format!("Contribute failed: {e}"))?;
179
180 let resp_body = resp
181 .into_body()
182 .read_to_string()
183 .map_err(|e| format!("Failed to read response: {e}"))?;
184
185 let json: serde_json::Value =
186 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
187
188 Ok(json["message"]
189 .as_str()
190 .unwrap_or("Contributed")
191 .to_string())
192}
193
194pub fn push_knowledge(entries: &[serde_json::Value]) -> Result<String, String> {
195 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
196 let url = format!("{}/api/sync/knowledge", api_url());
197
198 let body = serde_json::json!({ "entries": entries });
199
200 let resp = ureq::post(&url)
201 .header("Authorization", &format!("Bearer {api_key}"))
202 .header("Content-Type", "application/json")
203 .send(serde_json::to_vec(&body).unwrap().as_slice())
204 .map_err(|e| format!("Push failed: {e}"))?;
205
206 let resp_body = resp
207 .into_body()
208 .read_to_string()
209 .map_err(|e| format!("Failed to read response: {e}"))?;
210
211 let json: serde_json::Value =
212 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
213
214 Ok(format!(
215 "{} entries synced",
216 json["synced"].as_i64().unwrap_or(0)
217 ))
218}
219
220pub fn pull_cloud_models() -> Result<serde_json::Value, String> {
221 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login <email>")?;
222 let url = format!("{}/api/cloud/models", api_url());
223
224 let resp = ureq::get(&url)
225 .header("Authorization", &format!("Bearer {api_key}"))
226 .call()
227 .map_err(|e| {
228 let msg = e.to_string();
229 if msg.contains("403") {
230 "This feature is not available for your account.".to_string()
231 } else {
232 format!("Connection failed. Check your internet connection. ({e})")
233 }
234 })?;
235
236 let resp_body = resp
237 .into_body()
238 .read_to_string()
239 .map_err(|e| format!("Failed to read response: {e}"))?;
240
241 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))
242}
243
244pub fn save_cloud_models(data: &serde_json::Value) -> std::io::Result<()> {
245 let dir = config_dir();
246 std::fs::create_dir_all(&dir)?;
247 let json = serde_json::to_string_pretty(data).map_err(std::io::Error::other)?;
248 std::fs::write(dir.join("cloud_models.json"), json)
249}
250
251pub fn load_cloud_models() -> Option<serde_json::Value> {
252 let path = config_dir().join("cloud_models.json");
253 let data = std::fs::read_to_string(path).ok()?;
254 serde_json::from_str(&data).ok()
255}
256
257pub fn is_cloud_user() -> bool {
258 let path = config_dir().join("plan.txt");
259 std::fs::read_to_string(path)
260 .map(|p| matches!(p.trim(), "cloud" | "pro"))
261 .unwrap_or(false)
262}
263
264pub fn save_plan(plan: &str) -> std::io::Result<()> {
265 let dir = config_dir();
266 std::fs::create_dir_all(&dir)?;
267 std::fs::write(dir.join("plan.txt"), plan)
268}
269
270pub fn fetch_plan() -> Result<String, String> {
271 let api_key = load_api_key().ok_or("Not logged in")?;
272 let url = format!("{}/api/auth/me", api_url());
273
274 let resp = ureq::get(&url)
275 .header("Authorization", &format!("Bearer {api_key}"))
276 .call()
277 .map_err(|e| format!("Failed to check plan: {e}"))?;
278
279 let resp_body = resp
280 .into_body()
281 .read_to_string()
282 .map_err(|e| format!("Failed to read response: {e}"))?;
283
284 let json: serde_json::Value =
285 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))?;
286
287 Ok(json["plan"].as_str().unwrap_or("free").to_string())
288}
289
290pub fn push_commands(entries: &[serde_json::Value]) -> Result<String, String> {
291 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
292 let url = format!("{}/api/sync/commands", api_url());
293 let body = serde_json::json!({ "commands": entries });
294 let resp = ureq::post(&url)
295 .header("Authorization", &format!("Bearer {api_key}"))
296 .header("Content-Type", "application/json")
297 .send(serde_json::to_vec(&body).unwrap().as_slice())
298 .map_err(|e| format!("Push failed: {e}"))?;
299 let resp_body = resp
300 .into_body()
301 .read_to_string()
302 .map_err(|e| format!("Failed to read response: {e}"))?;
303 let json: serde_json::Value =
304 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
305 Ok(format!(
306 "{} commands synced",
307 json["synced"].as_i64().unwrap_or(0)
308 ))
309}
310
311pub fn push_cep(entries: &[serde_json::Value]) -> Result<String, String> {
312 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
313 let url = format!("{}/api/sync/cep", api_url());
314 let body = serde_json::json!({ "scores": entries });
315 let resp = ureq::post(&url)
316 .header("Authorization", &format!("Bearer {api_key}"))
317 .header("Content-Type", "application/json")
318 .send(serde_json::to_vec(&body).unwrap().as_slice())
319 .map_err(|e| format!("Push failed: {e}"))?;
320 let resp_body = resp
321 .into_body()
322 .read_to_string()
323 .map_err(|e| format!("Failed to read response: {e}"))?;
324 let json: serde_json::Value =
325 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
326 Ok(format!(
327 "{} sessions synced",
328 json["synced"].as_i64().unwrap_or(0)
329 ))
330}
331
332pub fn push_gain(entries: &[serde_json::Value]) -> Result<String, String> {
333 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
334 let url = format!("{}/api/sync/gain", api_url());
335 let body = serde_json::json!({ "scores": entries });
336 let resp = ureq::post(&url)
337 .header("Authorization", &format!("Bearer {api_key}"))
338 .header("Content-Type", "application/json")
339 .send(serde_json::to_vec(&body).unwrap().as_slice())
340 .map_err(|e| format!("Push failed: {e}"))?;
341 let resp_body = resp
342 .into_body()
343 .read_to_string()
344 .map_err(|e| format!("Failed to read response: {e}"))?;
345 let json: serde_json::Value =
346 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
347 Ok(format!(
348 "{} gain scores synced",
349 json["synced"].as_i64().unwrap_or(0)
350 ))
351}
352
353pub fn push_gotchas(entries: &[serde_json::Value]) -> Result<String, String> {
354 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
355 let url = format!("{}/api/sync/gotchas", api_url());
356 let body = serde_json::json!({ "gotchas": entries });
357 let resp = ureq::post(&url)
358 .header("Authorization", &format!("Bearer {api_key}"))
359 .header("Content-Type", "application/json")
360 .send(serde_json::to_vec(&body).unwrap().as_slice())
361 .map_err(|e| format!("Push failed: {e}"))?;
362 let resp_body = resp
363 .into_body()
364 .read_to_string()
365 .map_err(|e| format!("Failed to read response: {e}"))?;
366 let json: serde_json::Value =
367 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
368 Ok(format!(
369 "{} gotchas synced",
370 json["synced"].as_i64().unwrap_or(0)
371 ))
372}
373
374pub fn push_buddy(data: &serde_json::Value) -> Result<String, String> {
375 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
376 let url = format!("{}/api/sync/buddy", api_url());
377 let resp = ureq::post(&url)
378 .header("Authorization", &format!("Bearer {api_key}"))
379 .header("Content-Type", "application/json")
380 .send(serde_json::to_vec(data).unwrap().as_slice())
381 .map_err(|e| format!("Push failed: {e}"))?;
382 let resp_body = resp
383 .into_body()
384 .read_to_string()
385 .map_err(|e| format!("Failed to read response: {e}"))?;
386 let _json: serde_json::Value =
387 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
388 Ok("Buddy synced".to_string())
389}
390
391pub fn push_feedback(entries: &[serde_json::Value]) -> Result<String, String> {
392 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
393 let url = format!("{}/api/sync/feedback", api_url());
394 let resp = ureq::post(&url)
395 .header("Authorization", &format!("Bearer {api_key}"))
396 .header("Content-Type", "application/json")
397 .send(serde_json::to_vec(entries).unwrap().as_slice())
398 .map_err(|e| format!("Push failed: {e}"))?;
399 let resp_body = resp
400 .into_body()
401 .read_to_string()
402 .map_err(|e| format!("Failed to read response: {e}"))?;
403 let json: serde_json::Value =
404 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
405 Ok(format!(
406 "{} thresholds synced",
407 json["synced"].as_i64().unwrap_or(0)
408 ))
409}
410
411pub fn pull_knowledge() -> Result<Vec<serde_json::Value>, String> {
412 let api_key = load_api_key().ok_or("Not logged in. Run: lean-ctx login")?;
413 let url = format!("{}/api/sync/knowledge", api_url());
414
415 let resp = ureq::get(&url)
416 .header("Authorization", &format!("Bearer {api_key}"))
417 .call()
418 .map_err(|e| format!("Pull failed: {e}"))?;
419
420 let resp_body = resp
421 .into_body()
422 .read_to_string()
423 .map_err(|e| format!("Failed to read response: {e}"))?;
424
425 let entries: Vec<serde_json::Value> =
426 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
427
428 Ok(entries)
429}