use std::collections::HashMap;
use base64::{prelude::BASE64_STANDARD, Engine};
use chrono::Utc;
use reqwest::Url;
use crate::{auth::{config::AuthConfig, store::TokenData}, common::constant::BASE_URL};
pub struct OAuthApi {
config: AuthConfig,
client: reqwest::Client,
}
impl OAuthApi {
pub fn new(config: AuthConfig) -> Self {
Self {
config,
client: reqwest::Client::new()
}
}
pub fn generate_auth_url(&self) -> String {
format!("{}/oauth/authorize?response_type=code&client_id={}&scope=readonly&redirect_uri={}", BASE_URL, self.config.client_id, self.config.redirect_uri)
}
pub fn extract_code_from_url(&self, returned_url: &str) -> anyhow::Result<String> {
let parsed_url = Url::parse(returned_url)?;
let query_pairs = parsed_url.query_pairs().into_owned().collect::<HashMap<_, _>>();
let Some(code) = query_pairs.get("code").cloned() else {
anyhow::bail!("Failed to parse authorization code. Ensure you pasted the entire URL.");
};
Ok(code)
}
pub fn get_basic_auth_header(&self) -> String {
let credentials = format!("{}:{}", self.config.client_id, self.config.client_secret);
let encoded_creds = BASE64_STANDARD.encode(credentials.as_bytes());
format!("Basic {}", encoded_creds)
}
pub async fn exchange_code_for_tokens(&self, auth_code: &str) -> anyhow::Result<TokenData> {
let url = format!("{}/oauth/token", BASE_URL);
let mut payload = HashMap::new();
payload.insert("grant_type", "authorization_code");
payload.insert("code", auth_code);
payload.insert("redirect_uri", &self.config.redirect_uri);
let response = self.client.post(&url)
.header("Authorization", self.get_basic_auth_header())
.form(&payload)
.send().await?;
let status = response.status();
if !status.is_success() {
let text = response.text().await.unwrap_or_default();
anyhow::bail!(format!("Token exchange failed: {} - {}", status, text));
}
let resp_text = response.text().await?;
let mut token_data = serde_json::from_str::<TokenData>(&resp_text)?;
token_data.expires_at = Utc::now().timestamp() + token_data.expires_in;
Ok(token_data)
}
pub async fn refresh_access_token(&self, refresh_token: &str) -> anyhow::Result<TokenData> {
let url = format!("{}/oauth/token", BASE_URL);
let mut payload = HashMap::new();
payload.insert("grant_type", "refresh_token");
payload.insert("refresh_token", refresh_token);
let response = self.client.post(&url)
.header("Authorization", self.get_basic_auth_header())
.form(&payload)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let text = response.text().await.unwrap_or_default();
anyhow::bail!(format!("Token refresh failed: {} - {}", status, text));
}
let resp_text = response.text().await?;
let mut token_data = serde_json::from_str::<TokenData>(&resp_text)?;
token_data.expires_at = Utc::now().timestamp() + token_data.expires_in;
if token_data.refresh_token.is_empty() {
token_data.refresh_token = refresh_token.to_string();
}
Ok(token_data)
}
}