1use crate::types::{AuthError, Credentials, DeviceCodeResponse, DeviceCodeStatus};
7use crate::DEFAULT_AUTH_URL;
8use chrono::Utc;
9use std::time::Duration;
10
11pub struct AuthService {
13 auth_url: String,
15 client: reqwest::Client,
17}
18
19impl AuthService {
20 pub fn new() -> Self {
22 Self::with_auth_url(DEFAULT_AUTH_URL.to_string())
23 }
24
25 pub fn with_auth_url(auth_url: String) -> Self {
27 let client = reqwest::Client::builder()
28 .timeout(Duration::from_secs(30))
29 .build()
30 .expect("Failed to create HTTP client");
31
32 Self { auth_url, client }
33 }
34
35 pub fn auth_url(&self) -> &str {
37 &self.auth_url
38 }
39
40 pub async fn request_device_code(&self) -> Result<DeviceCodeResponse, AuthError> {
44 let url = format!("{}/device/code", self.auth_url);
45
46 let response = self
47 .client
48 .post(&url)
49 .header("Content-Type", "application/json")
50 .send()
51 .await
52 .map_err(|e| AuthError::NetworkError { message: e.to_string() })?;
53
54 let status = response.status();
55
56 if status.is_success() {
57 response.json().await.map_err(|e| AuthError::ServerError {
58 message: format!("Failed to parse response: {}", e),
59 status_code: Some(status.as_u16()),
60 })
61 } else if status.as_u16() == 429 {
62 let retry_after = response
63 .headers()
64 .get("retry-after")
65 .and_then(|v| v.to_str().ok())
66 .and_then(|s| s.parse().ok());
67
68 Err(AuthError::RateLimited { retry_after })
69 } else {
70 let message = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
71
72 Err(AuthError::ServerError {
73 message,
74 status_code: Some(status.as_u16()),
75 })
76 }
77 }
78
79 pub async fn poll_device_code(&self, device_code: &str) -> Result<DeviceCodeStatus, AuthError> {
83 let url = format!("{}/device/code/{}/status", self.auth_url, device_code);
84
85 let response = self
86 .client
87 .get(&url)
88 .send()
89 .await
90 .map_err(|e| AuthError::NetworkError { message: e.to_string() })?;
91
92 let status = response.status();
93
94 if status.is_success() {
95 response.json().await.map_err(|e| AuthError::ServerError {
96 message: format!("Failed to parse status: {}", e),
97 status_code: Some(status.as_u16()),
98 })
99 } else if status.as_u16() == 429 {
100 let retry_after = response
101 .headers()
102 .get("retry-after")
103 .and_then(|v| v.to_str().ok())
104 .and_then(|s| s.parse().ok());
105
106 Err(AuthError::RateLimited { retry_after })
107 } else if status.as_u16() == 404 {
108 Err(AuthError::ExpiredCode)
109 } else {
110 let message = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
111
112 Err(AuthError::ServerError {
113 message,
114 status_code: Some(status.as_u16()),
115 })
116 }
117 }
118
119 pub async fn run_device_code_flow<F>(&self, on_device_code: F) -> Result<Credentials, AuthError>
128 where
129 F: FnOnce(&DeviceCodeResponse),
130 {
131 let device_code_response = self.request_device_code().await?;
132 let poll_interval = Duration::from_secs(device_code_response.interval as u64);
133 let expires_at = std::time::Instant::now() + Duration::from_secs(device_code_response.expires_in as u64);
134
135 on_device_code(&device_code_response);
136
137 loop {
138 if std::time::Instant::now() > expires_at {
139 return Err(AuthError::ExpiredCode);
140 }
141
142 tokio::time::sleep(poll_interval).await;
143
144 match self.poll_device_code(&device_code_response.device_code).await {
145 Ok(DeviceCodeStatus::Pending) => {
146 continue;
147 }
148 Ok(DeviceCodeStatus::Authorized {
149 api_key,
150 user_id,
151 email,
152 name,
153 }) => {
154 return Ok(Credentials {
155 api_key,
156 user_id,
157 email,
158 name,
159 authenticated_at: Utc::now(),
160 auth_url: self.auth_url.clone(),
161 });
162 }
163 Ok(DeviceCodeStatus::Denied) => {
164 return Err(AuthError::AccessDenied);
165 }
166 Ok(DeviceCodeStatus::Expired) => {
167 return Err(AuthError::ExpiredCode);
168 }
169 Err(AuthError::RateLimited { retry_after }) => {
170 let wait_time = retry_after.map(|s| s as u64).unwrap_or(10);
171 tokio::time::sleep(Duration::from_secs(wait_time)).await;
172 continue;
173 }
174 Err(e) => {
175 return Err(e);
176 }
177 }
178 }
179 }
180}
181
182impl Default for AuthService {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_auth_service_creation() {
194 let service = AuthService::new();
195 assert_eq!(service.auth_url(), DEFAULT_AUTH_URL);
196 }
197
198 #[test]
199 fn test_custom_auth_url() {
200 let custom_url = "https://custom.auth.example.com";
201 let service = AuthService::with_auth_url(custom_url.to_string());
202 assert_eq!(service.auth_url(), custom_url);
203 }
204}