bctx_cloud_core/client/
auth.rs1use anyhow::{bail, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5const TOKEN_FILE: &str = ".bctx/cloud_token.json";
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct TokenStore {
9 pub access_token: String,
10 pub refresh_token: Option<String>,
11 pub expires_at: Option<String>,
12 pub endpoint: String,
13 pub user_id: String,
14 pub email: Option<String>,
15 pub tier: String,
16}
17
18#[derive(Debug, Deserialize)]
19pub struct DeviceCodeResponse {
20 pub device_code: String,
21 pub user_code: String,
22 pub verification_uri: String,
23 pub expires_in: u64,
24 pub interval: u64,
25}
26
27#[derive(Debug, Deserialize)]
28pub struct TokenResponse {
29 pub access_token: String,
30 pub refresh_token: Option<String>,
31 pub expires_in: Option<u64>,
32 pub user_id: String,
33 pub email: Option<String>,
34 pub tier: String,
35}
36
37#[cfg(feature = "cloud-server")]
39pub async fn device_flow_start(endpoint: &str, client_id: &str) -> Result<DeviceCodeResponse> {
40 let url = format!("{endpoint}/auth/device");
41 let resp = reqwest::Client::new()
42 .post(&url)
43 .json(&serde_json::json!({ "client_id": client_id }))
44 .send()
45 .await?
46 .error_for_status()?
47 .json::<DeviceCodeResponse>()
48 .await?;
49 Ok(resp)
50}
51
52#[cfg(not(feature = "cloud-server"))]
53pub async fn device_flow_start(_endpoint: &str, _client_id: &str) -> Result<DeviceCodeResponse> {
54 bail!("cloud-server feature not enabled")
55}
56
57#[cfg(feature = "cloud-server")]
59pub async fn device_flow_poll(
60 endpoint: &str,
61 client_id: &str,
62 device_code: &str,
63 interval_secs: u64,
64) -> Result<TokenResponse> {
65 let url = format!("{endpoint}/auth/token");
66 let client = reqwest::Client::new();
67 loop {
68 tokio::time::sleep(tokio::time::Duration::from_secs(interval_secs)).await;
69 let resp = client
70 .post(&url)
71 .json(&serde_json::json!({
72 "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
73 "device_code": device_code,
74 "client_id": client_id,
75 }))
76 .send()
77 .await?;
78 if resp.status().is_success() {
79 return Ok(resp.json::<TokenResponse>().await?);
80 }
81 let body: serde_json::Value = resp.json().await.unwrap_or_default();
82 match body["error"].as_str() {
83 Some("authorization_pending") => {
84 print!(".");
85 let _ = std::io::Write::flush(&mut std::io::stdout());
86 continue;
87 }
88 Some("slow_down") => tokio::time::sleep(tokio::time::Duration::from_secs(5)).await,
89 Some(e) => bail!("auth error: {e}"),
90 None => bail!("unexpected auth response"),
91 }
92 }
93}
94
95#[cfg(not(feature = "cloud-server"))]
96pub async fn device_flow_poll(
97 _endpoint: &str,
98 _client_id: &str,
99 _device_code: &str,
100 _interval_secs: u64,
101) -> Result<TokenResponse> {
102 bail!("cloud-server feature not enabled")
103}
104
105pub fn token_path() -> PathBuf {
106 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
107 PathBuf::from(home).join(TOKEN_FILE)
108}
109
110pub fn save_token(store: &TokenStore) -> Result<()> {
111 let path = token_path();
112 if let Some(parent) = path.parent() {
113 std::fs::create_dir_all(parent)?;
114 }
115 std::fs::write(&path, serde_json::to_string_pretty(store)?)?;
116 Ok(())
117}
118
119pub fn load_token() -> Option<TokenStore> {
120 let path = token_path();
121 let data = std::fs::read_to_string(path).ok()?;
122 serde_json::from_str(&data).ok()
123}
124
125pub fn clear_token() -> Result<()> {
126 let path = token_path();
127 if path.exists() {
128 std::fs::remove_file(path)?;
129 }
130 Ok(())
131}
132
133#[cfg(feature = "cloud-server")]
134pub async fn fetch_account(endpoint: &str, token: &str) -> Result<serde_json::Value> {
135 let url = format!("{endpoint}/account/me");
136 let resp = reqwest::Client::new()
137 .get(&url)
138 .bearer_auth(token)
139 .send()
140 .await?
141 .error_for_status()?
142 .json::<serde_json::Value>()
143 .await?;
144 Ok(resp)
145}
146
147#[cfg(not(feature = "cloud-server"))]
148pub async fn fetch_account(_endpoint: &str, _token: &str) -> Result<serde_json::Value> {
149 bail!("cloud-server feature not enabled")
150}