amazon_spapi/client/
auth.rs

1use anyhow::Result;
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6#[derive(Debug, Serialize)]
7pub struct LwaTokenRequest {
8    pub grant_type: String,
9    pub client_id: String,
10    pub client_secret: String,
11    pub refresh_token: String,
12}
13
14#[derive(Debug, Deserialize)]
15pub struct LwaTokenResponse {
16    pub access_token: String,
17    pub token_type: String,
18    pub expires_in: u64,
19}
20
21#[derive(Debug, Clone)]
22pub struct CachedToken {
23    pub access_token: String,
24    pub expires_at: u64, // Unix timestamp
25}
26
27pub struct AuthClient {
28    client: Client,
29    client_id: String,
30    client_secret: String,
31    refresh_token: String,
32    cached_token: Option<CachedToken>,
33}
34
35impl AuthClient {
36    pub fn new(client_id: String, client_secret: String, refresh_token: String, user_agent: &String) -> Result<Self> {
37        let client = Client::builder()
38            .timeout(Duration::from_secs(30))
39            .user_agent(user_agent)
40            .build()?;
41        Ok(Self {
42            client, //: Client::new(),
43            client_id,
44            client_secret,
45            refresh_token,
46            cached_token: None,
47        })
48    }
49
50    fn get_current_timestamp() -> u64 {
51        SystemTime::now()
52            .duration_since(UNIX_EPOCH)
53            .unwrap()
54            .as_secs()
55    }
56
57    pub fn is_token_valid(&self) -> bool {
58        if let Some(ref cached) = self.cached_token {
59            let current_time = Self::get_current_timestamp();
60            // Expire 5 minutes early to ensure token is still valid when used
61            let buffer_time = 300; // 5 Minutes
62            cached.expires_at > current_time + buffer_time
63        } else {
64            false
65        }
66    }
67
68    pub async fn get_access_token(&mut self) -> Result<String> {
69        // Check if we have a cached token
70        if self.is_token_valid() {
71            if let Some(ref cached) = self.cached_token {
72                return Ok(cached.access_token.clone());
73            }
74        }
75
76        // If no valid cached token, get a new token
77        self.refresh_access_token().await
78    }
79
80    pub async fn refresh_access_token(&mut self) -> Result<String> {
81        log::debug!("Refreshing access token...");
82        let lwa_url = "https://api.amazon.com/auth/o2/token";
83
84        let request_body = LwaTokenRequest {
85            grant_type: "refresh_token".to_string(),
86            client_id: self.client_id.clone(),
87            client_secret: self.client_secret.clone(),
88            refresh_token: self.refresh_token.clone(),
89        };
90
91        log::debug!("Request Body: {:?}", request_body);
92
93        let response = self
94            .client
95            .post(lwa_url)
96            .header("Content-Type", "application/x-www-form-urlencoded")
97            .form(&request_body)
98            .send()
99            .await?;
100
101        if response.status().is_success() {
102            let token_response: LwaTokenResponse = response.json().await?;
103
104            log::debug!("Response: {:?}", token_response);
105
106            // Calculate expiration time
107            let current_time = Self::get_current_timestamp();
108            let expires_at = current_time + token_response.expires_in;
109
110            // Cache the new token
111            self.cached_token = Some(CachedToken {
112                access_token: token_response.access_token.clone(),
113                expires_at,
114            });
115
116            // println!("New access token cached, Valid until: {}, ({} seconds remaining)",
117            //     expires_at,
118            //     token_response.expires_in
119            // );
120
121            Ok(token_response.access_token)
122        } else {
123            let error_text = response.text().await?;
124            Err(anyhow::anyhow!(
125                "Failed to get access token: {}",
126                error_text
127            ))
128        }
129    }
130
131    /// Get the remaining time (in seconds) for the current cached token
132    pub fn get_token_remaining_time(&self) -> Option<u64> {
133        if let Some(ref cached) = self.cached_token {
134            let current_time = Self::get_current_timestamp();
135            if cached.expires_at > current_time {
136                Some(cached.expires_at - current_time)
137            } else {
138                Some(0)
139            }
140        } else {
141            None
142        }
143    }
144}