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 #[serde(default)]
25 oauth_client_id: Option<String>,
26 #[serde(default)]
27 oauth_client_secret: Option<String>,
28 #[serde(default)]
29 oauth_access_token: Option<String>,
30 #[serde(default)]
31 oauth_expires_at_unix: Option<i64>,
32}
33
34fn load_credentials() -> Option<Credentials> {
35 let data = std::fs::read_to_string(credentials_path()).ok()?;
36 serde_json::from_str(&data).ok()
37}
38
39fn write_credentials(creds: &Credentials) -> std::io::Result<()> {
40 let dir = config_dir();
41 std::fs::create_dir_all(&dir)?;
42 let json = serde_json::to_string_pretty(creds).map_err(std::io::Error::other)?;
43 std::fs::write(credentials_path(), json)
44}
45
46pub fn save_credentials(api_key: &str, user_id: &str, email: &str) -> std::io::Result<()> {
47 let mut creds = load_credentials().unwrap_or(Credentials {
48 api_key: api_key.to_string(),
49 user_id: user_id.to_string(),
50 email: email.to_string(),
51 oauth_client_id: None,
52 oauth_client_secret: None,
53 oauth_access_token: None,
54 oauth_expires_at_unix: None,
55 });
56 creds.api_key = api_key.to_string();
57 creds.user_id = user_id.to_string();
58 creds.email = email.to_string();
59 creds.oauth_access_token = None;
61 creds.oauth_expires_at_unix = None;
62 write_credentials(&creds)
63}
64
65pub fn load_api_key() -> Option<String> {
66 load_credentials().map(|c| c.api_key)
67}
68
69pub fn is_logged_in() -> bool {
70 load_credentials().is_some()
71}
72
73fn now_unix() -> i64 {
74 use std::time::{SystemTime, UNIX_EPOCH};
75 SystemTime::now()
76 .duration_since(UNIX_EPOCH)
77 .unwrap_or_default()
78 .as_secs() as i64
79}
80
81fn auth_bearer_token() -> Result<String, String> {
82 let mut creds = load_credentials().ok_or("Not logged in. Run: lean-ctx login")?;
83
84 if let (Some(client_id), Some(client_secret)) = (
85 creds.oauth_client_id.clone(),
86 creds.oauth_client_secret.clone(),
87 ) {
88 let now = now_unix();
89 if let (Some(token), Some(exp)) = (
90 creds.oauth_access_token.clone(),
91 creds.oauth_expires_at_unix,
92 ) {
93 if exp > now + 10 {
94 return Ok(token);
95 }
96 }
97
98 let url = format!("{}/oauth/token", api_url());
99 let resp = ureq::post(&url)
100 .header("Content-Type", "application/x-www-form-urlencoded")
101 .send_form([
102 ("grant_type", "client_credentials"),
103 ("client_id", client_id.as_str()),
104 ("client_secret", client_secret.as_str()),
105 ])
106 .map_err(|e| format!("OAuth token request failed: {e}"))?;
107
108 let resp_body = resp
109 .into_body()
110 .read_to_string()
111 .map_err(|e| format!("Failed to read OAuth response: {e}"))?;
112
113 let json: serde_json::Value =
114 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
115
116 let token = json["access_token"]
117 .as_str()
118 .ok_or("Missing access_token in response")?
119 .to_string();
120 let expires_in = json["expires_in"].as_i64().unwrap_or(3600);
121 let exp = now + expires_in.saturating_sub(30);
122
123 creds.oauth_access_token = Some(token.clone());
124 creds.oauth_expires_at_unix = Some(exp);
125 let _ = write_credentials(&creds);
126
127 return Ok(token);
128 }
129
130 Ok(creds.api_key)
131}
132
133pub fn oauth_register_client(client_name: Option<&str>) -> Result<String, String> {
134 let mut creds = load_credentials().ok_or("Not logged in. Run: lean-ctx login")?;
135 if creds.oauth_client_id.is_some() && creds.oauth_client_secret.is_some() {
136 return Ok("OAuth client already registered.".to_string());
137 }
138
139 let url = format!("{}/oauth/register", api_url());
140 let body = if let Some(name) = client_name {
141 serde_json::json!({ "client_name": name })
142 } else {
143 serde_json::json!({})
144 };
145
146 let resp = ureq::post(&url)
147 .header("Authorization", &format!("Bearer {}", creds.api_key))
148 .header("Content-Type", "application/json")
149 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
150 .map_err(|e| format!("OAuth register failed: {e}"))?;
151
152 let resp_body = resp
153 .into_body()
154 .read_to_string()
155 .map_err(|e| format!("Failed to read response: {e}"))?;
156
157 let json: serde_json::Value =
158 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
159
160 creds.oauth_client_id = Some(
161 json["client_id"]
162 .as_str()
163 .ok_or("Missing client_id in response")?
164 .to_string(),
165 );
166 creds.oauth_client_secret = Some(
167 json["client_secret"]
168 .as_str()
169 .ok_or("Missing client_secret in response")?
170 .to_string(),
171 );
172 creds.oauth_access_token = None;
173 creds.oauth_expires_at_unix = None;
174 write_credentials(&creds).map_err(|e| format!("Failed to persist OAuth credentials: {e}"))?;
175
176 Ok("OAuth client registered. Cloud requests will use short-lived access tokens.".to_string())
177}
178
179pub struct RegisterResult {
180 pub api_key: String,
181 pub user_id: String,
182 pub email_verified: bool,
183 pub verification_sent: bool,
184}
185
186pub fn register(email: &str, password: Option<&str>) -> Result<RegisterResult, String> {
187 let url = format!("{}/api/auth/register", api_url());
188 let mut body = serde_json::json!({ "email": email });
189 if let Some(pw) = password {
190 body["password"] = serde_json::Value::String(pw.to_string());
191 }
192
193 let resp = ureq::post(&url)
194 .header("Content-Type", "application/json")
195 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
196 .map_err(|e| format!("Request failed: {e}"))?;
197
198 let resp_body = resp
199 .into_body()
200 .read_to_string()
201 .map_err(|e| format!("Failed to read response: {e}"))?;
202
203 let json: serde_json::Value =
204 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
205
206 Ok(RegisterResult {
207 api_key: json["api_key"]
208 .as_str()
209 .ok_or("Missing api_key in response")?
210 .to_string(),
211 user_id: json["user_id"]
212 .as_str()
213 .ok_or("Missing user_id in response")?
214 .to_string(),
215 email_verified: json["email_verified"].as_bool().unwrap_or(false),
216 verification_sent: json["verification_sent"].as_bool().unwrap_or(false),
217 })
218}
219
220pub fn forgot_password(email: &str) -> Result<String, String> {
221 let url = format!("{}/api/auth/forgot-password", api_url());
222 let body = serde_json::json!({ "email": email });
223
224 let resp = ureq::post(&url)
225 .header("Content-Type", "application/json")
226 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
227 .map_err(|e| format!("Request failed: {e}"))?;
228
229 let resp_body = resp
230 .into_body()
231 .read_to_string()
232 .map_err(|e| format!("Failed to read response: {e}"))?;
233
234 let json: serde_json::Value =
235 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
236
237 Ok(json["message"]
238 .as_str()
239 .unwrap_or("If an account exists, a reset email has been sent.")
240 .to_string())
241}
242
243pub fn login(email: &str, password: &str) -> Result<RegisterResult, String> {
244 let url = format!("{}/api/auth/login", api_url());
245 let body = serde_json::json!({ "email": email, "password": password });
246
247 let resp = ureq::post(&url)
248 .header("Content-Type", "application/json")
249 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
250 .map_err(|e| {
251 let msg = e.to_string();
252 if msg.contains("401") {
253 "Invalid email or password".to_string()
254 } else {
255 format!("Request failed: {e}")
256 }
257 })?;
258
259 let resp_body = resp
260 .into_body()
261 .read_to_string()
262 .map_err(|e| format!("Failed to read response: {e}"))?;
263
264 let json: serde_json::Value =
265 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
266
267 Ok(RegisterResult {
268 api_key: json["api_key"]
269 .as_str()
270 .ok_or("Missing api_key in response")?
271 .to_string(),
272 user_id: json["user_id"]
273 .as_str()
274 .ok_or("Missing user_id in response")?
275 .to_string(),
276 email_verified: json["email_verified"].as_bool().unwrap_or(false),
277 verification_sent: false,
278 })
279}
280
281pub fn sync_stats(stats: &[serde_json::Value]) -> Result<String, String> {
282 let bearer = auth_bearer_token()?;
283 let url = format!("{}/api/stats", api_url());
284
285 let body = serde_json::json!({ "stats": stats });
286
287 let resp = ureq::post(&url)
288 .header("Authorization", &format!("Bearer {bearer}"))
289 .header("Content-Type", "application/json")
290 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
291 .map_err(|e| format!("Sync failed: {e}"))?;
292
293 let resp_body = resp
294 .into_body()
295 .read_to_string()
296 .map_err(|e| format!("Failed to read response: {e}"))?;
297
298 let json: serde_json::Value =
299 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
300
301 Ok(json["message"].as_str().unwrap_or("Synced").to_string())
302}
303
304pub fn contribute(entries: &[serde_json::Value]) -> Result<String, String> {
305 let url = format!("{}/api/contribute", api_url());
306
307 let body = serde_json::json!({ "entries": entries });
308
309 let resp = ureq::post(&url)
310 .header("Content-Type", "application/json")
311 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
312 .map_err(|e| format!("Contribute failed: {e}"))?;
313
314 let resp_body = resp
315 .into_body()
316 .read_to_string()
317 .map_err(|e| format!("Failed to read response: {e}"))?;
318
319 let json: serde_json::Value =
320 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
321
322 Ok(json["message"]
323 .as_str()
324 .unwrap_or("Contributed")
325 .to_string())
326}
327
328pub fn push_knowledge(entries: &[serde_json::Value]) -> Result<String, String> {
329 let bearer = auth_bearer_token()?;
330 let url = format!("{}/api/sync/knowledge", api_url());
331
332 let body = serde_json::json!({ "entries": entries });
333
334 let resp = ureq::post(&url)
335 .header("Authorization", &format!("Bearer {bearer}"))
336 .header("Content-Type", "application/json")
337 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
338 .map_err(|e| format!("Push failed: {e}"))?;
339
340 let resp_body = resp
341 .into_body()
342 .read_to_string()
343 .map_err(|e| format!("Failed to read response: {e}"))?;
344
345 let json: serde_json::Value =
346 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
347
348 Ok(format!(
349 "{} entries synced",
350 json["synced"].as_i64().unwrap_or(0)
351 ))
352}
353
354pub fn pull_cloud_models() -> Result<serde_json::Value, String> {
355 let bearer = auth_bearer_token()?;
356 let url = format!("{}/api/cloud/models", api_url());
357
358 let resp = ureq::get(&url)
359 .header("Authorization", &format!("Bearer {bearer}"))
360 .call()
361 .map_err(|e| {
362 let msg = e.to_string();
363 if msg.contains("403") {
364 "This feature is not available for your account.".to_string()
365 } else {
366 format!("Connection failed. Check your internet connection. ({e})")
367 }
368 })?;
369
370 let resp_body = resp
371 .into_body()
372 .read_to_string()
373 .map_err(|e| format!("Failed to read response: {e}"))?;
374
375 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))
376}
377
378pub fn save_cloud_models(data: &serde_json::Value) -> std::io::Result<()> {
379 let dir = config_dir();
380 std::fs::create_dir_all(&dir)?;
381 let json = serde_json::to_string_pretty(data).map_err(std::io::Error::other)?;
382 std::fs::write(dir.join("cloud_models.json"), json)
383}
384
385pub fn load_cloud_models() -> Option<serde_json::Value> {
386 let path = config_dir().join("cloud_models.json");
387 let data = std::fs::read_to_string(path).ok()?;
388 serde_json::from_str(&data).ok()
389}
390
391pub fn is_cloud_user() -> bool {
392 let path = config_dir().join("plan.txt");
393 std::fs::read_to_string(path).is_ok_and(|p| matches!(p.trim(), "cloud" | "pro"))
394}
395
396pub fn save_plan(plan: &str) -> std::io::Result<()> {
397 let dir = config_dir();
398 std::fs::create_dir_all(&dir)?;
399 std::fs::write(dir.join("plan.txt"), plan)
400}
401
402pub fn fetch_plan() -> Result<String, String> {
403 let bearer = auth_bearer_token()?;
404 let url = format!("{}/api/auth/me", api_url());
405
406 let resp = ureq::get(&url)
407 .header("Authorization", &format!("Bearer {bearer}"))
408 .call()
409 .map_err(|e| format!("Failed to check plan: {e}"))?;
410
411 let resp_body = resp
412 .into_body()
413 .read_to_string()
414 .map_err(|e| format!("Failed to read response: {e}"))?;
415
416 let json: serde_json::Value =
417 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid response: {e}"))?;
418
419 Ok(json["plan"].as_str().unwrap_or("free").to_string())
420}
421
422pub fn push_commands(entries: &[serde_json::Value]) -> Result<String, String> {
423 let bearer = auth_bearer_token()?;
424 let url = format!("{}/api/sync/commands", api_url());
425 let body = serde_json::json!({ "commands": entries });
426 let resp = ureq::post(&url)
427 .header("Authorization", &format!("Bearer {bearer}"))
428 .header("Content-Type", "application/json")
429 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
430 .map_err(|e| format!("Push failed: {e}"))?;
431 let resp_body = resp
432 .into_body()
433 .read_to_string()
434 .map_err(|e| format!("Failed to read response: {e}"))?;
435 let json: serde_json::Value =
436 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
437 Ok(format!(
438 "{} commands synced",
439 json["synced"].as_i64().unwrap_or(0)
440 ))
441}
442
443pub fn push_cep(entries: &[serde_json::Value]) -> Result<String, String> {
444 let bearer = auth_bearer_token()?;
445 let url = format!("{}/api/sync/cep", api_url());
446 let body = serde_json::json!({ "scores": entries });
447 let resp = ureq::post(&url)
448 .header("Authorization", &format!("Bearer {bearer}"))
449 .header("Content-Type", "application/json")
450 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
451 .map_err(|e| format!("Push failed: {e}"))?;
452 let resp_body = resp
453 .into_body()
454 .read_to_string()
455 .map_err(|e| format!("Failed to read response: {e}"))?;
456 let json: serde_json::Value =
457 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
458 Ok(format!(
459 "{} sessions synced",
460 json["synced"].as_i64().unwrap_or(0)
461 ))
462}
463
464pub fn push_gain(entries: &[serde_json::Value]) -> Result<String, String> {
465 let bearer = auth_bearer_token()?;
466 let url = format!("{}/api/sync/gain", api_url());
467 let body = serde_json::json!({ "scores": entries });
468 let resp = ureq::post(&url)
469 .header("Authorization", &format!("Bearer {bearer}"))
470 .header("Content-Type", "application/json")
471 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
472 .map_err(|e| format!("Push failed: {e}"))?;
473 let resp_body = resp
474 .into_body()
475 .read_to_string()
476 .map_err(|e| format!("Failed to read response: {e}"))?;
477 let json: serde_json::Value =
478 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
479 Ok(format!(
480 "{} gain scores synced",
481 json["synced"].as_i64().unwrap_or(0)
482 ))
483}
484
485pub fn push_gotchas(entries: &[serde_json::Value]) -> Result<String, String> {
486 let bearer = auth_bearer_token()?;
487 let url = format!("{}/api/sync/gotchas", api_url());
488 let body = serde_json::json!({ "gotchas": entries });
489 let resp = ureq::post(&url)
490 .header("Authorization", &format!("Bearer {bearer}"))
491 .header("Content-Type", "application/json")
492 .send(&serde_json::to_vec(&body).map_err(|e| format!("JSON error: {e}"))?)
493 .map_err(|e| format!("Push failed: {e}"))?;
494 let resp_body = resp
495 .into_body()
496 .read_to_string()
497 .map_err(|e| format!("Failed to read response: {e}"))?;
498 let json: serde_json::Value =
499 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
500 Ok(format!(
501 "{} gotchas synced",
502 json["synced"].as_i64().unwrap_or(0)
503 ))
504}
505
506pub fn push_buddy(data: &serde_json::Value) -> Result<String, String> {
507 let bearer = auth_bearer_token()?;
508 let url = format!("{}/api/sync/buddy", api_url());
509 let resp = ureq::post(&url)
510 .header("Authorization", &format!("Bearer {bearer}"))
511 .header("Content-Type", "application/json")
512 .send(&serde_json::to_vec(data).map_err(|e| format!("JSON error: {e}"))?)
513 .map_err(|e| format!("Push failed: {e}"))?;
514 let resp_body = resp
515 .into_body()
516 .read_to_string()
517 .map_err(|e| format!("Failed to read response: {e}"))?;
518 let _json: serde_json::Value =
519 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
520 Ok("Buddy synced".to_string())
521}
522
523pub fn push_feedback(entries: &[serde_json::Value]) -> Result<String, String> {
524 let bearer = auth_bearer_token()?;
525 let url = format!("{}/api/sync/feedback", api_url());
526 let resp = ureq::post(&url)
527 .header("Authorization", &format!("Bearer {bearer}"))
528 .header("Content-Type", "application/json")
529 .send(&serde_json::to_vec(entries).map_err(|e| format!("JSON error: {e}"))?)
530 .map_err(|e| format!("Push failed: {e}"))?;
531 let resp_body = resp
532 .into_body()
533 .read_to_string()
534 .map_err(|e| format!("Failed to read response: {e}"))?;
535 let json: serde_json::Value =
536 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
537 Ok(format!(
538 "{} thresholds synced",
539 json["synced"].as_i64().unwrap_or(0)
540 ))
541}
542
543pub fn pull_knowledge() -> Result<Vec<serde_json::Value>, String> {
544 let bearer = auth_bearer_token()?;
545 let url = format!("{}/api/sync/knowledge", api_url());
546
547 let resp = ureq::get(&url)
548 .header("Authorization", &format!("Bearer {bearer}"))
549 .call()
550 .map_err(|e| format!("Pull failed: {e}"))?;
551
552 let resp_body = resp
553 .into_body()
554 .read_to_string()
555 .map_err(|e| format!("Failed to read response: {e}"))?;
556
557 let entries: Vec<serde_json::Value> =
558 serde_json::from_str(&resp_body).map_err(|e| format!("Invalid JSON: {e}"))?;
559
560 Ok(entries)
561}