supabase/
auth.rs

1//! Authentication module for Supabase client
2//!
3//! This module provides comprehensive authentication functionality including:
4//! - Email/password authentication
5//! - OAuth providers (Google, GitHub, Discord, Apple, etc.)
6//! - Phone/SMS authentication
7//! - Magic link authentication
8//! - Anonymous authentication
9//! - Session management and token refresh
10//! - Auth state change events
11
12use crate::{
13    error::{Error, Result},
14    types::{SupabaseConfig, Timestamp},
15};
16use chrono::Utc;
17use reqwest::Client as HttpClient;
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use std::sync::{Arc, RwLock, Weak};
21use tracing::{debug, info, warn};
22use uuid::Uuid;
23
24// MFA imports
25use base32;
26use phonenumber::Mode;
27use qrcode::QrCode;
28use totp_rs::{Algorithm, TOTP};
29
30/// Authentication state event types
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum AuthEvent {
33    /// User signed in
34    SignedIn,
35    /// User signed out
36    SignedOut,
37    /// Session refreshed
38    TokenRefreshed,
39    /// User information updated
40    UserUpdated,
41    /// Password reset initiated
42    PasswordReset,
43    /// MFA challenge required
44    MfaChallengeRequired,
45    /// MFA challenge completed
46    MfaChallengeCompleted,
47    /// MFA enabled for user
48    MfaEnabled,
49    /// MFA disabled for user
50    MfaDisabled,
51}
52
53/// Multi-factor authentication method types
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub enum MfaMethod {
56    /// Time-based One-Time Password (TOTP) - Google Authenticator, Authy, etc.
57    #[serde(rename = "totp")]
58    Totp,
59    /// SMS-based verification
60    #[serde(rename = "sms")]
61    Sms,
62    /// Email-based verification (future)
63    #[serde(rename = "email")]
64    Email,
65}
66
67impl std::fmt::Display for MfaMethod {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            MfaMethod::Totp => write!(f, "TOTP"),
71            MfaMethod::Sms => write!(f, "SMS"),
72            MfaMethod::Email => write!(f, "Email"),
73        }
74    }
75}
76
77/// MFA challenge status
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub enum MfaChallengeStatus {
80    /// Challenge is pending user input
81    #[serde(rename = "pending")]
82    Pending,
83    /// Challenge was completed successfully
84    #[serde(rename = "completed")]
85    Completed,
86    /// Challenge expired
87    #[serde(rename = "expired")]
88    Expired,
89    /// Challenge was cancelled
90    #[serde(rename = "cancelled")]
91    Cancelled,
92}
93
94/// MFA factor configuration
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct MfaFactor {
97    pub id: Uuid,
98    pub factor_type: MfaMethod,
99    pub friendly_name: String,
100    pub status: String, // "verified", "unverified"
101    pub created_at: Timestamp,
102    pub updated_at: Timestamp,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub phone: Option<String>,
105}
106
107/// TOTP setup response containing QR code data
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct TotpSetupResponse {
110    pub secret: String,
111    pub qr_code: String,
112    pub uri: String,
113    pub factor_id: Uuid,
114}
115
116/// MFA challenge information
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct MfaChallenge {
119    pub id: Uuid,
120    pub factor_id: Uuid,
121    pub status: MfaChallengeStatus,
122    pub challenge_type: MfaMethod,
123    pub expires_at: Timestamp,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub masked_phone: Option<String>,
126}
127
128/// MFA verification request
129#[derive(Debug, Serialize)]
130pub struct MfaVerificationRequest {
131    pub factor_id: Uuid,
132    pub challenge_id: Uuid,
133    pub code: String,
134}
135
136/// Enhanced phone number with country code support
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct EnhancedPhoneNumber {
139    pub raw: String,
140    pub formatted: String,
141    pub country_code: String,
142    pub national_number: String,
143    pub is_valid: bool,
144}
145
146impl EnhancedPhoneNumber {
147    /// Create enhanced phone number from raw input
148    pub fn new(phone: &str, _default_region: Option<&str>) -> Result<Self> {
149        // For now, let's use a simplified approach
150        // In production, this should be properly implemented with phonenumber crate
151        let parsed = phonenumber::parse(None, phone)
152            .map_err(|e| Error::auth(format!("Invalid phone number: {}", e)))?;
153
154        let formatted = phonenumber::format(&parsed)
155            .mode(Mode::International)
156            .to_string();
157
158        // Extract basic info
159        let country_code = parsed.code().value().to_string();
160        let national_number = parsed.national().to_string();
161
162        Ok(Self {
163            raw: phone.to_string(),
164            formatted,
165            country_code,
166            national_number,
167            is_valid: phonenumber::is_valid(&parsed),
168        })
169    }
170}
171
172/// OAuth token metadata for advanced management
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TokenMetadata {
175    pub issued_at: Timestamp,
176    pub expires_at: Timestamp,
177    pub refresh_count: u32,
178    pub last_refresh_at: Option<Timestamp>,
179    pub scopes: Vec<String>,
180    pub device_id: Option<String>,
181}
182
183/// Enhanced session with MFA and advanced token info
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct EnhancedSession {
186    pub access_token: String,
187    pub refresh_token: String,
188    pub expires_in: i64,
189    pub expires_at: Timestamp,
190    pub token_type: String,
191    pub user: User,
192    pub token_metadata: Option<TokenMetadata>,
193    pub mfa_verified: bool,
194    pub active_factors: Vec<MfaFactor>,
195}
196
197/// Authentication state change callback
198pub type AuthStateCallback = Box<dyn Fn(AuthEvent, Option<Session>) + Send + Sync + 'static>;
199
200/// Authentication event listener handle
201#[derive(Debug, Clone)]
202pub struct AuthEventHandle {
203    id: Uuid,
204    auth: Weak<Auth>,
205}
206
207impl AuthEventHandle {
208    /// Remove this event listener
209    pub fn remove(&self) {
210        if let Some(auth) = self.auth.upgrade() {
211            auth.remove_auth_listener(self.id);
212        }
213    }
214}
215
216/// Supported OAuth providers
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub enum OAuthProvider {
219    /// Google OAuth
220    #[serde(rename = "google")]
221    Google,
222    /// GitHub OAuth
223    #[serde(rename = "github")]
224    GitHub,
225    /// Discord OAuth
226    #[serde(rename = "discord")]
227    Discord,
228    /// Apple OAuth
229    #[serde(rename = "apple")]
230    Apple,
231    /// Twitter OAuth
232    #[serde(rename = "twitter")]
233    Twitter,
234    /// Facebook OAuth
235    #[serde(rename = "facebook")]
236    Facebook,
237    /// Microsoft OAuth
238    #[serde(rename = "azure")]
239    Microsoft,
240    /// LinkedIn OAuth
241    #[serde(rename = "linkedin_oidc")]
242    LinkedIn,
243}
244
245impl OAuthProvider {
246    /// Get provider name as string
247    pub fn as_str(&self) -> &'static str {
248        match self {
249            OAuthProvider::Google => "google",
250            OAuthProvider::GitHub => "github",
251            OAuthProvider::Discord => "discord",
252            OAuthProvider::Apple => "apple",
253            OAuthProvider::Twitter => "twitter",
254            OAuthProvider::Facebook => "facebook",
255            OAuthProvider::Microsoft => "azure",
256            OAuthProvider::LinkedIn => "linkedin_oidc",
257        }
258    }
259}
260
261/// OAuth sign-in options
262#[derive(Debug, Clone, Default)]
263pub struct OAuthOptions {
264    /// Custom redirect URL after sign-in
265    pub redirect_to: Option<String>,
266    /// Additional scopes to request
267    pub scopes: Option<Vec<String>>,
268    /// Additional provider-specific options
269    pub query_params: Option<std::collections::HashMap<String, String>>,
270}
271
272/// OAuth response with authorization URL
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct OAuthResponse {
275    /// Authorization URL to redirect user to
276    pub url: String,
277}
278
279/// Phone authentication request
280#[derive(Debug, Serialize)]
281struct PhoneSignUpRequest {
282    phone: String,
283    password: String,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    data: Option<serde_json::Value>,
286}
287
288/// Phone authentication sign-in request
289#[derive(Debug, Serialize)]
290struct PhoneSignInRequest {
291    phone: String,
292    password: String,
293}
294
295/// OTP verification request
296#[derive(Debug, Serialize)]
297struct OTPVerificationRequest {
298    phone: String,
299    token: String,
300    #[serde(rename = "type")]
301    verification_type: String,
302}
303
304/// Magic link request
305#[derive(Debug, Serialize)]
306struct MagicLinkRequest {
307    email: String,
308    #[serde(skip_serializing_if = "Option::is_none")]
309    redirect_to: Option<String>,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    data: Option<serde_json::Value>,
312}
313
314/// Anonymous sign-in request
315#[derive(Debug, Serialize)]
316struct AnonymousSignInRequest {
317    #[serde(skip_serializing_if = "Option::is_none")]
318    data: Option<serde_json::Value>,
319}
320
321/// Authentication client for handling user sessions and JWT tokens
322pub struct Auth {
323    http_client: Arc<HttpClient>,
324    config: Arc<SupabaseConfig>,
325    session: Arc<RwLock<Option<Session>>>,
326    event_listeners: Arc<RwLock<HashMap<Uuid, AuthStateCallback>>>,
327}
328
329impl Clone for Auth {
330    fn clone(&self) -> Self {
331        Self {
332            http_client: self.http_client.clone(),
333            config: self.config.clone(),
334            session: self.session.clone(),
335            event_listeners: Arc::new(RwLock::new(HashMap::new())),
336        }
337    }
338}
339
340impl std::fmt::Debug for Auth {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        f.debug_struct("Auth")
343            .field("http_client", &"HttpClient")
344            .field("config", &self.config)
345            .field("session", &self.session)
346            .field(
347                "event_listeners",
348                &format!(
349                    "HashMap<Uuid, Callback> with {} listeners",
350                    self.event_listeners.read().map(|l| l.len()).unwrap_or(0)
351                ),
352            )
353            .finish()
354    }
355}
356
357/// User information from Supabase Auth
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct User {
360    pub id: Uuid,
361    pub email: Option<String>,
362    pub phone: Option<String>,
363    pub email_confirmed_at: Option<Timestamp>,
364    pub phone_confirmed_at: Option<Timestamp>,
365    pub created_at: Timestamp,
366    pub updated_at: Timestamp,
367    pub last_sign_in_at: Option<Timestamp>,
368    pub app_metadata: serde_json::Value,
369    pub user_metadata: serde_json::Value,
370    pub aud: String,
371    pub role: Option<String>,
372}
373
374/// Authentication session containing user and tokens
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct Session {
377    pub access_token: String,
378    pub refresh_token: String,
379    pub expires_in: i64,
380    pub expires_at: Timestamp,
381    pub token_type: String,
382    pub user: User,
383}
384
385/// Response from authentication operations
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct AuthResponse {
388    pub user: Option<User>,
389    pub session: Option<Session>,
390}
391
392/// Sign up request payload
393#[derive(Debug, Serialize)]
394struct SignUpRequest {
395    email: String,
396    password: String,
397    #[serde(skip_serializing_if = "Option::is_none")]
398    data: Option<serde_json::Value>,
399    #[serde(skip_serializing_if = "Option::is_none")]
400    redirect_to: Option<String>,
401}
402
403/// Sign in request payload
404#[derive(Debug, Serialize)]
405struct SignInRequest {
406    email: String,
407    password: String,
408}
409
410/// Password reset request payload
411#[derive(Debug, Serialize)]
412struct PasswordResetRequest {
413    email: String,
414    #[serde(skip_serializing_if = "Option::is_none")]
415    redirect_to: Option<String>,
416}
417
418/// Token refresh request payload
419#[derive(Debug, Serialize)]
420struct RefreshTokenRequest {
421    refresh_token: String,
422}
423
424/// Update user request payload
425#[derive(Debug, Serialize)]
426struct UpdateUserRequest {
427    #[serde(skip_serializing_if = "Option::is_none")]
428    email: Option<String>,
429    #[serde(skip_serializing_if = "Option::is_none")]
430    password: Option<String>,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    data: Option<serde_json::Value>,
433}
434
435impl Auth {
436    /// Create a new Auth instance
437    pub fn new(config: Arc<SupabaseConfig>, http_client: Arc<HttpClient>) -> Result<Self> {
438        debug!("Initializing Auth module");
439
440        Ok(Self {
441            http_client,
442            config,
443            session: Arc::new(RwLock::new(None)),
444            event_listeners: Arc::new(RwLock::new(HashMap::new())),
445        })
446    }
447
448    /// Sign up a new user with email and password
449    pub async fn sign_up_with_email_and_password(
450        &self,
451        email: &str,
452        password: &str,
453    ) -> Result<AuthResponse> {
454        self.sign_up_with_email_password_and_data(email, password, None, None)
455            .await
456    }
457
458    /// Sign up a new user with email, password, and optional metadata
459    pub async fn sign_up_with_email_password_and_data(
460        &self,
461        email: &str,
462        password: &str,
463        data: Option<serde_json::Value>,
464        redirect_to: Option<String>,
465    ) -> Result<AuthResponse> {
466        debug!("Signing up user with email: {}", email);
467
468        let payload = SignUpRequest {
469            email: email.to_string(),
470            password: password.to_string(),
471            data,
472            redirect_to,
473        };
474
475        let response = self
476            .http_client
477            .post(format!("{}/auth/v1/signup", self.config.url))
478            .json(&payload)
479            .send()
480            .await?;
481
482        if !response.status().is_success() {
483            let status = response.status();
484            let error_msg = match response.text().await {
485                Ok(text) => text,
486                Err(_) => format!("Sign up failed with status: {}", status),
487            };
488            return Err(Error::auth(error_msg));
489        }
490
491        let auth_response: AuthResponse = response.json().await?;
492
493        if let Some(ref session) = auth_response.session {
494            self.set_session(session.clone()).await?;
495            self.trigger_auth_event(AuthEvent::SignedIn);
496            info!("User signed up successfully");
497        }
498
499        Ok(auth_response)
500    }
501
502    /// Sign in with email and password
503    pub async fn sign_in_with_email_and_password(
504        &self,
505        email: &str,
506        password: &str,
507    ) -> Result<AuthResponse> {
508        debug!("Signing in user with email: {}", email);
509
510        let payload = SignInRequest {
511            email: email.to_string(),
512            password: password.to_string(),
513        };
514
515        let response = self
516            .http_client
517            .post(format!(
518                "{}/auth/v1/token?grant_type=password",
519                self.config.url
520            ))
521            .json(&payload)
522            .send()
523            .await?;
524
525        if !response.status().is_success() {
526            let status = response.status();
527            let error_msg = match response.text().await {
528                Ok(text) => text,
529                Err(_) => format!("Sign in failed with status: {}", status),
530            };
531            return Err(Error::auth(error_msg));
532        }
533
534        let auth_response: AuthResponse = response.json().await?;
535
536        if let Some(ref session) = auth_response.session {
537            self.set_session(session.clone()).await?;
538            self.trigger_auth_event(AuthEvent::SignedIn);
539            info!("User signed in successfully");
540        }
541
542        Ok(auth_response)
543    }
544
545    /// Sign out the current user
546    pub async fn sign_out(&self) -> Result<()> {
547        debug!("Signing out user");
548
549        let session = self.get_session()?;
550
551        let response = self
552            .http_client
553            .post(format!("{}/auth/v1/logout", self.config.url))
554            .header("Authorization", format!("Bearer {}", session.access_token))
555            .send()
556            .await?;
557
558        if !response.status().is_success() {
559            warn!("Sign out request failed with status: {}", response.status());
560        }
561
562        self.clear_session().await?;
563        self.trigger_auth_event(AuthEvent::SignedOut);
564        info!("User signed out successfully");
565
566        Ok(())
567    }
568
569    /// Reset password via email
570    pub async fn reset_password_for_email(&self, email: &str) -> Result<()> {
571        self.reset_password_for_email_with_redirect(email, None)
572            .await
573    }
574
575    /// Reset password via email with optional redirect URL
576    pub async fn reset_password_for_email_with_redirect(
577        &self,
578        email: &str,
579        redirect_to: Option<String>,
580    ) -> Result<()> {
581        debug!("Requesting password reset for email: {}", email);
582
583        let payload = PasswordResetRequest {
584            email: email.to_string(),
585            redirect_to,
586        };
587
588        let response = self
589            .http_client
590            .post(format!("{}/auth/v1/recover", self.config.url))
591            .json(&payload)
592            .send()
593            .await?;
594
595        if !response.status().is_success() {
596            let status = response.status();
597            let error_msg = match response.text().await {
598                Ok(text) => text,
599                Err(_) => format!("Password reset failed with status: {}", status),
600            };
601            return Err(Error::auth(error_msg));
602        }
603
604        info!("Password reset email sent successfully");
605        Ok(())
606    }
607
608    /// Update the current user's information
609    pub async fn update_user(
610        &self,
611        email: Option<String>,
612        password: Option<String>,
613        data: Option<serde_json::Value>,
614    ) -> Result<AuthResponse> {
615        debug!("Updating user information");
616
617        let session = self.get_session()?;
618
619        let payload = UpdateUserRequest {
620            email,
621            password,
622            data,
623        };
624
625        let response = self
626            .http_client
627            .put(format!("{}/auth/v1/user", self.config.url))
628            .header("Authorization", format!("Bearer {}", session.access_token))
629            .json(&payload)
630            .send()
631            .await?;
632
633        if !response.status().is_success() {
634            let status = response.status();
635            let error_msg = match response.text().await {
636                Ok(text) => text,
637                Err(_) => format!("User update failed with status: {}", status),
638            };
639            return Err(Error::auth(error_msg));
640        }
641
642        let auth_response: AuthResponse = response.json().await?;
643
644        if let Some(ref session) = auth_response.session {
645            self.set_session(session.clone()).await?;
646        }
647
648        info!("User updated successfully");
649        Ok(auth_response)
650    }
651
652    /// Refresh the current session token
653    pub async fn refresh_session(&self) -> Result<AuthResponse> {
654        debug!("Refreshing session token");
655
656        let current_session = self.get_session()?;
657
658        let payload = RefreshTokenRequest {
659            refresh_token: current_session.refresh_token.clone(),
660        };
661
662        let response = self
663            .http_client
664            .post(format!(
665                "{}/auth/v1/token?grant_type=refresh_token",
666                self.config.url
667            ))
668            .json(&payload)
669            .send()
670            .await?;
671
672        if !response.status().is_success() {
673            let status = response.status();
674            let error_msg = match response.text().await {
675                Ok(text) => text,
676                Err(_) => format!("Token refresh failed with status: {}", status),
677            };
678            return Err(Error::auth(error_msg));
679        }
680
681        let auth_response: AuthResponse = response.json().await?;
682
683        if let Some(ref session) = auth_response.session {
684            self.set_session(session.clone()).await?;
685            self.trigger_auth_event(AuthEvent::TokenRefreshed);
686            info!("Session refreshed successfully");
687        }
688
689        Ok(auth_response)
690    }
691
692    /// Get the current user information
693    pub async fn current_user(&self) -> Result<Option<User>> {
694        let session_guard = self
695            .session
696            .read()
697            .map_err(|_| Error::auth("Failed to read session"))?;
698        Ok(session_guard.as_ref().map(|s| s.user.clone()))
699    }
700
701    /// Get the current session
702    pub fn get_session(&self) -> Result<Session> {
703        let session_guard = self
704            .session
705            .read()
706            .map_err(|_| Error::auth("Failed to read session"))?;
707        session_guard
708            .as_ref()
709            .cloned()
710            .ok_or_else(|| Error::auth("No active session"))
711    }
712
713    /// Set a new session
714    pub async fn set_session(&self, session: Session) -> Result<()> {
715        let mut session_guard = self
716            .session
717            .write()
718            .map_err(|_| Error::auth("Failed to write session"))?;
719        *session_guard = Some(session);
720        Ok(())
721    }
722
723    /// Set session from JWT token
724    pub async fn set_session_token(&self, token: &str) -> Result<()> {
725        debug!("Setting session from token");
726
727        let user_response = self
728            .http_client
729            .get(format!("{}/auth/v1/user", self.config.url))
730            .header("Authorization", format!("Bearer {}", token))
731            .send()
732            .await?;
733
734        if !user_response.status().is_success() {
735            return Err(Error::auth("Invalid token"));
736        }
737
738        let user: User = user_response.json().await?;
739
740        let session = Session {
741            access_token: token.to_string(),
742            refresh_token: String::new(),
743            expires_in: 3600,
744            expires_at: Utc::now() + chrono::Duration::seconds(3600),
745            token_type: "bearer".to_string(),
746            user,
747        };
748
749        self.set_session(session).await?;
750        Ok(())
751    }
752
753    /// Clear the current session
754    pub async fn clear_session(&self) -> Result<()> {
755        let mut session_guard = self
756            .session
757            .write()
758            .map_err(|_| Error::auth("Failed to write session"))?;
759        *session_guard = None;
760        Ok(())
761    }
762
763    /// Check if the user is authenticated
764    pub fn is_authenticated(&self) -> bool {
765        let session_guard = self.session.read().unwrap_or_else(|_| {
766            warn!("Failed to read session lock");
767            self.session.read().unwrap()
768        });
769
770        match session_guard.as_ref() {
771            Some(session) => {
772                let now = Utc::now();
773                session.expires_at > now
774            }
775            None => false,
776        }
777    }
778
779    /// Check if the current token needs refresh
780    pub fn needs_refresh(&self) -> bool {
781        let session_guard = match self.session.read() {
782            Ok(guard) => guard,
783            Err(_) => {
784                warn!("Failed to read session lock");
785                return false;
786            }
787        };
788
789        match session_guard.as_ref() {
790            Some(session) => {
791                let now = Utc::now();
792                let buffer = chrono::Duration::minutes(5); // Refresh 5 minutes before expiry
793                session.expires_at < (now + buffer)
794            }
795            None => false,
796        }
797    }
798
799    /// Sign in with OAuth provider
800    ///
801    /// Returns a URL that the user should be redirected to for authentication.
802    /// After successful authentication, the user will be redirected back with the session.
803    ///
804    /// # Example
805    ///
806    /// ```rust
807    /// use supabase::auth::{OAuthProvider, OAuthOptions};
808    ///
809    /// # async fn example() -> supabase::Result<()> {
810    /// let client = supabase::Client::new("url", "key")?;
811    ///
812    /// let options = OAuthOptions {
813    ///     redirect_to: Some("https://myapp.com/callback".to_string()),
814    ///     scopes: Some(vec!["email".to_string(), "profile".to_string()]),
815    ///     ..Default::default()
816    /// };
817    ///
818    /// let response = client.auth().sign_in_with_oauth(OAuthProvider::Google, Some(options)).await?;
819    /// println!("Redirect to: {}", response.url);
820    /// # Ok(())
821    /// # }
822    /// ```
823    pub async fn sign_in_with_oauth(
824        &self,
825        provider: OAuthProvider,
826        options: Option<OAuthOptions>,
827    ) -> Result<OAuthResponse> {
828        debug!("Initiating OAuth sign-in with provider: {:?}", provider);
829
830        let mut url = format!(
831            "{}/auth/v1/authorize?provider={}",
832            self.config.url,
833            provider.as_str()
834        );
835
836        if let Some(opts) = options {
837            if let Some(redirect_to) = opts.redirect_to {
838                url.push_str(&format!(
839                    "&redirect_to={}",
840                    urlencoding::encode(&redirect_to)
841                ));
842            }
843
844            if let Some(scopes) = opts.scopes {
845                let scope_str = scopes.join(" ");
846                url.push_str(&format!("&scope={}", urlencoding::encode(&scope_str)));
847            }
848
849            if let Some(query_params) = opts.query_params {
850                for (key, value) in query_params {
851                    url.push_str(&format!(
852                        "&{}={}",
853                        urlencoding::encode(&key),
854                        urlencoding::encode(&value)
855                    ));
856                }
857            }
858        }
859
860        Ok(OAuthResponse { url })
861    }
862
863    /// Sign up with phone number
864    ///
865    /// # Example
866    ///
867    /// ```rust
868    /// # async fn example() -> supabase::Result<()> {
869    /// let client = supabase::Client::new("url", "key")?;
870    ///
871    /// let response = client.auth()
872    ///     .sign_up_with_phone("+1234567890", "securepassword", None)
873    ///     .await?;
874    ///
875    /// if let Some(user) = response.user {
876    ///     println!("User created: {:?}", user.phone);
877    /// }
878    /// # Ok(())
879    /// # }
880    /// ```
881    pub async fn sign_up_with_phone(
882        &self,
883        phone: &str,
884        password: &str,
885        data: Option<serde_json::Value>,
886    ) -> Result<AuthResponse> {
887        debug!("Signing up user with phone: {}", phone);
888
889        let payload = PhoneSignUpRequest {
890            phone: phone.to_string(),
891            password: password.to_string(),
892            data,
893        };
894
895        let response = self
896            .http_client
897            .post(format!("{}/auth/v1/signup", self.config.url))
898            .json(&payload)
899            .send()
900            .await?;
901
902        if !response.status().is_success() {
903            let status = response.status();
904            let error_msg = match response.text().await {
905                Ok(text) => text,
906                Err(_) => format!("Phone sign up failed with status: {}", status),
907            };
908            return Err(Error::auth(error_msg));
909        }
910
911        let auth_response: AuthResponse = response.json().await?;
912
913        if let Some(ref session) = auth_response.session {
914            self.set_session(session.clone()).await?;
915            self.trigger_auth_event(AuthEvent::SignedIn);
916            info!("User signed up with phone successfully");
917        }
918
919        Ok(auth_response)
920    }
921
922    /// Sign in with phone number
923    ///
924    /// # Example
925    ///
926    /// ```rust
927    /// # async fn example() -> supabase::Result<()> {
928    /// let client = supabase::Client::new("url", "key")?;
929    ///
930    /// let response = client.auth()
931    ///     .sign_in_with_phone("+1234567890", "securepassword")
932    ///     .await?;
933    ///
934    /// if let Some(user) = response.user {
935    ///     println!("User signed in: {:?}", user.phone);
936    /// }
937    /// # Ok(())
938    /// # }
939    /// ```
940    pub async fn sign_in_with_phone(&self, phone: &str, password: &str) -> Result<AuthResponse> {
941        debug!("Signing in user with phone: {}", phone);
942
943        let payload = PhoneSignInRequest {
944            phone: phone.to_string(),
945            password: password.to_string(),
946        };
947
948        let response = self
949            .http_client
950            .post(format!(
951                "{}/auth/v1/token?grant_type=password",
952                self.config.url
953            ))
954            .json(&payload)
955            .send()
956            .await?;
957
958        if !response.status().is_success() {
959            let status = response.status();
960            let error_msg = match response.text().await {
961                Ok(text) => text,
962                Err(_) => format!("Phone sign in failed with status: {}", status),
963            };
964            return Err(Error::auth(error_msg));
965        }
966
967        let auth_response: AuthResponse = response.json().await?;
968
969        if let Some(ref session) = auth_response.session {
970            self.set_session(session.clone()).await?;
971            self.trigger_auth_event(AuthEvent::SignedIn);
972            info!("User signed in with phone successfully");
973        }
974
975        Ok(auth_response)
976    }
977
978    /// Verify OTP token
979    ///
980    /// # Example
981    ///
982    /// ```rust
983    /// # async fn example() -> supabase::Result<()> {
984    /// let client = supabase::Client::new("url", "key")?;
985    ///
986    /// let response = client.auth()
987    ///     .verify_otp("+1234567890", "123456", "sms")
988    ///     .await?;
989    ///
990    /// if let Some(session) = response.session {
991    ///     println!("OTP verified, user signed in");
992    /// }
993    /// # Ok(())
994    /// # }
995    /// ```
996    pub async fn verify_otp(
997        &self,
998        phone: &str,
999        token: &str,
1000        verification_type: &str,
1001    ) -> Result<AuthResponse> {
1002        debug!("Verifying OTP for phone: {}", phone);
1003
1004        let payload = OTPVerificationRequest {
1005            phone: phone.to_string(),
1006            token: token.to_string(),
1007            verification_type: verification_type.to_string(),
1008        };
1009
1010        let response = self
1011            .http_client
1012            .post(format!("{}/auth/v1/verify", self.config.url))
1013            .json(&payload)
1014            .send()
1015            .await?;
1016
1017        if !response.status().is_success() {
1018            let status = response.status();
1019            let error_msg = match response.text().await {
1020                Ok(text) => text,
1021                Err(_) => format!("OTP verification failed with status: {}", status),
1022            };
1023            return Err(Error::auth(error_msg));
1024        }
1025
1026        let auth_response: AuthResponse = response.json().await?;
1027
1028        if let Some(ref session) = auth_response.session {
1029            self.set_session(session.clone()).await?;
1030            self.trigger_auth_event(AuthEvent::SignedIn);
1031            info!("OTP verified successfully");
1032        }
1033
1034        Ok(auth_response)
1035    }
1036
1037    /// Send magic link for passwordless authentication
1038    ///
1039    /// # Example
1040    ///
1041    /// ```rust
1042    /// # async fn example() -> supabase::Result<()> {
1043    /// let client = supabase::Client::new("url", "key")?;
1044    ///
1045    /// client.auth()
1046    ///     .sign_in_with_magic_link("user@example.com", Some("https://myapp.com/callback".to_string()), None)
1047    ///     .await?;
1048    ///
1049    /// println!("Magic link sent to email");
1050    /// # Ok(())
1051    /// # }
1052    /// ```
1053    pub async fn sign_in_with_magic_link(
1054        &self,
1055        email: &str,
1056        redirect_to: Option<String>,
1057        data: Option<serde_json::Value>,
1058    ) -> Result<()> {
1059        debug!("Sending magic link to email: {}", email);
1060
1061        let payload = MagicLinkRequest {
1062            email: email.to_string(),
1063            redirect_to,
1064            data,
1065        };
1066
1067        let response = self
1068            .http_client
1069            .post(format!("{}/auth/v1/magiclink", self.config.url))
1070            .json(&payload)
1071            .send()
1072            .await?;
1073
1074        if !response.status().is_success() {
1075            let status = response.status();
1076            let error_msg = match response.text().await {
1077                Ok(text) => text,
1078                Err(_) => format!("Magic link request failed with status: {}", status),
1079            };
1080            return Err(Error::auth(error_msg));
1081        }
1082
1083        info!("Magic link sent successfully");
1084        Ok(())
1085    }
1086
1087    /// Sign in anonymously
1088    ///
1089    /// Creates a temporary anonymous user session that can be converted to a permanent account later.
1090    ///
1091    /// # Example
1092    ///
1093    /// ```rust
1094    /// # async fn example() -> supabase::Result<()> {
1095    /// let client = supabase::Client::new("url", "key")?;
1096    ///
1097    /// let response = client.auth()
1098    ///     .sign_in_anonymously(None)
1099    ///     .await?;
1100    ///
1101    /// if let Some(user) = response.user {
1102    ///     println!("Anonymous user created: {}", user.id);
1103    /// }
1104    /// # Ok(())
1105    /// # }
1106    /// ```
1107    pub async fn sign_in_anonymously(
1108        &self,
1109        data: Option<serde_json::Value>,
1110    ) -> Result<AuthResponse> {
1111        debug!("Creating anonymous user session");
1112
1113        let payload = AnonymousSignInRequest { data };
1114
1115        let response = self
1116            .http_client
1117            .post(format!("{}/auth/v1/signup", self.config.url))
1118            .header("Authorization", format!("Bearer {}", self.config.key))
1119            .json(&payload)
1120            .send()
1121            .await?;
1122
1123        if !response.status().is_success() {
1124            let status = response.status();
1125            let error_msg = match response.text().await {
1126                Ok(text) => text,
1127                Err(_) => format!("Anonymous sign in failed with status: {}", status),
1128            };
1129            return Err(Error::auth(error_msg));
1130        }
1131
1132        let auth_response: AuthResponse = response.json().await?;
1133
1134        if let Some(ref session) = auth_response.session {
1135            self.set_session(session.clone()).await?;
1136            self.trigger_auth_event(AuthEvent::SignedIn);
1137            info!("Anonymous user session created successfully");
1138        }
1139
1140        Ok(auth_response)
1141    }
1142
1143    /// Enhanced password recovery with custom redirect and options
1144    ///
1145    /// # Example
1146    ///
1147    /// ```rust
1148    /// # async fn example() -> supabase::Result<()> {
1149    /// let client = supabase::Client::new("url", "key")?;
1150    ///
1151    /// client.auth()
1152    ///     .reset_password_for_email_enhanced("user@example.com", Some("https://myapp.com/reset".to_string()))
1153    ///     .await?;
1154    ///
1155    /// println!("Password reset email sent");
1156    /// # Ok(())
1157    /// # }
1158    /// ```
1159    pub async fn reset_password_for_email_enhanced(
1160        &self,
1161        email: &str,
1162        redirect_to: Option<String>,
1163    ) -> Result<()> {
1164        debug!("Initiating enhanced password recovery for email: {}", email);
1165
1166        let payload = PasswordResetRequest {
1167            email: email.to_string(),
1168            redirect_to,
1169        };
1170
1171        let response = self
1172            .http_client
1173            .post(format!("{}/auth/v1/recover", self.config.url))
1174            .json(&payload)
1175            .send()
1176            .await?;
1177
1178        if !response.status().is_success() {
1179            let status = response.status();
1180            let error_msg = match response.text().await {
1181                Ok(text) => text,
1182                Err(_) => format!("Enhanced password recovery failed with status: {}", status),
1183            };
1184            return Err(Error::auth(error_msg));
1185        }
1186
1187        self.trigger_auth_event(AuthEvent::PasswordReset);
1188        info!("Enhanced password recovery email sent successfully");
1189        Ok(())
1190    }
1191
1192    /// Subscribe to authentication state changes
1193    ///
1194    /// Returns a handle that can be used to remove the listener later.
1195    ///
1196    /// # Example
1197    ///
1198    /// ```rust
1199    /// use supabase::auth::AuthEvent;
1200    ///
1201    /// # async fn example() -> supabase::Result<()> {
1202    /// let client = supabase::Client::new("url", "key")?;
1203    ///
1204    /// let handle = client.auth().on_auth_state_change(|event, session| {
1205    ///     match event {
1206    ///         AuthEvent::SignedIn => {
1207    ///             if let Some(session) = session {
1208    ///                 println!("User signed in: {}", session.user.email.unwrap_or_default());
1209    ///             }
1210    ///         }
1211    ///         AuthEvent::SignedOut => println!("User signed out"),
1212    ///         AuthEvent::TokenRefreshed => println!("Token refreshed"),
1213    ///         _ => {}
1214    ///     }
1215    /// });
1216    ///
1217    /// // Later remove the listener
1218    /// handle.remove();
1219    /// # Ok(())
1220    /// # }
1221    /// ```
1222    pub fn on_auth_state_change<F>(&self, callback: F) -> AuthEventHandle
1223    where
1224        F: Fn(AuthEvent, Option<Session>) + Send + Sync + 'static,
1225    {
1226        let id = Uuid::new_v4();
1227        let callback = Box::new(callback);
1228
1229        if let Ok(mut listeners) = self.event_listeners.write() {
1230            listeners.insert(id, callback);
1231        }
1232
1233        AuthEventHandle {
1234            id,
1235            auth: Arc::downgrade(&Arc::new(self.clone())),
1236        }
1237    }
1238
1239    /// Remove an authentication state listener
1240    pub fn remove_auth_listener(&self, id: Uuid) {
1241        if let Ok(mut listeners) = self.event_listeners.write() {
1242            listeners.remove(&id);
1243        }
1244    }
1245
1246    /// Trigger authentication state change event
1247    fn trigger_auth_event(&self, event: AuthEvent) {
1248        let session = match self.session.read() {
1249            Ok(guard) => guard.clone(),
1250            Err(_) => {
1251                warn!("Failed to read session for event trigger");
1252                return;
1253            }
1254        };
1255
1256        let listeners = match self.event_listeners.read() {
1257            Ok(guard) => guard,
1258            Err(_) => {
1259                warn!("Failed to read event listeners");
1260                return;
1261            }
1262        };
1263
1264        for callback in listeners.values() {
1265            callback(event.clone(), session.clone());
1266        }
1267    }
1268
1269    // ==== MFA (Multi-Factor Authentication) Methods ====
1270
1271    /// List all MFA factors for the current user
1272    ///
1273    /// # Examples
1274    ///
1275    /// ```rust,no_run
1276    /// # use supabase::Client;
1277    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1278    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1279    ///
1280    /// // List MFA factors
1281    /// let factors = client.auth().list_mfa_factors().await?;
1282    /// println!("User has {} MFA factors configured", factors.len());
1283    /// # Ok(())
1284    /// # }
1285    /// ```
1286    pub async fn list_mfa_factors(&self) -> Result<Vec<MfaFactor>> {
1287        debug!("Listing MFA factors for user");
1288
1289        let session = self.get_session()?;
1290        let response = self
1291            .http_client
1292            .get(format!("{}/auth/v1/factors", self.config.url))
1293            .header("Authorization", format!("Bearer {}", session.access_token))
1294            .send()
1295            .await?;
1296
1297        if !response.status().is_success() {
1298            return Err(Error::auth("Failed to list MFA factors"));
1299        }
1300
1301        let factors: Vec<MfaFactor> = response.json().await?;
1302        Ok(factors)
1303    }
1304
1305    /// Setup TOTP (Time-based One-Time Password) authentication
1306    ///
1307    /// # Examples
1308    ///
1309    /// ```rust,no_run
1310    /// # use supabase::Client;
1311    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1312    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1313    ///
1314    /// // Setup TOTP
1315    /// let totp_setup = client.auth()
1316    ///     .setup_totp("My Authenticator")
1317    ///     .await?;
1318    ///
1319    /// println!("Scan QR code: {}", totp_setup.qr_code);
1320    /// println!("Or enter secret manually: {}", totp_setup.secret);
1321    /// # Ok(())
1322    /// # }
1323    /// ```
1324    pub async fn setup_totp(&self, friendly_name: &str) -> Result<TotpSetupResponse> {
1325        debug!("Setting up TOTP factor: {}", friendly_name);
1326
1327        let session = self.get_session()?;
1328
1329        let request_body = serde_json::json!({
1330            "friendly_name": friendly_name,
1331            "factor_type": "totp"
1332        });
1333
1334        let response = self
1335            .http_client
1336            .post(format!("{}/auth/v1/factors", self.config.url))
1337            .header("Authorization", format!("Bearer {}", session.access_token))
1338            .json(&request_body)
1339            .send()
1340            .await?;
1341
1342        if !response.status().is_success() {
1343            return Err(Error::auth("Failed to setup TOTP"));
1344        }
1345
1346        let setup_response: TotpSetupResponse = response.json().await?;
1347
1348        // Generate QR code for the TOTP URI
1349        let qr = QrCode::new(&setup_response.uri)
1350            .map_err(|e| Error::auth(format!("Failed to generate QR code: {}", e)))?;
1351
1352        // Convert QR code to string representation (for console display)
1353        let qr_string = qr
1354            .render::<char>()
1355            .quiet_zone(false)
1356            .module_dimensions(2, 1)
1357            .build();
1358
1359        Ok(TotpSetupResponse {
1360            secret: setup_response.secret,
1361            qr_code: qr_string,
1362            uri: setup_response.uri,
1363            factor_id: setup_response.factor_id,
1364        })
1365    }
1366
1367    /// Setup SMS-based MFA with international phone number support
1368    ///
1369    /// # Examples
1370    ///
1371    /// ```rust,no_run
1372    /// # use supabase::Client;
1373    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1374    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1375    ///
1376    /// // Setup SMS MFA with international number
1377    /// let factor = client.auth()
1378    ///     .setup_sms_mfa("+1-555-123-4567", "My Phone", Some("US"))
1379    ///     .await?;
1380    ///
1381    /// println!("SMS MFA configured for: {}", factor.phone.unwrap());
1382    /// # Ok(())
1383    /// # }
1384    /// ```
1385    pub async fn setup_sms_mfa(
1386        &self,
1387        phone: &str,
1388        friendly_name: &str,
1389        default_region: Option<&str>,
1390    ) -> Result<MfaFactor> {
1391        debug!("Setting up SMS MFA factor: {} for {}", friendly_name, phone);
1392
1393        // Validate and format phone number
1394        let enhanced_phone = EnhancedPhoneNumber::new(phone, default_region)?;
1395
1396        if !enhanced_phone.is_valid {
1397            return Err(Error::auth("Invalid phone number provided"));
1398        }
1399
1400        let session = self.get_session()?;
1401
1402        let request_body = serde_json::json!({
1403            "friendly_name": friendly_name,
1404            "factor_type": "sms",
1405            "phone": enhanced_phone.formatted
1406        });
1407
1408        let response = self
1409            .http_client
1410            .post(format!("{}/auth/v1/factors", self.config.url))
1411            .header("Authorization", format!("Bearer {}", session.access_token))
1412            .json(&request_body)
1413            .send()
1414            .await?;
1415
1416        if !response.status().is_success() {
1417            return Err(Error::auth("Failed to setup SMS MFA"));
1418        }
1419
1420        let factor: MfaFactor = response.json().await?;
1421        self.trigger_auth_event(AuthEvent::MfaEnabled);
1422
1423        Ok(factor)
1424    }
1425
1426    /// Create MFA challenge for a specific factor
1427    ///
1428    /// # Examples
1429    ///
1430    /// ```rust,no_run
1431    /// # use supabase::Client;
1432    /// # use uuid::Uuid;
1433    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1434    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1435    ///
1436    /// // Get factors and create challenge
1437    /// let factors = client.auth().list_mfa_factors().await?;
1438    /// if let Some(factor) = factors.first() {
1439    ///     let challenge = client.auth().create_mfa_challenge(factor.id).await?;
1440    ///     println!("Challenge created: {}", challenge.id);
1441    /// }
1442    /// # Ok(())
1443    /// # }
1444    /// ```
1445    pub async fn create_mfa_challenge(&self, factor_id: Uuid) -> Result<MfaChallenge> {
1446        debug!("Creating MFA challenge for factor: {}", factor_id);
1447
1448        let session = self.get_session()?;
1449
1450        let request_body = serde_json::json!({
1451            "factor_id": factor_id
1452        });
1453
1454        let response = self
1455            .http_client
1456            .post(format!(
1457                "{}/auth/v1/factors/{}/challenge",
1458                self.config.url, factor_id
1459            ))
1460            .header("Authorization", format!("Bearer {}", session.access_token))
1461            .json(&request_body)
1462            .send()
1463            .await?;
1464
1465        if !response.status().is_success() {
1466            return Err(Error::auth("Failed to create MFA challenge"));
1467        }
1468
1469        let challenge: MfaChallenge = response.json().await?;
1470        self.trigger_auth_event(AuthEvent::MfaChallengeRequired);
1471
1472        Ok(challenge)
1473    }
1474
1475    /// Verify MFA challenge with user-provided code
1476    ///
1477    /// # Examples
1478    ///
1479    /// ```rust,no_run
1480    /// # use supabase::Client;
1481    /// # use uuid::Uuid;
1482    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1483    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1484    ///
1485    /// // Verify MFA code
1486    /// let factor_id = Uuid::new_v4(); // Your factor ID
1487    /// let challenge_id = Uuid::new_v4(); // Your challenge ID
1488    ///
1489    /// let result = client.auth()
1490    ///     .verify_mfa_challenge(factor_id, challenge_id, "123456")
1491    ///     .await?;
1492    ///
1493    /// println!("MFA verified successfully!");
1494    /// # Ok(())
1495    /// # }
1496    /// ```
1497    pub async fn verify_mfa_challenge(
1498        &self,
1499        factor_id: Uuid,
1500        challenge_id: Uuid,
1501        code: &str,
1502    ) -> Result<AuthResponse> {
1503        debug!("Verifying MFA challenge: {}", challenge_id);
1504
1505        let session = self.get_session()?;
1506
1507        let request_body = serde_json::json!({
1508            "factor_id": factor_id,
1509            "challenge_id": challenge_id,
1510            "code": code
1511        });
1512
1513        let response = self
1514            .http_client
1515            .post(format!(
1516                "{}/auth/v1/factors/{}/verify",
1517                self.config.url, factor_id
1518            ))
1519            .header("Authorization", format!("Bearer {}", session.access_token))
1520            .json(&request_body)
1521            .send()
1522            .await?;
1523
1524        if !response.status().is_success() {
1525            return Err(Error::auth("Failed to verify MFA challenge"));
1526        }
1527
1528        let auth_response: AuthResponse = response.json().await?;
1529
1530        // Update session if new token provided
1531        if let Some(session) = &auth_response.session {
1532            self.set_session(session.clone()).await?;
1533        }
1534
1535        self.trigger_auth_event(AuthEvent::MfaChallengeCompleted);
1536        info!("MFA verification successful");
1537
1538        Ok(auth_response)
1539    }
1540
1541    /// Delete an MFA factor
1542    ///
1543    /// # Examples
1544    ///
1545    /// ```rust,no_run
1546    /// # use supabase::Client;
1547    /// # use uuid::Uuid;
1548    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1549    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1550    ///
1551    /// // Remove MFA factor
1552    /// let factor_id = Uuid::new_v4(); // Your factor ID
1553    /// client.auth().delete_mfa_factor(factor_id).await?;
1554    /// println!("MFA factor removed successfully!");
1555    /// # Ok(())
1556    /// # }
1557    /// ```
1558    pub async fn delete_mfa_factor(&self, factor_id: Uuid) -> Result<()> {
1559        debug!("Deleting MFA factor: {}", factor_id);
1560
1561        let session = self.get_session()?;
1562
1563        let response = self
1564            .http_client
1565            .delete(format!("{}/auth/v1/factors/{}", self.config.url, factor_id))
1566            .header("Authorization", format!("Bearer {}", session.access_token))
1567            .send()
1568            .await?;
1569
1570        if !response.status().is_success() {
1571            return Err(Error::auth("Failed to delete MFA factor"));
1572        }
1573
1574        self.trigger_auth_event(AuthEvent::MfaDisabled);
1575        info!("MFA factor deleted successfully");
1576
1577        Ok(())
1578    }
1579
1580    /// Generate TOTP code for testing purposes (development only)
1581    ///
1582    /// This method is primarily for testing and development. In production,
1583    /// users should use their authenticator apps.
1584    ///
1585    /// # Examples
1586    ///
1587    /// ```rust,no_run
1588    /// # use supabase::Client;
1589    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1590    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1591    ///
1592    /// // For testing - generate TOTP code from secret
1593    /// let secret = "JBSWY3DPEHPK3PXP"; // Base32 encoded secret
1594    /// let code = client.auth().generate_totp_code(secret)?;
1595    /// println!("Generated TOTP code: {}", code);
1596    /// # Ok(())
1597    /// # }
1598    /// ```
1599    pub fn generate_totp_code(&self, secret: &str) -> Result<String> {
1600        debug!("Generating TOTP code for testing");
1601
1602        // Decode base32 secret
1603        let decoded_secret = base32::decode(base32::Alphabet::Rfc4648 { padding: true }, secret)
1604            .ok_or_else(|| Error::auth("Invalid base32 secret"))?;
1605
1606        // Create TOTP
1607        let totp = TOTP::new(
1608            Algorithm::SHA1,
1609            6,  // digits
1610            1,  // skew (allow 1 step tolerance)
1611            30, // step (30 seconds)
1612            decoded_secret,
1613        )
1614        .map_err(|e| Error::auth(format!("Failed to create TOTP: {}", e)))?;
1615
1616        // Generate current code
1617        let code = totp
1618            .generate_current()
1619            .map_err(|e| Error::auth(format!("Failed to generate TOTP code: {}", e)))?;
1620
1621        Ok(code)
1622    }
1623
1624    // ==== Advanced OAuth Token Management ====
1625
1626    /// Get current token metadata with advanced information
1627    ///
1628    /// # Examples
1629    ///
1630    /// ```rust,no_run
1631    /// # use supabase::Client;
1632    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1633    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1634    ///
1635    /// // Get detailed token metadata
1636    /// if let Some(metadata) = client.auth().get_token_metadata()? {
1637    ///     println!("Token expires at: {}", metadata.expires_at);
1638    ///     println!("Refresh count: {}", metadata.refresh_count);
1639    ///     println!("Scopes: {:?}", metadata.scopes);
1640    /// }
1641    /// # Ok(())
1642    /// # }
1643    /// ```
1644    pub fn get_token_metadata(&self) -> Result<Option<TokenMetadata>> {
1645        debug!("Getting token metadata");
1646
1647        let session = self
1648            .session
1649            .read()
1650            .map_err(|_| Error::auth("Failed to read session"))?;
1651
1652        if let Some(session) = session.as_ref() {
1653            // Create metadata from current session
1654            let metadata = TokenMetadata {
1655                issued_at: Utc::now() - chrono::Duration::seconds(session.expires_in),
1656                expires_at: session.expires_at,
1657                refresh_count: 0, // TODO: Track this in enhanced session
1658                last_refresh_at: None,
1659                scopes: vec![],  // TODO: Extract from JWT
1660                device_id: None, // TODO: Add device tracking
1661            };
1662
1663            Ok(Some(metadata))
1664        } else {
1665            Ok(None)
1666        }
1667    }
1668
1669    /// Refresh token with advanced error handling and retry logic
1670    ///
1671    /// # Examples
1672    ///
1673    /// ```rust,no_run
1674    /// # use supabase::Client;
1675    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1676    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1677    ///
1678    /// // Refresh token with advanced handling
1679    /// match client.auth().refresh_token_advanced().await {
1680    ///     Ok(session) => {
1681    ///         println!("Token refreshed successfully!");
1682    ///         println!("New expiry: {}", session.expires_at);
1683    ///     }
1684    ///     Err(e) => {
1685    ///         if e.is_retryable() {
1686    ///             // Handle retryable error
1687    ///             println!("Retryable error: {}", e);
1688    ///         } else {
1689    ///             // Handle non-retryable error - require re-login
1690    ///             println!("Re-authentication required: {}", e);
1691    ///         }
1692    ///     }
1693    /// }
1694    /// # Ok(())
1695    /// # }
1696    /// ```
1697    pub async fn refresh_token_advanced(&self) -> Result<Session> {
1698        debug!("Refreshing token with advanced handling");
1699
1700        let current_session = self
1701            .session
1702            .read()
1703            .map_err(|_| Error::auth("Failed to read session"))?
1704            .clone();
1705
1706        let session = match current_session {
1707            Some(session) => session,
1708            None => return Err(Error::auth("No active session to refresh")),
1709        };
1710
1711        let request_body = serde_json::json!({
1712            "refresh_token": session.refresh_token
1713        });
1714
1715        let response = self
1716            .http_client
1717            .post(format!(
1718                "{}/auth/v1/token?grant_type=refresh_token",
1719                self.config.url
1720            ))
1721            .header("apikey", &self.config.key)
1722            .header("Authorization", format!("Bearer {}", &self.config.key))
1723            .json(&request_body)
1724            .send()
1725            .await;
1726
1727        match response {
1728            Ok(response) => {
1729                if response.status().is_success() {
1730                    let auth_response: AuthResponse = response.json().await?;
1731
1732                    if let Some(new_session) = auth_response.session {
1733                        self.set_session(new_session.clone()).await?;
1734                        self.trigger_auth_event(AuthEvent::TokenRefreshed);
1735                        info!("Token refreshed successfully");
1736                        Ok(new_session)
1737                    } else {
1738                        Err(Error::auth("No session in refresh response"))
1739                    }
1740                } else {
1741                    let status = response.status();
1742                    let error_text = response.text().await.unwrap_or_default();
1743
1744                    // Provide specific error context
1745                    let context = crate::error::ErrorContext {
1746                        platform: Some(crate::error::detect_platform_context()),
1747                        http: Some(crate::error::HttpErrorContext {
1748                            status_code: Some(status.as_u16()),
1749                            headers: None,
1750                            response_body: Some(error_text.clone()),
1751                            url: Some(format!("{}/auth/v1/token", self.config.url)),
1752                            method: Some("POST".to_string()),
1753                        }),
1754                        retry: if status.is_server_error() {
1755                            Some(crate::error::RetryInfo {
1756                                retryable: true,
1757                                retry_after: Some(60), // 1 minute
1758                                attempts: 0,
1759                            })
1760                        } else {
1761                            None
1762                        },
1763                        metadata: std::collections::HashMap::new(),
1764                        timestamp: chrono::Utc::now(),
1765                    };
1766
1767                    Err(Error::auth_with_context(
1768                        format!("Token refresh failed: {} - {}", status, error_text),
1769                        context,
1770                    ))
1771                }
1772            }
1773            Err(e) => {
1774                let context = crate::error::ErrorContext {
1775                    platform: Some(crate::error::detect_platform_context()),
1776                    http: Some(crate::error::HttpErrorContext {
1777                        status_code: None,
1778                        headers: None,
1779                        response_body: None,
1780                        url: Some(format!("{}/auth/v1/token", self.config.url)),
1781                        method: Some("POST".to_string()),
1782                    }),
1783                    retry: Some(crate::error::RetryInfo {
1784                        retryable: true,
1785                        retry_after: Some(30),
1786                        attempts: 0,
1787                    }),
1788                    metadata: std::collections::HashMap::new(),
1789                    timestamp: chrono::Utc::now(),
1790                };
1791
1792                Err(Error::auth_with_context(
1793                    format!("Network error during token refresh: {}", e),
1794                    context,
1795                ))
1796            }
1797        }
1798    }
1799
1800    /// Check if current token needs refresh with buffer time
1801    ///
1802    /// # Examples
1803    ///
1804    /// ```rust,no_run
1805    /// # use supabase::Client;
1806    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1807    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1808    ///
1809    /// // Check if refresh is needed with 5-minute buffer
1810    /// if client.auth().needs_refresh_with_buffer(300)? {
1811    ///     println!("Token refresh recommended");
1812    ///     client.auth().refresh_token_advanced().await?;
1813    /// }
1814    /// # Ok(())
1815    /// # }
1816    /// ```
1817    pub fn needs_refresh_with_buffer(&self, buffer_seconds: i64) -> Result<bool> {
1818        let session_guard = self
1819            .session
1820            .read()
1821            .map_err(|_| Error::auth("Failed to read session"))?;
1822
1823        match session_guard.as_ref() {
1824            Some(session) => {
1825                let now = Utc::now();
1826                let refresh_threshold =
1827                    session.expires_at - chrono::Duration::seconds(buffer_seconds);
1828                Ok(now >= refresh_threshold)
1829            }
1830            None => Ok(false), // No session, no need to refresh
1831        }
1832    }
1833
1834    /// Get time until token expiry in seconds
1835    ///
1836    /// # Examples
1837    ///
1838    /// ```rust,no_run
1839    /// # use supabase::Client;
1840    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1841    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1842    ///
1843    /// match client.auth().time_until_expiry()? {
1844    ///     Some(seconds) => {
1845    ///         println!("Token expires in {} seconds", seconds);
1846    ///         if seconds < 300 {
1847    ///             println!("Consider refreshing token soon!");
1848    ///         }
1849    ///     }
1850    ///     None => println!("No active session"),
1851    /// }
1852    /// # Ok(())
1853    /// # }
1854    /// ```
1855    pub fn time_until_expiry(&self) -> Result<Option<i64>> {
1856        let session_guard = self
1857            .session
1858            .read()
1859            .map_err(|_| Error::auth("Failed to read session"))?;
1860
1861        match session_guard.as_ref() {
1862            Some(session) => {
1863                let now = Utc::now();
1864                let duration = session.expires_at.signed_duration_since(now);
1865                Ok(Some(duration.num_seconds()))
1866            }
1867            None => Ok(None),
1868        }
1869    }
1870
1871    /// Validate current token without making API call (local validation)
1872    ///
1873    /// # Examples
1874    ///
1875    /// ```rust,no_run
1876    /// # use supabase::Client;
1877    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1878    /// let client = Client::new("https://example.supabase.co", "your-anon-key")?;
1879    ///
1880    /// match client.auth().validate_token_local() {
1881    ///     Ok(true) => println!("Token is valid locally"),
1882    ///     Ok(false) => println!("Token is expired or invalid"),
1883    ///     Err(e) => println!("Validation error: {}", e),
1884    /// }
1885    /// # Ok(())
1886    /// # }
1887    /// ```
1888    pub fn validate_token_local(&self) -> Result<bool> {
1889        let session_guard = self
1890            .session
1891            .read()
1892            .map_err(|_| Error::auth("Failed to read session"))?;
1893
1894        match session_guard.as_ref() {
1895            Some(session) => {
1896                let now = Utc::now();
1897                Ok(session.expires_at > now && !session.access_token.is_empty())
1898            }
1899            None => Ok(false),
1900        }
1901    }
1902}
1903
1904#[cfg(test)]
1905mod tests {
1906    use super::*;
1907    use crate::types::SupabaseConfig;
1908    use std::sync::Arc;
1909
1910    fn mock_config() -> Arc<SupabaseConfig> {
1911        Arc::new(SupabaseConfig {
1912            url: "https://test.supabase.co".to_string(),
1913            key: "test-key".to_string(),
1914            service_role_key: None,
1915            http_config: crate::types::HttpConfig::default(),
1916            auth_config: crate::types::AuthConfig::default(),
1917            database_config: crate::types::DatabaseConfig::default(),
1918            storage_config: crate::types::StorageConfig::default(),
1919        })
1920    }
1921
1922    #[test]
1923    fn test_enhanced_phone_number_creation() {
1924        let phone = EnhancedPhoneNumber::new("+1-555-123-4567", Some("US"));
1925        assert!(phone.is_ok());
1926
1927        let phone = phone.unwrap();
1928        assert!(!phone.raw.is_empty());
1929        assert!(!phone.formatted.is_empty());
1930        assert!(!phone.country_code.is_empty());
1931    }
1932
1933    #[test]
1934    fn test_mfa_method_serialization() {
1935        let totp = MfaMethod::Totp;
1936        let sms = MfaMethod::Sms;
1937        let email = MfaMethod::Email;
1938
1939        let totp_json = serde_json::to_string(&totp).unwrap();
1940        let sms_json = serde_json::to_string(&sms).unwrap();
1941        let email_json = serde_json::to_string(&email).unwrap();
1942
1943        assert_eq!(totp_json, r#""totp""#);
1944        assert_eq!(sms_json, r#""sms""#);
1945        assert_eq!(email_json, r#""email""#);
1946    }
1947
1948    #[test]
1949    fn test_mfa_challenge_status() {
1950        let pending = MfaChallengeStatus::Pending;
1951        let completed = MfaChallengeStatus::Completed;
1952        let expired = MfaChallengeStatus::Expired;
1953        let cancelled = MfaChallengeStatus::Cancelled;
1954
1955        assert_eq!(pending, MfaChallengeStatus::Pending);
1956        assert_eq!(completed, MfaChallengeStatus::Completed);
1957        assert_eq!(expired, MfaChallengeStatus::Expired);
1958        assert_eq!(cancelled, MfaChallengeStatus::Cancelled);
1959    }
1960
1961    #[test]
1962    fn test_auth_event_variants() {
1963        let events = vec![
1964            AuthEvent::SignedIn,
1965            AuthEvent::SignedOut,
1966            AuthEvent::TokenRefreshed,
1967            AuthEvent::UserUpdated,
1968            AuthEvent::PasswordReset,
1969            AuthEvent::MfaChallengeRequired,
1970            AuthEvent::MfaChallengeCompleted,
1971            AuthEvent::MfaEnabled,
1972            AuthEvent::MfaDisabled,
1973        ];
1974
1975        assert_eq!(events.len(), 9);
1976
1977        // Test cloning
1978        let cloned_events: Vec<AuthEvent> = events.to_vec();
1979        assert_eq!(cloned_events, events);
1980    }
1981
1982    #[tokio::test]
1983    async fn test_auth_creation() {
1984        let config = mock_config();
1985        let http_client = Arc::new(reqwest::Client::new());
1986
1987        let auth = Auth::new(config, http_client);
1988        assert!(auth.is_ok());
1989
1990        let auth = auth.unwrap();
1991        assert!(!auth.is_authenticated());
1992    }
1993
1994    #[test]
1995    fn test_totp_code_generation() {
1996        let config = mock_config();
1997        let http_client = Arc::new(reqwest::Client::new());
1998        let auth = Auth::new(config, http_client).unwrap();
1999
2000        // Test with a known base32 secret
2001        let secret = "JBSWY3DPEHPK3PXP"; // "Hello" in base32
2002        let result = auth.generate_totp_code(secret);
2003
2004        match &result {
2005            Ok(code) => {
2006                println!("Generated TOTP code: {}", code);
2007                assert_eq!(code.len(), 6);
2008                assert!(code.chars().all(|c| c.is_ascii_digit()));
2009            }
2010            Err(e) => {
2011                println!("TOTP generation error: {}", e);
2012                // For now, just check that error is reasonable
2013                assert!(e.to_string().contains("base32") || e.to_string().contains("TOTP"));
2014            }
2015        }
2016    }
2017
2018    #[test]
2019    fn test_totp_code_generation_invalid_secret() {
2020        let config = mock_config();
2021        let http_client = Arc::new(reqwest::Client::new());
2022        let auth = Auth::new(config, http_client).unwrap();
2023
2024        // Test with invalid base32 secret
2025        let result = auth.generate_totp_code("invalid-secret");
2026        assert!(result.is_err());
2027    }
2028
2029    #[tokio::test]
2030    async fn test_token_validation_no_session() {
2031        let config = mock_config();
2032        let http_client = Arc::new(reqwest::Client::new());
2033        let auth = Auth::new(config, http_client).unwrap();
2034
2035        // No session should return false
2036        let is_valid = auth.validate_token_local().unwrap();
2037        assert!(!is_valid);
2038    }
2039
2040    #[test]
2041    fn test_time_until_expiry_no_session() {
2042        let config = mock_config();
2043        let http_client = Arc::new(reqwest::Client::new());
2044        let auth = Auth::new(config, http_client).unwrap();
2045
2046        // No session should return None
2047        let time = auth.time_until_expiry().unwrap();
2048        assert!(time.is_none());
2049    }
2050
2051    #[test]
2052    fn test_needs_refresh_no_session() {
2053        let config = mock_config();
2054        let http_client = Arc::new(reqwest::Client::new());
2055        let auth = Auth::new(config, http_client).unwrap();
2056
2057        // No session should return false
2058        let needs_refresh = auth.needs_refresh_with_buffer(300).unwrap();
2059        assert!(!needs_refresh);
2060    }
2061
2062    #[test]
2063    fn test_get_token_metadata_no_session() {
2064        let config = mock_config();
2065        let http_client = Arc::new(reqwest::Client::new());
2066        let auth = Auth::new(config, http_client).unwrap();
2067
2068        // No session should return None
2069        let metadata = auth.get_token_metadata().unwrap();
2070        assert!(metadata.is_none());
2071    }
2072
2073    #[test]
2074    fn test_token_metadata_structure() {
2075        let metadata = TokenMetadata {
2076            issued_at: Utc::now(),
2077            expires_at: Utc::now() + chrono::Duration::hours(1),
2078            refresh_count: 5,
2079            last_refresh_at: Some(Utc::now()),
2080            scopes: vec!["read".to_string(), "write".to_string()],
2081            device_id: Some("device-123".to_string()),
2082        };
2083
2084        assert_eq!(metadata.refresh_count, 5);
2085        assert!(metadata.last_refresh_at.is_some());
2086        assert_eq!(metadata.scopes.len(), 2);
2087        assert!(metadata.device_id.is_some());
2088    }
2089
2090    #[test]
2091    fn test_mfa_factor_structure() {
2092        let factor = MfaFactor {
2093            id: uuid::Uuid::new_v4(),
2094            factor_type: MfaMethod::Totp,
2095            friendly_name: "My Authenticator".to_string(),
2096            status: "verified".to_string(),
2097            created_at: Utc::now(),
2098            updated_at: Utc::now(),
2099            phone: None,
2100        };
2101
2102        assert_eq!(factor.factor_type, MfaMethod::Totp);
2103        assert_eq!(factor.friendly_name, "My Authenticator");
2104        assert_eq!(factor.status, "verified");
2105        assert!(factor.phone.is_none());
2106    }
2107
2108    #[test]
2109    fn test_enhanced_session_structure() {
2110        let user = User {
2111            id: uuid::Uuid::new_v4(),
2112            email: Some("user@example.com".to_string()),
2113            phone: None,
2114            email_confirmed_at: Some(Utc::now()),
2115            phone_confirmed_at: None,
2116            created_at: Utc::now(),
2117            updated_at: Utc::now(),
2118            last_sign_in_at: Some(Utc::now()),
2119            app_metadata: serde_json::json!({}),
2120            user_metadata: serde_json::json!({}),
2121            aud: "authenticated".to_string(),
2122            role: Some("authenticated".to_string()),
2123        };
2124
2125        let enhanced_session = EnhancedSession {
2126            access_token: "access-token".to_string(),
2127            refresh_token: "refresh-token".to_string(),
2128            expires_in: 3600,
2129            expires_at: Utc::now() + chrono::Duration::hours(1),
2130            token_type: "bearer".to_string(),
2131            user,
2132            token_metadata: None,
2133            mfa_verified: true,
2134            active_factors: vec![],
2135        };
2136
2137        assert!(enhanced_session.mfa_verified);
2138        assert_eq!(enhanced_session.active_factors.len(), 0);
2139        assert_eq!(enhanced_session.token_type, "bearer");
2140    }
2141}