use crate::errors::WOWSQLError;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ProjectAuthConfig {
pub project_url: String,
pub base_domain: String,
pub secure: bool,
pub timeout_seconds: u64,
pub api_key: Option<String>,
pub public_api_key: Option<String>,
}
impl Default for ProjectAuthConfig {
fn default() -> Self {
Self {
project_url: String::new(),
base_domain: "wowsql.com".to_string(),
secure: true,
timeout_seconds: 30,
api_key: None,
public_api_key: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthUser {
pub id: String,
pub email: String,
#[serde(rename = "full_name")]
pub full_name: Option<String>,
#[serde(rename = "avatar_url")]
pub avatar_url: Option<String>,
#[serde(rename = "email_verified")]
pub email_verified: bool,
#[serde(rename = "user_metadata")]
pub user_metadata: Value,
#[serde(rename = "app_metadata")]
pub app_metadata: Value,
#[serde(rename = "created_at")]
pub created_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthSession {
#[serde(rename = "access_token")]
pub access_token: String,
#[serde(rename = "refresh_token")]
pub refresh_token: String,
#[serde(rename = "token_type")]
pub token_type: String,
#[serde(rename = "expires_in")]
pub expires_in: i32,
}
#[derive(Debug, Clone)]
pub struct AuthResult {
pub user: Option<AuthUser>,
pub session: AuthSession,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthAuthorizationResponse {
#[serde(rename = "authorization_url")]
pub authorization_url: String,
pub provider: String,
#[serde(rename = "redirect_uri")]
pub redirect_uri: String,
#[serde(rename = "backend_callback_url")]
pub backend_callback_url: Option<String>,
#[serde(rename = "frontend_redirect_uri")]
pub frontend_redirect_uri: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SignUpRequest {
pub email: String,
pub password: String,
pub full_name: Option<String>,
pub user_metadata: Option<Value>,
}
#[derive(Debug, Clone)]
pub struct SignInRequest {
pub email: String,
pub password: String,
}
pub struct ProjectAuthClient {
config: ProjectAuthConfig,
client: Client,
access_token: Option<String>,
refresh_token: Option<String>,
}
impl ProjectAuthClient {
pub fn new(config: ProjectAuthConfig) -> Result<Self, WOWSQLError> {
let client = Client::builder()
.timeout(Duration::from_secs(config.timeout_seconds))
.build()
.map_err(|e| WOWSQLError::Network(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
config,
client,
access_token: None,
refresh_token: None,
})
}
pub async fn sign_up(&mut self, request: SignUpRequest) -> Result<AuthResult, WOWSQLError> {
let url = self.build_auth_url("/signup");
let mut body = serde_json::json!({
"email": request.email,
"password": request.password
});
if let Some(full_name) = request.full_name {
body["full_name"] = Value::String(full_name);
}
if let Some(metadata) = request.user_metadata {
body["user_metadata"] = metadata;
}
let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
let session = self.parse_session(&response)?;
self.persist_session(&session);
let user = self.parse_user(response.get("user"));
Ok(AuthResult { user, session })
}
pub async fn sign_in(&mut self, request: SignInRequest) -> Result<AuthResult, WOWSQLError> {
let url = self.build_auth_url("/login");
let body = serde_json::json!({
"email": request.email,
"password": request.password
});
let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
let session = self.parse_session(&response)?;
self.persist_session(&session);
Ok(AuthResult {
user: None,
session,
})
}
pub async fn get_user(&self, token_override: Option<&str>) -> Result<AuthUser, WOWSQLError> {
let token = token_override
.or(self.access_token.as_deref())
.ok_or_else(|| {
WOWSQLError::Authentication(
"Access token is required. Call sign_in first.".to_string(),
)
})?;
let url = self.build_auth_url("/me");
let mut request = self
.client
.get(&url)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", token));
request = self.apply_api_key_to_request(request);
let response: Value = request
.send()
.await
.map_err(|e| WOWSQLError::Network(format!("Request failed: {}", e)))?
.json()
.await
.map_err(|e| WOWSQLError::Network(format!("Failed to parse response: {}", e)))?;
self.parse_user(Some(&response))
.ok_or_else(|| WOWSQLError::Authentication("Invalid user response".to_string()))
}
pub async fn get_oauth_authorization_url(
&self,
provider: &str,
redirect_uri: &str,
) -> Result<OAuthAuthorizationResponse, WOWSQLError> {
let encoded_uri = urlencoding::encode(redirect_uri);
let url = format!(
"{}?frontend_redirect_uri={}",
self.build_auth_url(&format!("/oauth/{}", provider)),
encoded_uri
);
let response: Value = self.execute_request(&url, "GET", None).await?;
serde_json::from_value(response)
.map_err(|e| WOWSQLError::General(format!("Failed to parse OAuth response: {}", e)))
}
pub async fn exchange_oauth_callback(
&mut self,
provider: &str,
code: &str,
redirect_uri: Option<&str>,
) -> Result<AuthResult, WOWSQLError> {
let url = self.build_auth_url(&format!("/oauth/{}/callback", provider));
let mut body = serde_json::json!({ "code": code });
if let Some(redirect_uri) = redirect_uri {
body["redirect_uri"] = Value::String(redirect_uri.to_string());
}
let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
let session = self.parse_session(&response)?;
self.persist_session(&session);
let user = self.parse_user(response.get("user"));
Ok(AuthResult { user, session })
}
pub async fn forgot_password(&self, email: &str) -> Result<Value, WOWSQLError> {
let url = self.build_auth_url("/forgot-password");
let body = serde_json::json!({ "email": email });
self.execute_request(&url, "POST", Some(body)).await
}
pub async fn reset_password(
&self,
token: &str,
new_password: &str,
) -> Result<Value, WOWSQLError> {
let url = self.build_auth_url("/reset-password");
let body = serde_json::json!({
"token": token,
"new_password": new_password
});
self.execute_request(&url, "POST", Some(body)).await
}
pub fn set_session(&mut self, access_token: String, refresh_token: Option<String>) {
self.access_token = Some(access_token);
self.refresh_token = refresh_token;
}
pub fn clear_session(&mut self) {
self.access_token = None;
self.refresh_token = None;
}
pub fn get_session(&self) -> Option<AuthSession> {
self.access_token.as_ref().map(|token| AuthSession {
access_token: token.clone(),
refresh_token: self.refresh_token.clone().unwrap_or_default(),
token_type: "bearer".to_string(),
expires_in: 0,
})
}
fn build_auth_url(&self, path: &str) -> String {
let mut normalized = self.config.project_url.trim().to_string();
if normalized.starts_with("http://") || normalized.starts_with("https://") {
normalized = normalized.trim_end_matches('/').to_string();
if normalized.ends_with("/api") {
normalized = normalized.trim_end_matches("/api").to_string();
}
return format!("{}/api/auth{}", normalized, path);
}
let protocol = if self.config.secure { "https" } else { "http" };
if normalized.contains(&format!(".{}", self.config.base_domain))
|| normalized.ends_with(&self.config.base_domain)
{
normalized = format!("{}://{}", protocol, normalized);
} else {
normalized = format!("{}://{}.{}", protocol, normalized, self.config.base_domain);
}
normalized = normalized.trim_end_matches('/').to_string();
if normalized.ends_with("/api") {
normalized = normalized.trim_end_matches("/api").to_string();
}
format!("{}/api/auth{}", normalized, path)
}
async fn execute_request(
&self,
url: &str,
method: &str,
body: Option<Value>,
) -> Result<Value, WOWSQLError> {
let mut request = self
.client
.request(
method
.parse()
.map_err(|_| WOWSQLError::General("Invalid method".to_string()))?,
url,
)
.header("Content-Type", "application/json");
request = self.apply_api_key_to_request(request);
if let Some(body) = body {
request = request.json(&body);
}
let response = request
.send()
.await
.map_err(|e| WOWSQLError::Network(format!("Request failed: {}", e)))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|e| WOWSQLError::Network(format!("Failed to read response: {}", e)))?;
if !status.is_success() {
return Err(self.handle_error(status.as_u16(), &text));
}
serde_json::from_str(&text)
.map_err(|e| WOWSQLError::General(format!("Failed to parse response: {}", e)))
}
fn handle_error(&self, status_code: u16, body: &str) -> WOWSQLError {
let error_response: Value = serde_json::from_str(body).unwrap_or(Value::Null);
let message = error_response
.get("detail")
.and_then(|v| v.as_str())
.or_else(|| error_response.get("message").and_then(|v| v.as_str()))
.or_else(|| error_response.get("error").and_then(|v| v.as_str()))
.unwrap_or(&format!("Request failed with status {}", status_code))
.to_string();
match status_code {
401 | 403 => WOWSQLError::Authentication(message),
404 => WOWSQLError::NotFound(message),
429 => WOWSQLError::RateLimit(message),
_ => WOWSQLError::General(message),
}
}
fn parse_session(&self, response: &Value) -> Result<AuthSession, WOWSQLError> {
serde_json::from_value(response.clone())
.map_err(|e| WOWSQLError::Authentication(format!("Invalid session response: {}", e)))
}
fn parse_user(&self, user_value: Option<&Value>) -> Option<AuthUser> {
user_value.and_then(|v| serde_json::from_value(v.clone()).ok())
}
fn persist_session(&mut self, session: &AuthSession) {
self.access_token = Some(session.access_token.clone());
self.refresh_token = Some(session.refresh_token.clone());
}
}
impl ProjectAuthClient {
fn apply_api_key_to_request(
&self,
mut request: reqwest::RequestBuilder,
) -> reqwest::RequestBuilder {
let unified_key = self
.config
.api_key
.as_ref()
.or(self.config.public_api_key.as_ref());
if let Some(api_key) = unified_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
request
}
}