1use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use reqwest::Client as HttpClient;
9use serde::Serialize;
10use tokio::sync::RwLock;
11
12use crate::error::{Error, ErrorCode, Result};
13use crate::models::{ApiResponse, TokenResponse};
14
15pub const TOKEN_EXPIRY_SECONDS: u64 = 3600;
17
18pub const TOKEN_EXPIRY_BUFFER: u64 = 60;
20
21pub const AUTH_ENDPOINT: &str = "/dceapi/cms/auth/accessToken";
23
24#[derive(Debug, Default)]
26struct TokenState {
27 token: String,
29 expires_at: Option<Instant>,
31}
32
33#[derive(Debug, Serialize)]
35struct AuthRequest {
36 secret: String,
37}
38
39#[derive(Debug)]
48pub struct TokenManager {
49 api_key: String,
50 secret: String,
51 base_url: String,
52 http_client: HttpClient,
53 state: Arc<RwLock<TokenState>>,
54}
55
56impl TokenManager {
57 pub fn new(
59 api_key: impl Into<String>,
60 secret: impl Into<String>,
61 base_url: impl Into<String>,
62 http_client: HttpClient,
63 ) -> Self {
64 TokenManager {
65 api_key: api_key.into(),
66 secret: secret.into(),
67 base_url: base_url.into(),
68 http_client,
69 state: Arc::new(RwLock::new(TokenState::default())),
70 }
71 }
72
73 pub async fn token(&self) -> Result<String> {
77 {
79 let state = self.state.read().await;
80 if !state.token.is_empty() && !self.is_expired_locked(&state) {
81 return Ok(state.token.clone());
82 }
83 }
84
85 self.refresh_and_get_token().await
87 }
88
89 pub async fn refresh(&self) -> Result<()> {
91 let mut state = self.state.write().await;
92 self.refresh_locked(&mut state).await
93 }
94
95 async fn refresh_and_get_token(&self) -> Result<String> {
97 let mut state = self.state.write().await;
98
99 if !state.token.is_empty() && !self.is_expired_locked(&state) {
101 return Ok(state.token.clone());
102 }
103
104 self.refresh_locked(&mut state).await?;
105 Ok(state.token.clone())
106 }
107
108 async fn refresh_locked(&self, state: &mut TokenState) -> Result<()> {
110 let auth_url = format!("{}{}", self.base_url, AUTH_ENDPOINT);
111
112 let req_body = AuthRequest {
113 secret: self.secret.clone(),
114 };
115
116 let response = self
117 .http_client
118 .post(&auth_url)
119 .header("Content-Type", "application/json")
120 .header("apikey", &self.api_key)
121 .json(&req_body)
122 .send()
123 .await
124 .map_err(|e| Error::auth(format!("failed to send auth request: {}", e)))?;
125
126 let resp_text = response
127 .text()
128 .await
129 .map_err(|e| Error::auth(format!("failed to read auth response: {}", e)))?;
130
131 let api_resp: ApiResponse = serde_json::from_str(&resp_text)
132 .map_err(|e| Error::auth(format!("failed to parse auth response: {}, body: {}", e, resp_text)))?;
133
134 if api_resp.code != ErrorCode::Success as i32 {
135 return Err(self.handle_auth_error(api_resp.code, &api_resp.msg));
136 }
137
138 let token_resp: TokenResponse = serde_json::from_value(api_resp.data)
139 .map_err(|e| Error::auth(format!("failed to parse token data: {}", e)))?;
140
141 if token_resp.access_token.is_empty() {
142 return Err(Error::auth("received empty access token"));
143 }
144
145 state.token = token_resp.access_token;
147 let expires_in = if token_resp.expires_in > 0 {
148 token_resp.expires_in as u64
149 } else {
150 TOKEN_EXPIRY_SECONDS
151 };
152 let effective_expiry = expires_in.saturating_sub(TOKEN_EXPIRY_BUFFER);
154 state.expires_at = Some(Instant::now() + Duration::from_secs(effective_expiry));
155
156 Ok(())
157 }
158
159 fn handle_auth_error(&self, code: i32, message: &str) -> Error {
161 match ErrorCode::from_code(code) {
162 Some(ErrorCode::ParamError) => Error::auth(format!("invalid parameters: {}", message)),
163 Some(ErrorCode::NoPermission) => Error::auth(format!("permission denied: {}", message)),
164 Some(ErrorCode::ServerError) => Error::auth(format!("server error: {}", message)),
165 Some(ErrorCode::RateLimit) => Error::auth(format!("rate limited: {}", message)),
166 _ => Error::auth(format!("authentication failed (code {}): {}", code, message)),
167 }
168 }
169
170 fn is_expired_locked(&self, state: &TokenState) -> bool {
172 if state.token.is_empty() {
173 return true;
174 }
175 match state.expires_at {
176 Some(expires_at) => Instant::now() >= expires_at,
177 None => true,
178 }
179 }
180
181 pub async fn is_expired(&self) -> bool {
183 let state = self.state.read().await;
184 self.is_expired_locked(&state)
185 }
186
187 pub async fn clear_token(&self) {
189 let mut state = self.state.write().await;
190 state.token.clear();
191 state.expires_at = None;
192 }
193
194 pub async fn get_cached_token(&self) -> String {
196 let state = self.state.read().await;
197 state.token.clone()
198 }
199}