1use std::time::{Duration, Instant};
2
3use serde::Deserialize;
4use serde_json::json;
5
6use crate::client::Client;
7use crate::error::Error;
8
9const TOKEN_REFRESH_SKEW_SECS: u64 = 30;
10
11#[derive(Debug)]
12pub(crate) struct AccessToken {
13 pub(crate) value: String,
14 pub(crate) expires_at: Instant,
15}
16
17impl AccessToken {
18 pub(crate) fn is_expired(&self) -> bool {
19 let refresh_skew = Duration::from_secs(TOKEN_REFRESH_SKEW_SECS);
20 Instant::now() + refresh_skew >= self.expires_at
21 }
22}
23
24#[derive(Debug, Deserialize)]
25struct OAuthResponse {
26 access_token: String,
27 expires_in: u64,
28}
29
30impl Client {
31 pub async fn authenticate(&self) -> Result<(), Error> {
32 let endpoints = self.endpoints();
33 self.authenticate_with_url(endpoints.pix_api_oauth_token_url)
34 .await
35 }
36
37 pub async fn authenticate_billing(&self) -> Result<(), Error> {
38 let endpoints = self.endpoints();
39 self.authenticate_with_url(endpoints.billing_api_oauth_token_url)
40 .await
41 }
42
43 pub(crate) async fn get_valid_access_token(&self) -> Result<String, Error> {
44 let endpoints = self.endpoints();
45 self.get_valid_access_token_with_url(endpoints.pix_api_oauth_token_url)
46 .await
47 }
48
49 pub(crate) async fn get_valid_billing_access_token(&self) -> Result<String, Error> {
50 let endpoints = self.endpoints();
51 self.get_valid_access_token_with_url(endpoints.billing_api_oauth_token_url)
52 .await
53 }
54
55 async fn authenticate_with_url(&self, token_url: &str) -> Result<(), Error> {
56 let response = self
57 .http
58 .post(token_url)
59 .basic_auth(&self.id, Some(&self.secret))
60 .json(&json!({ "grant_type": "client_credentials" }))
61 .send()
62 .await?;
63
64 if !response.status().is_success() {
65 let status = response.status();
66 let body = response.text().await.unwrap_or_else(|_| String::new());
67 return Err(Error::RequestFailed { status, body });
68 }
69
70 let oauth = response.json::<OAuthResponse>().await?;
71 let expires_at = Instant::now() + Duration::from_secs(oauth.expires_in);
72
73 self.token
74 .lock()
75 .map_err(|_| Error::AuthUnavailable)?
76 .replace(AccessToken {
77 value: oauth.access_token,
78 expires_at,
79 });
80
81 Ok(())
82 }
83
84 async fn get_valid_access_token_with_url(&self, token_url: &str) -> Result<String, Error> {
85 let needs_authentication = {
86 let token = self.token.lock().map_err(|_| Error::AuthUnavailable)?;
87 token.as_ref().is_none_or(AccessToken::is_expired)
88 };
89
90 if needs_authentication {
91 self.authenticate_with_url(token_url).await?;
92 }
93
94 let token = self.token.lock().map_err(|_| Error::AuthUnavailable)?;
95 token
96 .as_ref()
97 .map(|cached| cached.value.clone())
98 .ok_or(Error::AuthUnavailable)
99 }
100}