Skip to main content

highlevel_api/auth/
oauth.rs

1use super::token::TokenData;
2use crate::{
3    config::HighLevelConfig,
4    error::{Error, Result},
5};
6use reqwest::Client;
7use std::sync::Arc;
8use url::Url;
9
10const AUTH_BASE_URL: &str = "https://marketplace.gohighlevel.com";
11const TOKEN_URL: &str = "https://services.leadconnectorhq.com/oauth/token";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum AccessType {
15    Online,
16    Offline,
17}
18
19impl AccessType {
20    pub fn as_str(self) -> &'static str {
21        match self {
22            AccessType::Online => "online",
23            AccessType::Offline => "offline",
24        }
25    }
26}
27
28pub struct OAuthClient {
29    config: Arc<HighLevelConfig>,
30    inner: Client,
31    auth_base_url: String,
32}
33
34impl OAuthClient {
35    pub fn new(config: Arc<HighLevelConfig>, inner: Client) -> Self {
36        Self {
37            config,
38            inner,
39            auth_base_url: AUTH_BASE_URL.to_string(),
40        }
41    }
42
43    /// Build the authorization URL to redirect the user to for consent.
44    pub fn authorization_url(&self, scopes: &[&str], access_type: AccessType) -> Result<String> {
45        let mut url = Url::parse(&self.auth_base_url)
46            .map_err(|e| Error::Internal(format!("invalid auth base URL: {e}")))?;
47        url.set_path("/oauth/chooselocation");
48        {
49            let mut q = url.query_pairs_mut();
50            q.append_pair("response_type", "code");
51            if let Some(redirect_uri) = &self.config.redirect_uri {
52                q.append_pair("redirect_uri", redirect_uri);
53            }
54            q.append_pair("client_id", &self.config.client_id);
55            q.append_pair("scope", &scopes.join(" "));
56            q.append_pair("access_type", access_type.as_str());
57        }
58        Ok(url.to_string())
59    }
60
61    /// Exchange an authorization code for an access + refresh token pair.
62    pub async fn exchange_code(&self, code: &str) -> Result<TokenData> {
63        let redirect_uri = self.config.redirect_uri.clone().unwrap_or_default();
64        let params = [
65            ("client_id", self.config.client_id.as_str()),
66            ("client_secret", self.config.client_secret.as_str()),
67            ("grant_type", "authorization_code"),
68            ("code", code),
69            ("redirect_uri", &redirect_uri),
70        ];
71        self.fetch_token(&params).await
72    }
73
74    /// Use a refresh token to obtain a new access token.
75    pub async fn refresh_token(&self, refresh_token: &str) -> Result<TokenData> {
76        let params = [
77            ("client_id", self.config.client_id.as_str()),
78            ("client_secret", self.config.client_secret.as_str()),
79            ("grant_type", "refresh_token"),
80            ("refresh_token", refresh_token),
81        ];
82        self.fetch_token(&params).await
83    }
84
85    /// Exchange an agency-level access token for a location-scoped token.
86    ///
87    /// Equivalent to `POST /oauth/locationToken` with the company and location IDs.
88    /// Requires an agency token to be set on the client before calling.
89    pub async fn location_token(
90        &self,
91        company_id: &str,
92        location_id: &str,
93        agency_token: &str,
94    ) -> Result<TokenData> {
95        let params = [("company_id", company_id), ("location_id", location_id)];
96        let resp = self
97            .inner
98            .post(TOKEN_URL.trim_end_matches("/token").to_string() + "/locationToken")
99            .header("Authorization", format!("Bearer {}", agency_token))
100            .header("Version", &self.config.api_version)
101            .form(&params)
102            .send()
103            .await?;
104
105        if !resp.status().is_success() {
106            let status = resp.status().as_u16();
107            let message = resp.text().await.unwrap_or_default();
108            return Err(Error::Api { status, message });
109        }
110        let mut token: TokenData = resp.json().await?;
111        token.mark_issued_now();
112        Ok(token)
113    }
114
115    async fn fetch_token(&self, params: &[(&str, &str)]) -> Result<TokenData> {
116        let resp = self.inner.post(TOKEN_URL).form(params).send().await?;
117        if !resp.status().is_success() {
118            let status = resp.status().as_u16();
119            let message = resp.text().await.unwrap_or_default();
120            return Err(Error::Api { status, message });
121        }
122        let mut token: TokenData = resp.json().await?;
123        token.mark_issued_now();
124        Ok(token)
125    }
126}