use super::token::TokenData;
use crate::{
config::HighLevelConfig,
error::{Error, Result},
};
use reqwest::Client;
use std::sync::Arc;
use url::Url;
const AUTH_BASE_URL: &str = "https://marketplace.gohighlevel.com";
const TOKEN_URL: &str = "https://services.leadconnectorhq.com/oauth/token";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessType {
Online,
Offline,
}
impl AccessType {
pub fn as_str(self) -> &'static str {
match self {
AccessType::Online => "online",
AccessType::Offline => "offline",
}
}
}
pub struct OAuthClient {
config: Arc<HighLevelConfig>,
inner: Client,
auth_base_url: String,
}
impl OAuthClient {
pub fn new(config: Arc<HighLevelConfig>, inner: Client) -> Self {
Self {
config,
inner,
auth_base_url: AUTH_BASE_URL.to_string(),
}
}
pub fn authorization_url(&self, scopes: &[&str], access_type: AccessType) -> Result<String> {
let mut url = Url::parse(&self.auth_base_url)
.map_err(|e| Error::Internal(format!("invalid auth base URL: {e}")))?;
url.set_path("/oauth/chooselocation");
{
let mut q = url.query_pairs_mut();
q.append_pair("response_type", "code");
if let Some(redirect_uri) = &self.config.redirect_uri {
q.append_pair("redirect_uri", redirect_uri);
}
q.append_pair("client_id", &self.config.client_id);
q.append_pair("scope", &scopes.join(" "));
q.append_pair("access_type", access_type.as_str());
}
Ok(url.to_string())
}
pub async fn exchange_code(&self, code: &str) -> Result<TokenData> {
let redirect_uri = self.config.redirect_uri.clone().unwrap_or_default();
let params = [
("client_id", self.config.client_id.as_str()),
("client_secret", self.config.client_secret.as_str()),
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", &redirect_uri),
];
self.fetch_token(¶ms).await
}
pub async fn refresh_token(&self, refresh_token: &str) -> Result<TokenData> {
let params = [
("client_id", self.config.client_id.as_str()),
("client_secret", self.config.client_secret.as_str()),
("grant_type", "refresh_token"),
("refresh_token", refresh_token),
];
self.fetch_token(¶ms).await
}
pub async fn location_token(
&self,
company_id: &str,
location_id: &str,
agency_token: &str,
) -> Result<TokenData> {
let params = [("company_id", company_id), ("location_id", location_id)];
let resp = self
.inner
.post(TOKEN_URL.trim_end_matches("/token").to_string() + "/locationToken")
.header("Authorization", format!("Bearer {}", agency_token))
.header("Version", &self.config.api_version)
.form(¶ms)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let message = resp.text().await.unwrap_or_default();
return Err(Error::Api { status, message });
}
let mut token: TokenData = resp.json().await?;
token.mark_issued_now();
Ok(token)
}
async fn fetch_token(&self, params: &[(&str, &str)]) -> Result<TokenData> {
let resp = self.inner.post(TOKEN_URL).form(params).send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let message = resp.text().await.unwrap_or_default();
return Err(Error::Api { status, message });
}
let mut token: TokenData = resp.json().await?;
token.mark_issued_now();
Ok(token)
}
}