highlevel_api/auth/
oauth.rs1use 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 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 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(¶ms).await
72 }
73
74 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(¶ms).await
83 }
84
85 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(¶ms)
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}