Skip to main content

highlevel_api/auth/
oauth.rs

1use reqwest::Client;
2use std::sync::Arc;
3use url::Url;
4use crate::{
5    config::HighLevelConfig,
6    error::{Error, Result},
7};
8use super::token::TokenData;
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}
32
33impl OAuthClient {
34    pub fn new(config: Arc<HighLevelConfig>, inner: Client) -> Self {
35        Self { config, inner }
36    }
37
38    /// Build the authorization URL to redirect the user to for consent.
39    pub fn authorization_url(&self, scopes: &[&str], access_type: AccessType) -> String {
40        let mut url = Url::parse(AUTH_BASE_URL).expect("static URL is valid");
41        url.set_path("/oauth/chooselocation");
42        {
43            let mut q = url.query_pairs_mut();
44            q.append_pair("response_type", "code");
45            if let Some(redirect_uri) = &self.config.redirect_uri {
46                q.append_pair("redirect_uri", redirect_uri);
47            }
48            q.append_pair("client_id", &self.config.client_id);
49            q.append_pair("scope", &scopes.join(" "));
50            q.append_pair("access_type", access_type.as_str());
51        }
52        url.to_string()
53    }
54
55    /// Exchange an authorization code for an access + refresh token pair.
56    pub async fn exchange_code(&self, code: &str) -> Result<TokenData> {
57        let redirect_uri = self.config.redirect_uri.clone().unwrap_or_default();
58        let params = [
59            ("client_id", self.config.client_id.as_str()),
60            ("client_secret", self.config.client_secret.as_str()),
61            ("grant_type", "authorization_code"),
62            ("code", code),
63            ("redirect_uri", &redirect_uri),
64        ];
65        self.fetch_token(&params).await
66    }
67
68    /// Use a refresh token to obtain a new access token.
69    pub async fn refresh_token(&self, refresh_token: &str) -> Result<TokenData> {
70        let params = [
71            ("client_id", self.config.client_id.as_str()),
72            ("client_secret", self.config.client_secret.as_str()),
73            ("grant_type", "refresh_token"),
74            ("refresh_token", refresh_token),
75        ];
76        self.fetch_token(&params).await
77    }
78
79    async fn fetch_token(&self, params: &[(&str, &str)]) -> Result<TokenData> {
80        let resp = self.inner.post(TOKEN_URL).form(params).send().await?;
81        if !resp.status().is_success() {
82            let status = resp.status().as_u16();
83            let message = resp.text().await.unwrap_or_default();
84            return Err(Error::Api { status, message });
85        }
86        let mut token: TokenData = resp.json().await?;
87        token.mark_issued_now();
88        Ok(token)
89    }
90}