amazon_spapi/client/
auth.rs

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