Skip to main content

efi_bank/
auth.rs

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}