amazon_spapi/client/
auth.rs1use 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, }
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 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 let buffer_time = 300; 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 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 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 let current_time = Self::get_current_timestamp();
122 let expires_at = current_time + token_response.expires_in;
123
124 self.cached_token = Some(CachedToken {
126 access_token: token_response.access_token.clone(),
127 expires_at,
128 });
129
130 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 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}