use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
const TOKEN_FILE: &str = ".bctx/cloud_token.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenStore {
pub access_token: String,
pub refresh_token: Option<String>,
pub expires_at: Option<String>,
pub endpoint: String,
pub user_id: String,
pub tier: String,
}
#[derive(Debug, Deserialize)]
pub struct DeviceCodeResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub expires_in: u64,
pub interval: u64,
}
#[derive(Debug, Deserialize)]
pub struct TokenResponse {
pub access_token: String,
pub refresh_token: Option<String>,
pub expires_in: Option<u64>,
pub user_id: String,
pub tier: String,
}
#[cfg(feature = "cloud-server")]
pub async fn device_flow_start(endpoint: &str, client_id: &str) -> Result<DeviceCodeResponse> {
let url = format!("{endpoint}/auth/device");
let resp = reqwest::Client::new()
.post(&url)
.json(&serde_json::json!({ "client_id": client_id }))
.send()
.await?
.error_for_status()?
.json::<DeviceCodeResponse>()
.await?;
Ok(resp)
}
#[cfg(not(feature = "cloud-server"))]
pub async fn device_flow_start(_endpoint: &str, _client_id: &str) -> Result<DeviceCodeResponse> {
bail!("cloud-server feature not enabled")
}
#[cfg(feature = "cloud-server")]
pub async fn device_flow_poll(
endpoint: &str,
client_id: &str,
device_code: &str,
interval_secs: u64,
) -> Result<TokenResponse> {
let url = format!("{endpoint}/auth/token");
let client = reqwest::Client::new();
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(interval_secs)).await;
let resp = client
.post(&url)
.json(&serde_json::json!({
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code,
"client_id": client_id,
}))
.send()
.await?;
if resp.status().is_success() {
return Ok(resp.json::<TokenResponse>().await?);
}
let body: serde_json::Value = resp.json().await.unwrap_or_default();
match body["error"].as_str() {
Some("authorization_pending") => {
print!(".");
let _ = std::io::Write::flush(&mut std::io::stdout());
continue;
}
Some("slow_down") => tokio::time::sleep(tokio::time::Duration::from_secs(5)).await,
Some(e) => bail!("auth error: {e}"),
None => bail!("unexpected auth response"),
}
}
}
#[cfg(not(feature = "cloud-server"))]
pub async fn device_flow_poll(
_endpoint: &str,
_client_id: &str,
_device_code: &str,
_interval_secs: u64,
) -> Result<TokenResponse> {
bail!("cloud-server feature not enabled")
}
pub fn token_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(TOKEN_FILE)
}
pub fn save_token(store: &TokenStore) -> Result<()> {
let path = token_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, serde_json::to_string_pretty(store)?)?;
Ok(())
}
pub fn load_token() -> Option<TokenStore> {
let path = token_path();
let data = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&data).ok()
}
pub fn clear_token() -> Result<()> {
let path = token_path();
if path.exists() {
std::fs::remove_file(path)?;
}
Ok(())
}
#[cfg(feature = "cloud-server")]
pub async fn fetch_account(endpoint: &str, token: &str) -> Result<serde_json::Value> {
let url = format!("{endpoint}/account/me");
let resp = reqwest::Client::new()
.get(&url)
.bearer_auth(token)
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?;
Ok(resp)
}
#[cfg(not(feature = "cloud-server"))]
pub async fn fetch_account(_endpoint: &str, _token: &str) -> Result<serde_json::Value> {
bail!("cloud-server feature not enabled")
}