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