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