Skip to main content

google_workspace_apis/auth/
client.rs

1use std::{str::FromStr, sync::Arc};
2
3use anyhow::Error;
4use chrono::{DateTime, Utc};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use super::refresh_acces_token;
9
10#[derive(Debug, JsonSchema, Clone, Default, Serialize, Deserialize)]
11pub struct AccessToken {
12    #[serde(
13        default,
14        skip_serializing_if = "String::is_empty",
15        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
16    )]
17    pub token_type: String,
18
19    #[serde(
20        default,
21        skip_serializing_if = "String::is_empty",
22        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
23    )]
24    pub access_token: String,
25    #[serde(default)]
26    pub expires_in: i64,
27
28    #[serde(
29        default,
30        skip_serializing_if = "String::is_empty",
31        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
32    )]
33    pub refresh_token: String,
34    #[serde(default, alias = "x_refresh_token_expires_in")]
35    pub refresh_token_expires_in: i64,
36
37    #[serde(
38        default,
39        skip_serializing_if = "String::is_empty",
40        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
41    )]
42    pub scope: String,
43}
44
45#[derive(Debug, JsonSchema, Clone, Default, Serialize, Deserialize)]
46pub struct ClientTokenData {
47    #[serde(
48        default,
49        skip_serializing_if = "String::is_empty",
50        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
51    )]
52    pub access_token: String,
53
54    #[serde(default)]
55    pub expires_on: chrono::DateTime<chrono::Utc>,
56
57    #[serde(
58        default,
59        skip_serializing_if = "String::is_empty",
60        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
61    )]
62    pub refresh_token: String,
63}
64
65#[derive(Debug, JsonSchema, Clone, Default, Serialize, Deserialize)]
66pub struct ClientCredentials {
67    #[serde(
68        default,
69        skip_serializing_if = "String::is_empty",
70        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
71    )]
72    pub client_id: String,
73
74    #[serde(
75        default,
76        skip_serializing_if = "String::is_empty",
77        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
78    )]
79    pub client_secret: String,
80
81    #[serde(
82        default,
83        skip_serializing_if = "String::is_empty",
84        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
85    )]
86    pub redirect_uri: String,
87
88    #[serde(
89        default,
90        skip_serializing_if = "String::is_empty",
91        deserialize_with = "crate::utils::deserialize::deserialize_nullable_string::deserialize"
92    )]
93    pub refresh_token: String,
94}
95
96#[derive(Clone, Default)]
97pub struct GoogleClient {
98    pub client_credentials: ClientCredentials,
99    pub access_token: Option<ClientTokenData>,
100    pub req_client: reqwest::Client,
101    pub auto_refresh_token: bool,
102    refresh_handlers: Vec<Arc<dyn TokenRefreshHandler>>,
103}
104
105impl std::fmt::Debug for GoogleClient {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("GoogleClient")
108            .field("access_token", &self.access_token)
109            .field("refresh_token", &"[REDACTED]")
110            .field(
111                "token_expiry",
112                &self.access_token.as_ref().unwrap().expires_on,
113            )
114            .field("client_id", &self.client_credentials.client_id)
115            .field("client_secret", &"[REDACTED]")
116            .field(
117                "refresh_handlers",
118                &format!("[{} handlers]", self.refresh_handlers.len()),
119            )
120            .finish()
121    }
122}
123
124impl From<AccessToken> for ClientTokenData {
125    fn from(token: AccessToken) -> Self {
126        let now = chrono::Utc::now();
127        let new_expires_on = now + chrono::Duration::seconds(token.expires_in);
128        Self {
129            access_token: token.access_token,
130            expires_on: new_expires_on,
131            refresh_token: token.refresh_token,
132        }
133    }
134}
135
136impl GoogleClient {
137    pub fn new(
138        client_credentials: ClientCredentials,
139        access_token: AccessToken,
140        auto_refresh_token: bool,
141    ) -> Self {
142        let client = build_default_reqwest_client(&access_token.access_token);
143        Self {
144            client_credentials,
145            access_token: Some(access_token.into()),
146            req_client: client,
147            auto_refresh_token,
148            refresh_handlers: Vec::new(),
149        }
150    }
151
152    pub fn add_token_refresh_handler<H>(&mut self, handler: H)
153    where
154        H: TokenRefreshHandler + 'static,
155    {
156        self.refresh_handlers.push(Arc::new(handler));
157    }
158
159    pub async fn refresh_access_token_check(&mut self) -> Result<(), Error> {
160        if self.auto_refresh_token && !self.is_access_token_valid() {
161            self.update_access_token().await?;
162        }
163        Ok(())
164    }
165
166    pub fn enable_auto_refresh(&mut self) {
167        self.auto_refresh_token = true;
168    }
169
170    pub fn disable_auto_refresh(&mut self) {
171        self.auto_refresh_token = false;
172    }
173
174    pub fn is_access_token_valid(&self) -> bool {
175        if let Some(token_data) = &self.access_token {
176            let now = chrono::Utc::now();
177            return now < token_data.expires_on;
178        }
179        false
180    }
181
182    pub async fn update_access_token(&mut self) -> Result<(), Error> {
183        let new_token = refresh_acces_token(&self.client_credentials).await?;
184        self.access_token = Some(new_token.clone().into());
185        let client = build_default_reqwest_client(&new_token.access_token);
186        self.req_client = client;
187
188        for handler in &mut self.refresh_handlers {
189            handler.on_token_refresh(
190                self.access_token.as_ref().unwrap().access_token.clone(),
191                self.access_token.as_ref().unwrap().refresh_token.clone(),
192                self.access_token.as_ref().unwrap().expires_on,
193            );
194        }
195        Ok(())
196    }
197}
198
199pub trait TokenRefreshHandler: Send + Sync {
200    fn on_token_refresh(&self, new_token: String, refresh_token: String, new_expiry: DateTime<Utc>);
201}
202
203/// Helper function to convert a stored expiration date in UTC string format
204/// to the amount of seconds the token is still valid
205pub fn get_validity_token_secs(datetime_str: &str) -> i64 {
206    let now = chrono::Utc::now();
207
208    let saved_date = chrono::DateTime::<chrono::Utc>::from_str(datetime_str).unwrap();
209    let seconds_valid = saved_date - now;
210    seconds_valid.num_seconds()
211}
212
213fn build_default_reqwest_client(token: &str) -> reqwest::Client {
214    let mut headers = reqwest::header::HeaderMap::new();
215    headers.insert(
216        reqwest::header::AUTHORIZATION,
217        format!("Bearer {token}").parse().unwrap(),
218    );
219    headers.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap());
220    headers.insert(
221        reqwest::header::CONTENT_TYPE,
222        "application/json".parse().unwrap(),
223    );
224    reqwest::Client::builder()
225        .default_headers(headers)
226        .build()
227        .expect("Failed to build reqwest client")
228}
229
230// Implement for Fn closures
231impl<F> TokenRefreshHandler for F
232where
233    F: Fn(String, String, DateTime<Utc>) + Send + Sync,
234{
235    fn on_token_refresh(
236        &self,
237        new_token: String,
238        refresh_token: String,
239        new_expiry: DateTime<Utc>,
240    ) {
241        self(new_token, refresh_token, new_expiry);
242    }
243}