1use crate::errors::WOWSQLError;
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::time::Duration;
6
7#[derive(Debug, Clone)]
10pub struct ProjectAuthConfig {
11 pub project_url: String,
12 pub base_domain: String,
13 pub secure: bool,
14 pub timeout_seconds: u64,
15 pub api_key: Option<String>,
19 pub public_api_key: Option<String>,
21}
22
23impl Default for ProjectAuthConfig {
24 fn default() -> Self {
25 Self {
26 project_url: String::new(),
27 base_domain: "wowsql.com".to_string(),
28 secure: true,
29 timeout_seconds: 30,
30 api_key: None,
31 public_api_key: None,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AuthUser {
39 pub id: String,
40 pub email: String,
41 #[serde(rename = "full_name")]
42 pub full_name: Option<String>,
43 #[serde(rename = "avatar_url")]
44 pub avatar_url: Option<String>,
45 #[serde(rename = "email_verified")]
46 pub email_verified: bool,
47 #[serde(rename = "user_metadata")]
48 pub user_metadata: Value,
49 #[serde(rename = "app_metadata")]
50 pub app_metadata: Value,
51 #[serde(rename = "created_at")]
52 pub created_at: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct AuthSession {
58 #[serde(rename = "access_token")]
59 pub access_token: String,
60 #[serde(rename = "refresh_token")]
61 pub refresh_token: String,
62 #[serde(rename = "token_type")]
63 pub token_type: String,
64 #[serde(rename = "expires_in")]
65 pub expires_in: i32,
66}
67
68#[derive(Debug, Clone)]
70pub struct AuthResult {
71 pub user: Option<AuthUser>,
72 pub session: AuthSession,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct OAuthAuthorizationResponse {
78 #[serde(rename = "authorization_url")]
79 pub authorization_url: String,
80 pub provider: String,
81 #[serde(rename = "redirect_uri")]
82 pub redirect_uri: String,
83 #[serde(rename = "backend_callback_url")]
84 pub backend_callback_url: Option<String>,
85 #[serde(rename = "frontend_redirect_uri")]
86 pub frontend_redirect_uri: Option<String>,
87}
88
89#[derive(Debug, Clone)]
91pub struct SignUpRequest {
92 pub email: String,
93 pub password: String,
94 pub full_name: Option<String>,
95 pub user_metadata: Option<Value>,
96}
97
98#[derive(Debug, Clone)]
100pub struct SignInRequest {
101 pub email: String,
102 pub password: String,
103}
104
105pub struct ProjectAuthClient {
107 config: ProjectAuthConfig,
108 client: Client,
109 access_token: Option<String>,
110 refresh_token: Option<String>,
111}
112
113impl ProjectAuthClient {
114 pub fn new(config: ProjectAuthConfig) -> Result<Self, WOWSQLError> {
116 let client = Client::builder()
117 .timeout(Duration::from_secs(config.timeout_seconds))
118 .build()
119 .map_err(|e| WOWSQLError::Network(format!("Failed to create HTTP client: {}", e)))?;
120
121 Ok(Self {
122 config,
123 client,
124 access_token: None,
125 refresh_token: None,
126 })
127 }
128
129 pub async fn sign_up(&mut self, request: SignUpRequest) -> Result<AuthResult, WOWSQLError> {
131 let url = self.build_auth_url("/signup");
132 let mut body = serde_json::json!({
133 "email": request.email,
134 "password": request.password
135 });
136
137 if let Some(full_name) = request.full_name {
138 body["full_name"] = Value::String(full_name);
139 }
140
141 if let Some(metadata) = request.user_metadata {
142 body["user_metadata"] = metadata;
143 }
144
145 let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
146 let session = self.parse_session(&response)?;
147 self.persist_session(&session);
148
149 let user = self.parse_user(response.get("user"));
150 Ok(AuthResult { user, session })
151 }
152
153 pub async fn sign_in(&mut self, request: SignInRequest) -> Result<AuthResult, WOWSQLError> {
155 let url = self.build_auth_url("/login");
156 let body = serde_json::json!({
157 "email": request.email,
158 "password": request.password
159 });
160
161 let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
162 let session = self.parse_session(&response)?;
163 self.persist_session(&session);
164
165 Ok(AuthResult {
166 user: None,
167 session,
168 })
169 }
170
171 pub async fn get_user(&self, token_override: Option<&str>) -> Result<AuthUser, WOWSQLError> {
173 let token = token_override
174 .or(self.access_token.as_deref())
175 .ok_or_else(|| {
176 WOWSQLError::Authentication(
177 "Access token is required. Call sign_in first.".to_string(),
178 )
179 })?;
180
181 let url = self.build_auth_url("/me");
182 let mut request = self
183 .client
184 .get(&url)
185 .header("Content-Type", "application/json")
186 .header("Authorization", format!("Bearer {}", token));
187 request = self.apply_api_key_to_request(request);
188 let response: Value = request
189 .send()
190 .await
191 .map_err(|e| WOWSQLError::Network(format!("Request failed: {}", e)))?
192 .json()
193 .await
194 .map_err(|e| WOWSQLError::Network(format!("Failed to parse response: {}", e)))?;
195
196 self.parse_user(Some(&response))
197 .ok_or_else(|| WOWSQLError::Authentication("Invalid user response".to_string()))
198 }
199
200 pub async fn get_oauth_authorization_url(
202 &self,
203 provider: &str,
204 redirect_uri: &str,
205 ) -> Result<OAuthAuthorizationResponse, WOWSQLError> {
206 let encoded_uri = urlencoding::encode(redirect_uri);
207 let url = format!(
208 "{}?frontend_redirect_uri={}",
209 self.build_auth_url(&format!("/oauth/{}", provider)),
210 encoded_uri
211 );
212
213 let response: Value = self.execute_request(&url, "GET", None).await?;
214 serde_json::from_value(response)
215 .map_err(|e| WOWSQLError::General(format!("Failed to parse OAuth response: {}", e)))
216 }
217
218 pub async fn exchange_oauth_callback(
220 &mut self,
221 provider: &str,
222 code: &str,
223 redirect_uri: Option<&str>,
224 ) -> Result<AuthResult, WOWSQLError> {
225 let url = self.build_auth_url(&format!("/oauth/{}/callback", provider));
226 let mut body = serde_json::json!({ "code": code });
227
228 if let Some(redirect_uri) = redirect_uri {
229 body["redirect_uri"] = Value::String(redirect_uri.to_string());
230 }
231
232 let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
233 let session = self.parse_session(&response)?;
234 self.persist_session(&session);
235
236 let user = self.parse_user(response.get("user"));
237 Ok(AuthResult { user, session })
238 }
239
240 pub async fn forgot_password(&self, email: &str) -> Result<Value, WOWSQLError> {
242 let url = self.build_auth_url("/forgot-password");
243 let body = serde_json::json!({ "email": email });
244 self.execute_request(&url, "POST", Some(body)).await
245 }
246
247 pub async fn reset_password(
249 &self,
250 token: &str,
251 new_password: &str,
252 ) -> Result<Value, WOWSQLError> {
253 let url = self.build_auth_url("/reset-password");
254 let body = serde_json::json!({
255 "token": token,
256 "new_password": new_password
257 });
258 self.execute_request(&url, "POST", Some(body)).await
259 }
260
261 pub fn set_session(&mut self, access_token: String, refresh_token: Option<String>) {
263 self.access_token = Some(access_token);
264 self.refresh_token = refresh_token;
265 }
266
267 pub fn clear_session(&mut self) {
269 self.access_token = None;
270 self.refresh_token = None;
271 }
272
273 pub fn get_session(&self) -> Option<AuthSession> {
275 self.access_token.as_ref().map(|token| AuthSession {
276 access_token: token.clone(),
277 refresh_token: self.refresh_token.clone().unwrap_or_default(),
278 token_type: "bearer".to_string(),
279 expires_in: 0,
280 })
281 }
282
283 pub async fn send_otp(&self, email: &str, purpose: &str) -> Result<Value, WOWSQLError> {
286 if purpose != "login" && purpose != "signup" && purpose != "password_reset" {
287 return Err(WOWSQLError::General(
288 "Purpose must be 'login', 'signup', or 'password_reset'".to_string(),
289 ));
290 }
291
292 let url = self.build_auth_url("/otp/send");
293 let body = serde_json::json!({
294 "email": email,
295 "purpose": purpose
296 });
297 self.execute_request(&url, "POST", Some(body)).await
298 }
299
300 pub async fn verify_otp(
305 &mut self,
306 email: &str,
307 otp: &str,
308 purpose: &str,
309 new_password: Option<&str>,
310 ) -> Result<AuthResult, WOWSQLError> {
311 if purpose != "login" && purpose != "signup" && purpose != "password_reset" {
312 return Err(WOWSQLError::General(
313 "Purpose must be 'login', 'signup', or 'password_reset'".to_string(),
314 ));
315 }
316
317 if purpose == "password_reset" && new_password.is_none() {
318 return Err(WOWSQLError::General(
319 "new_password is required for password_reset purpose".to_string(),
320 ));
321 }
322
323 let url = self.build_auth_url("/otp/verify");
324 let mut body = serde_json::json!({
325 "email": email,
326 "otp": otp,
327 "purpose": purpose
328 });
329
330 if let Some(new_password) = new_password {
331 body["new_password"] = Value::String(new_password.to_string());
332 }
333
334 let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
335
336 if purpose == "password_reset" {
337 return Ok(AuthResult {
338 user: None,
339 session: AuthSession {
340 access_token: String::new(),
341 refresh_token: String::new(),
342 token_type: "bearer".to_string(),
343 expires_in: 0,
344 },
345 });
346 }
347
348 let session = self.parse_session(&response)?;
349 self.persist_session(&session);
350 let user = self.parse_user(response.get("user"));
351 Ok(AuthResult { user, session })
352 }
353
354 pub async fn send_magic_link(&self, email: &str, purpose: &str) -> Result<Value, WOWSQLError> {
357 if purpose != "login" && purpose != "signup" && purpose != "email_verification" {
358 return Err(WOWSQLError::General(
359 "Purpose must be 'login', 'signup', or 'email_verification'".to_string(),
360 ));
361 }
362
363 let url = self.build_auth_url("/magic-link/send");
364 let body = serde_json::json!({
365 "email": email,
366 "purpose": purpose
367 });
368 self.execute_request(&url, "POST", Some(body)).await
369 }
370
371 pub async fn verify_email(&self, token: &str) -> Result<Value, WOWSQLError> {
373 let url = self.build_auth_url("/verify-email");
374 let body = serde_json::json!({ "token": token });
375 self.execute_request(&url, "POST", Some(body)).await
376 }
377
378 pub async fn resend_verification(&self, email: &str) -> Result<Value, WOWSQLError> {
381 let url = self.build_auth_url("/resend-verification");
382 let body = serde_json::json!({ "email": email });
383 self.execute_request(&url, "POST", Some(body)).await
384 }
385
386 fn build_auth_url(&self, path: &str) -> String {
389 let mut normalized = self.config.project_url.trim().to_string();
390
391 if normalized.starts_with("http://") || normalized.starts_with("https://") {
393 normalized = normalized.trim_end_matches('/').to_string();
394 if normalized.ends_with("/api") {
395 normalized = normalized.trim_end_matches("/api").to_string();
396 }
397 return format!("{}/api/auth{}", normalized, path);
398 }
399
400 let protocol = if self.config.secure { "https" } else { "http" };
402 if normalized.contains(&format!(".{}", self.config.base_domain))
403 || normalized.ends_with(&self.config.base_domain)
404 {
405 normalized = format!("{}://{}", protocol, normalized);
406 } else {
407 normalized = format!("{}://{}.{}", protocol, normalized, self.config.base_domain);
408 }
409
410 normalized = normalized.trim_end_matches('/').to_string();
411 if normalized.ends_with("/api") {
412 normalized = normalized.trim_end_matches("/api").to_string();
413 }
414
415 format!("{}/api/auth{}", normalized, path)
416 }
417
418 async fn execute_request(
419 &self,
420 url: &str,
421 method: &str,
422 body: Option<Value>,
423 ) -> Result<Value, WOWSQLError> {
424 let mut request = self
425 .client
426 .request(
427 method
428 .parse()
429 .map_err(|_| WOWSQLError::General("Invalid method".to_string()))?,
430 url,
431 )
432 .header("Content-Type", "application/json");
433 request = self.apply_api_key_to_request(request);
434
435 if let Some(body) = body {
436 request = request.json(&body);
437 }
438
439 let response = request
440 .send()
441 .await
442 .map_err(|e| WOWSQLError::Network(format!("Request failed: {}", e)))?;
443
444 let status = response.status();
445 let text = response
446 .text()
447 .await
448 .map_err(|e| WOWSQLError::Network(format!("Failed to read response: {}", e)))?;
449
450 if !status.is_success() {
451 return Err(self.handle_error(status.as_u16(), &text));
452 }
453
454 serde_json::from_str(&text)
455 .map_err(|e| WOWSQLError::General(format!("Failed to parse response: {}", e)))
456 }
457
458 fn handle_error(&self, status_code: u16, body: &str) -> WOWSQLError {
459 let error_response: Value = serde_json::from_str(body).unwrap_or(Value::Null);
460
461 let message = error_response
462 .get("detail")
463 .and_then(|v| v.as_str())
464 .or_else(|| error_response.get("message").and_then(|v| v.as_str()))
465 .or_else(|| error_response.get("error").and_then(|v| v.as_str()))
466 .unwrap_or(&format!("Request failed with status {}", status_code))
467 .to_string();
468
469 match status_code {
470 401 | 403 => WOWSQLError::Authentication(message),
471 404 => WOWSQLError::NotFound(message),
472 429 => WOWSQLError::RateLimit(message),
473 _ => WOWSQLError::General(message),
474 }
475 }
476
477 fn parse_session(&self, response: &Value) -> Result<AuthSession, WOWSQLError> {
478 serde_json::from_value(response.clone())
479 .map_err(|e| WOWSQLError::Authentication(format!("Invalid session response: {}", e)))
480 }
481
482 fn parse_user(&self, user_value: Option<&Value>) -> Option<AuthUser> {
483 user_value.and_then(|v| serde_json::from_value(v.clone()).ok())
484 }
485
486 fn persist_session(&mut self, session: &AuthSession) {
487 self.access_token = Some(session.access_token.clone());
488 self.refresh_token = Some(session.refresh_token.clone());
489 }
490}
491
492impl ProjectAuthClient {
494 fn apply_api_key_to_request(
495 &self,
496 mut request: reqwest::RequestBuilder,
497 ) -> reqwest::RequestBuilder {
498 let unified_key = self
500 .config
501 .api_key
502 .as_ref()
503 .or(self.config.public_api_key.as_ref());
504 if let Some(api_key) = unified_key {
505 request = request.header("Authorization", format!("Bearer {}", api_key));
507 }
508 request
509 }
510}