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/// Authentication state event types
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum AuthEvent {
27    /// User signed in
28    SignedIn,
29    /// User signed out
30    SignedOut,
31    /// Session refreshed
32    TokenRefreshed,
33    /// User information updated
34    UserUpdated,
35    /// Password reset initiated
36    PasswordReset,
37}
38
39/// Authentication state change callback
40pub type AuthStateCallback = Box<dyn Fn(AuthEvent, Option<Session>) + Send + Sync + 'static>;
41
42/// Authentication event listener handle
43#[derive(Debug, Clone)]
44pub struct AuthEventHandle {
45    id: Uuid,
46    auth: Weak<Auth>,
47}
48
49impl AuthEventHandle {
50    /// Remove this event listener
51    pub fn remove(&self) {
52        if let Some(auth) = self.auth.upgrade() {
53            auth.remove_auth_listener(self.id);
54        }
55    }
56}
57
58/// Supported OAuth providers
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub enum OAuthProvider {
61    /// Google OAuth
62    #[serde(rename = "google")]
63    Google,
64    /// GitHub OAuth
65    #[serde(rename = "github")]
66    GitHub,
67    /// Discord OAuth
68    #[serde(rename = "discord")]
69    Discord,
70    /// Apple OAuth
71    #[serde(rename = "apple")]
72    Apple,
73    /// Twitter OAuth
74    #[serde(rename = "twitter")]
75    Twitter,
76    /// Facebook OAuth
77    #[serde(rename = "facebook")]
78    Facebook,
79    /// Microsoft OAuth
80    #[serde(rename = "azure")]
81    Microsoft,
82    /// LinkedIn OAuth
83    #[serde(rename = "linkedin_oidc")]
84    LinkedIn,
85}
86
87impl OAuthProvider {
88    /// Get provider name as string
89    pub fn as_str(&self) -> &'static str {
90        match self {
91            OAuthProvider::Google => "google",
92            OAuthProvider::GitHub => "github",
93            OAuthProvider::Discord => "discord",
94            OAuthProvider::Apple => "apple",
95            OAuthProvider::Twitter => "twitter",
96            OAuthProvider::Facebook => "facebook",
97            OAuthProvider::Microsoft => "azure",
98            OAuthProvider::LinkedIn => "linkedin_oidc",
99        }
100    }
101}
102
103/// OAuth sign-in options
104#[derive(Debug, Clone, Default)]
105pub struct OAuthOptions {
106    /// Custom redirect URL after sign-in
107    pub redirect_to: Option<String>,
108    /// Additional scopes to request
109    pub scopes: Option<Vec<String>>,
110    /// Additional provider-specific options
111    pub query_params: Option<std::collections::HashMap<String, String>>,
112}
113
114/// OAuth response with authorization URL
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct OAuthResponse {
117    /// Authorization URL to redirect user to
118    pub url: String,
119}
120
121/// Phone authentication request
122#[derive(Debug, Serialize)]
123struct PhoneSignUpRequest {
124    phone: String,
125    password: String,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    data: Option<serde_json::Value>,
128}
129
130/// Phone authentication sign-in request
131#[derive(Debug, Serialize)]
132struct PhoneSignInRequest {
133    phone: String,
134    password: String,
135}
136
137/// OTP verification request
138#[derive(Debug, Serialize)]
139struct OTPVerificationRequest {
140    phone: String,
141    token: String,
142    #[serde(rename = "type")]
143    verification_type: String,
144}
145
146/// Magic link request
147#[derive(Debug, Serialize)]
148struct MagicLinkRequest {
149    email: String,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    redirect_to: Option<String>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    data: Option<serde_json::Value>,
154}
155
156/// Anonymous sign-in request
157#[derive(Debug, Serialize)]
158struct AnonymousSignInRequest {
159    #[serde(skip_serializing_if = "Option::is_none")]
160    data: Option<serde_json::Value>,
161}
162
163/// Authentication client for handling user sessions and JWT tokens
164pub struct Auth {
165    http_client: Arc<HttpClient>,
166    config: Arc<SupabaseConfig>,
167    session: Arc<RwLock<Option<Session>>>,
168    event_listeners: Arc<RwLock<HashMap<Uuid, AuthStateCallback>>>,
169}
170
171impl Clone for Auth {
172    fn clone(&self) -> Self {
173        Self {
174            http_client: self.http_client.clone(),
175            config: self.config.clone(),
176            session: self.session.clone(),
177            event_listeners: Arc::new(RwLock::new(HashMap::new())),
178        }
179    }
180}
181
182impl std::fmt::Debug for Auth {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        f.debug_struct("Auth")
185            .field("http_client", &"HttpClient")
186            .field("config", &self.config)
187            .field("session", &self.session)
188            .field(
189                "event_listeners",
190                &format!(
191                    "HashMap<Uuid, Callback> with {} listeners",
192                    self.event_listeners.read().map(|l| l.len()).unwrap_or(0)
193                ),
194            )
195            .finish()
196    }
197}
198
199/// User information from Supabase Auth
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct User {
202    pub id: Uuid,
203    pub email: Option<String>,
204    pub phone: Option<String>,
205    pub email_confirmed_at: Option<Timestamp>,
206    pub phone_confirmed_at: Option<Timestamp>,
207    pub created_at: Timestamp,
208    pub updated_at: Timestamp,
209    pub last_sign_in_at: Option<Timestamp>,
210    pub app_metadata: serde_json::Value,
211    pub user_metadata: serde_json::Value,
212    pub aud: String,
213    pub role: Option<String>,
214}
215
216/// Authentication session containing user and tokens
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct Session {
219    pub access_token: String,
220    pub refresh_token: String,
221    pub expires_in: i64,
222    pub expires_at: Timestamp,
223    pub token_type: String,
224    pub user: User,
225}
226
227/// Response from authentication operations
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct AuthResponse {
230    pub user: Option<User>,
231    pub session: Option<Session>,
232}
233
234/// Sign up request payload
235#[derive(Debug, Serialize)]
236struct SignUpRequest {
237    email: String,
238    password: String,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    data: Option<serde_json::Value>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    redirect_to: Option<String>,
243}
244
245/// Sign in request payload
246#[derive(Debug, Serialize)]
247struct SignInRequest {
248    email: String,
249    password: String,
250}
251
252/// Password reset request payload
253#[derive(Debug, Serialize)]
254struct PasswordResetRequest {
255    email: String,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    redirect_to: Option<String>,
258}
259
260/// Token refresh request payload
261#[derive(Debug, Serialize)]
262struct RefreshTokenRequest {
263    refresh_token: String,
264}
265
266/// Update user request payload
267#[derive(Debug, Serialize)]
268struct UpdateUserRequest {
269    #[serde(skip_serializing_if = "Option::is_none")]
270    email: Option<String>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    password: Option<String>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    data: Option<serde_json::Value>,
275}
276
277impl Auth {
278    /// Create a new Auth instance
279    pub fn new(config: Arc<SupabaseConfig>, http_client: Arc<HttpClient>) -> Result<Self> {
280        debug!("Initializing Auth module");
281
282        Ok(Self {
283            http_client,
284            config,
285            session: Arc::new(RwLock::new(None)),
286            event_listeners: Arc::new(RwLock::new(HashMap::new())),
287        })
288    }
289
290    /// Sign up a new user with email and password
291    pub async fn sign_up_with_email_and_password(
292        &self,
293        email: &str,
294        password: &str,
295    ) -> Result<AuthResponse> {
296        self.sign_up_with_email_password_and_data(email, password, None, None)
297            .await
298    }
299
300    /// Sign up a new user with email, password, and optional metadata
301    pub async fn sign_up_with_email_password_and_data(
302        &self,
303        email: &str,
304        password: &str,
305        data: Option<serde_json::Value>,
306        redirect_to: Option<String>,
307    ) -> Result<AuthResponse> {
308        debug!("Signing up user with email: {}", email);
309
310        let payload = SignUpRequest {
311            email: email.to_string(),
312            password: password.to_string(),
313            data,
314            redirect_to,
315        };
316
317        let response = self
318            .http_client
319            .post(format!("{}/auth/v1/signup", self.config.url))
320            .json(&payload)
321            .send()
322            .await?;
323
324        if !response.status().is_success() {
325            let status = response.status();
326            let error_msg = match response.text().await {
327                Ok(text) => text,
328                Err(_) => format!("Sign up failed with status: {}", status),
329            };
330            return Err(Error::auth(error_msg));
331        }
332
333        let auth_response: AuthResponse = response.json().await?;
334
335        if let Some(ref session) = auth_response.session {
336            self.set_session(session.clone()).await?;
337            self.trigger_auth_event(AuthEvent::SignedIn);
338            info!("User signed up successfully");
339        }
340
341        Ok(auth_response)
342    }
343
344    /// Sign in with email and password
345    pub async fn sign_in_with_email_and_password(
346        &self,
347        email: &str,
348        password: &str,
349    ) -> Result<AuthResponse> {
350        debug!("Signing in user with email: {}", email);
351
352        let payload = SignInRequest {
353            email: email.to_string(),
354            password: password.to_string(),
355        };
356
357        let response = self
358            .http_client
359            .post(format!(
360                "{}/auth/v1/token?grant_type=password",
361                self.config.url
362            ))
363            .json(&payload)
364            .send()
365            .await?;
366
367        if !response.status().is_success() {
368            let status = response.status();
369            let error_msg = match response.text().await {
370                Ok(text) => text,
371                Err(_) => format!("Sign in failed with status: {}", status),
372            };
373            return Err(Error::auth(error_msg));
374        }
375
376        let auth_response: AuthResponse = response.json().await?;
377
378        if let Some(ref session) = auth_response.session {
379            self.set_session(session.clone()).await?;
380            self.trigger_auth_event(AuthEvent::SignedIn);
381            info!("User signed in successfully");
382        }
383
384        Ok(auth_response)
385    }
386
387    /// Sign out the current user
388    pub async fn sign_out(&self) -> Result<()> {
389        debug!("Signing out user");
390
391        let session = self.get_session()?;
392
393        let response = self
394            .http_client
395            .post(format!("{}/auth/v1/logout", self.config.url))
396            .header("Authorization", format!("Bearer {}", session.access_token))
397            .send()
398            .await?;
399
400        if !response.status().is_success() {
401            warn!("Sign out request failed with status: {}", response.status());
402        }
403
404        self.clear_session().await?;
405        self.trigger_auth_event(AuthEvent::SignedOut);
406        info!("User signed out successfully");
407
408        Ok(())
409    }
410
411    /// Reset password via email
412    pub async fn reset_password_for_email(&self, email: &str) -> Result<()> {
413        self.reset_password_for_email_with_redirect(email, None)
414            .await
415    }
416
417    /// Reset password via email with optional redirect URL
418    pub async fn reset_password_for_email_with_redirect(
419        &self,
420        email: &str,
421        redirect_to: Option<String>,
422    ) -> Result<()> {
423        debug!("Requesting password reset for email: {}", email);
424
425        let payload = PasswordResetRequest {
426            email: email.to_string(),
427            redirect_to,
428        };
429
430        let response = self
431            .http_client
432            .post(format!("{}/auth/v1/recover", self.config.url))
433            .json(&payload)
434            .send()
435            .await?;
436
437        if !response.status().is_success() {
438            let status = response.status();
439            let error_msg = match response.text().await {
440                Ok(text) => text,
441                Err(_) => format!("Password reset failed with status: {}", status),
442            };
443            return Err(Error::auth(error_msg));
444        }
445
446        info!("Password reset email sent successfully");
447        Ok(())
448    }
449
450    /// Update the current user's information
451    pub async fn update_user(
452        &self,
453        email: Option<String>,
454        password: Option<String>,
455        data: Option<serde_json::Value>,
456    ) -> Result<AuthResponse> {
457        debug!("Updating user information");
458
459        let session = self.get_session()?;
460
461        let payload = UpdateUserRequest {
462            email,
463            password,
464            data,
465        };
466
467        let response = self
468            .http_client
469            .put(format!("{}/auth/v1/user", self.config.url))
470            .header("Authorization", format!("Bearer {}", session.access_token))
471            .json(&payload)
472            .send()
473            .await?;
474
475        if !response.status().is_success() {
476            let status = response.status();
477            let error_msg = match response.text().await {
478                Ok(text) => text,
479                Err(_) => format!("User update failed with status: {}", status),
480            };
481            return Err(Error::auth(error_msg));
482        }
483
484        let auth_response: AuthResponse = response.json().await?;
485
486        if let Some(ref session) = auth_response.session {
487            self.set_session(session.clone()).await?;
488        }
489
490        info!("User updated successfully");
491        Ok(auth_response)
492    }
493
494    /// Refresh the current session token
495    pub async fn refresh_session(&self) -> Result<AuthResponse> {
496        debug!("Refreshing session token");
497
498        let current_session = self.get_session()?;
499
500        let payload = RefreshTokenRequest {
501            refresh_token: current_session.refresh_token.clone(),
502        };
503
504        let response = self
505            .http_client
506            .post(format!(
507                "{}/auth/v1/token?grant_type=refresh_token",
508                self.config.url
509            ))
510            .json(&payload)
511            .send()
512            .await?;
513
514        if !response.status().is_success() {
515            let status = response.status();
516            let error_msg = match response.text().await {
517                Ok(text) => text,
518                Err(_) => format!("Token refresh failed with status: {}", status),
519            };
520            return Err(Error::auth(error_msg));
521        }
522
523        let auth_response: AuthResponse = response.json().await?;
524
525        if let Some(ref session) = auth_response.session {
526            self.set_session(session.clone()).await?;
527            self.trigger_auth_event(AuthEvent::TokenRefreshed);
528            info!("Session refreshed successfully");
529        }
530
531        Ok(auth_response)
532    }
533
534    /// Get the current user information
535    pub async fn current_user(&self) -> Result<Option<User>> {
536        let session_guard = self
537            .session
538            .read()
539            .map_err(|_| Error::auth("Failed to read session"))?;
540        Ok(session_guard.as_ref().map(|s| s.user.clone()))
541    }
542
543    /// Get the current session
544    pub fn get_session(&self) -> Result<Session> {
545        let session_guard = self
546            .session
547            .read()
548            .map_err(|_| Error::auth("Failed to read session"))?;
549        session_guard
550            .as_ref()
551            .cloned()
552            .ok_or_else(|| Error::auth("No active session"))
553    }
554
555    /// Set a new session
556    pub async fn set_session(&self, session: Session) -> Result<()> {
557        let mut session_guard = self
558            .session
559            .write()
560            .map_err(|_| Error::auth("Failed to write session"))?;
561        *session_guard = Some(session);
562        Ok(())
563    }
564
565    /// Set session from JWT token
566    pub async fn set_session_token(&self, token: &str) -> Result<()> {
567        debug!("Setting session from token");
568
569        let user_response = self
570            .http_client
571            .get(format!("{}/auth/v1/user", self.config.url))
572            .header("Authorization", format!("Bearer {}", token))
573            .send()
574            .await?;
575
576        if !user_response.status().is_success() {
577            return Err(Error::auth("Invalid token"));
578        }
579
580        let user: User = user_response.json().await?;
581
582        let session = Session {
583            access_token: token.to_string(),
584            refresh_token: String::new(),
585            expires_in: 3600,
586            expires_at: Utc::now() + chrono::Duration::seconds(3600),
587            token_type: "bearer".to_string(),
588            user,
589        };
590
591        self.set_session(session).await?;
592        Ok(())
593    }
594
595    /// Clear the current session
596    pub async fn clear_session(&self) -> Result<()> {
597        let mut session_guard = self
598            .session
599            .write()
600            .map_err(|_| Error::auth("Failed to write session"))?;
601        *session_guard = None;
602        Ok(())
603    }
604
605    /// Check if the user is authenticated
606    pub fn is_authenticated(&self) -> bool {
607        let session_guard = self.session.read().unwrap_or_else(|_| {
608            warn!("Failed to read session lock");
609            self.session.read().unwrap()
610        });
611
612        match session_guard.as_ref() {
613            Some(session) => {
614                let now = Utc::now();
615                session.expires_at > now
616            }
617            None => false,
618        }
619    }
620
621    /// Check if the current token needs refresh
622    pub fn needs_refresh(&self) -> bool {
623        let session_guard = match self.session.read() {
624            Ok(guard) => guard,
625            Err(_) => {
626                warn!("Failed to read session lock");
627                return false;
628            }
629        };
630
631        match session_guard.as_ref() {
632            Some(session) => {
633                let now = Utc::now();
634                let buffer = chrono::Duration::minutes(5); // Refresh 5 minutes before expiry
635                session.expires_at < (now + buffer)
636            }
637            None => false,
638        }
639    }
640
641    /// Sign in with OAuth provider
642    ///
643    /// Returns a URL that the user should be redirected to for authentication.
644    /// After successful authentication, the user will be redirected back with the session.
645    ///
646    /// # Example
647    ///
648    /// ```rust
649    /// use supabase::auth::{OAuthProvider, OAuthOptions};
650    ///
651    /// # async fn example() -> supabase::Result<()> {
652    /// let client = supabase::Client::new("url", "key")?;
653    ///
654    /// let options = OAuthOptions {
655    ///     redirect_to: Some("https://myapp.com/callback".to_string()),
656    ///     scopes: Some(vec!["email".to_string(), "profile".to_string()]),
657    ///     ..Default::default()
658    /// };
659    ///
660    /// let response = client.auth().sign_in_with_oauth(OAuthProvider::Google, Some(options)).await?;
661    /// println!("Redirect to: {}", response.url);
662    /// # Ok(())
663    /// # }
664    /// ```
665    pub async fn sign_in_with_oauth(
666        &self,
667        provider: OAuthProvider,
668        options: Option<OAuthOptions>,
669    ) -> Result<OAuthResponse> {
670        debug!("Initiating OAuth sign-in with provider: {:?}", provider);
671
672        let mut url = format!(
673            "{}/auth/v1/authorize?provider={}",
674            self.config.url,
675            provider.as_str()
676        );
677
678        if let Some(opts) = options {
679            if let Some(redirect_to) = opts.redirect_to {
680                url.push_str(&format!(
681                    "&redirect_to={}",
682                    urlencoding::encode(&redirect_to)
683                ));
684            }
685
686            if let Some(scopes) = opts.scopes {
687                let scope_str = scopes.join(" ");
688                url.push_str(&format!("&scope={}", urlencoding::encode(&scope_str)));
689            }
690
691            if let Some(query_params) = opts.query_params {
692                for (key, value) in query_params {
693                    url.push_str(&format!(
694                        "&{}={}",
695                        urlencoding::encode(&key),
696                        urlencoding::encode(&value)
697                    ));
698                }
699            }
700        }
701
702        Ok(OAuthResponse { url })
703    }
704
705    /// Sign up with phone number
706    ///
707    /// # Example
708    ///
709    /// ```rust
710    /// # async fn example() -> supabase::Result<()> {
711    /// let client = supabase::Client::new("url", "key")?;
712    ///
713    /// let response = client.auth()
714    ///     .sign_up_with_phone("+1234567890", "securepassword", None)
715    ///     .await?;
716    ///
717    /// if let Some(user) = response.user {
718    ///     println!("User created: {:?}", user.phone);
719    /// }
720    /// # Ok(())
721    /// # }
722    /// ```
723    pub async fn sign_up_with_phone(
724        &self,
725        phone: &str,
726        password: &str,
727        data: Option<serde_json::Value>,
728    ) -> Result<AuthResponse> {
729        debug!("Signing up user with phone: {}", phone);
730
731        let payload = PhoneSignUpRequest {
732            phone: phone.to_string(),
733            password: password.to_string(),
734            data,
735        };
736
737        let response = self
738            .http_client
739            .post(format!("{}/auth/v1/signup", self.config.url))
740            .json(&payload)
741            .send()
742            .await?;
743
744        if !response.status().is_success() {
745            let status = response.status();
746            let error_msg = match response.text().await {
747                Ok(text) => text,
748                Err(_) => format!("Phone sign up failed with status: {}", status),
749            };
750            return Err(Error::auth(error_msg));
751        }
752
753        let auth_response: AuthResponse = response.json().await?;
754
755        if let Some(ref session) = auth_response.session {
756            self.set_session(session.clone()).await?;
757            self.trigger_auth_event(AuthEvent::SignedIn);
758            info!("User signed up with phone successfully");
759        }
760
761        Ok(auth_response)
762    }
763
764    /// Sign in with phone number
765    ///
766    /// # Example
767    ///
768    /// ```rust
769    /// # async fn example() -> supabase::Result<()> {
770    /// let client = supabase::Client::new("url", "key")?;
771    ///
772    /// let response = client.auth()
773    ///     .sign_in_with_phone("+1234567890", "securepassword")
774    ///     .await?;
775    ///
776    /// if let Some(user) = response.user {
777    ///     println!("User signed in: {:?}", user.phone);
778    /// }
779    /// # Ok(())
780    /// # }
781    /// ```
782    pub async fn sign_in_with_phone(&self, phone: &str, password: &str) -> Result<AuthResponse> {
783        debug!("Signing in user with phone: {}", phone);
784
785        let payload = PhoneSignInRequest {
786            phone: phone.to_string(),
787            password: password.to_string(),
788        };
789
790        let response = self
791            .http_client
792            .post(format!(
793                "{}/auth/v1/token?grant_type=password",
794                self.config.url
795            ))
796            .json(&payload)
797            .send()
798            .await?;
799
800        if !response.status().is_success() {
801            let status = response.status();
802            let error_msg = match response.text().await {
803                Ok(text) => text,
804                Err(_) => format!("Phone sign in failed with status: {}", status),
805            };
806            return Err(Error::auth(error_msg));
807        }
808
809        let auth_response: AuthResponse = response.json().await?;
810
811        if let Some(ref session) = auth_response.session {
812            self.set_session(session.clone()).await?;
813            self.trigger_auth_event(AuthEvent::SignedIn);
814            info!("User signed in with phone successfully");
815        }
816
817        Ok(auth_response)
818    }
819
820    /// Verify OTP token
821    ///
822    /// # Example
823    ///
824    /// ```rust
825    /// # async fn example() -> supabase::Result<()> {
826    /// let client = supabase::Client::new("url", "key")?;
827    ///
828    /// let response = client.auth()
829    ///     .verify_otp("+1234567890", "123456", "sms")
830    ///     .await?;
831    ///
832    /// if let Some(session) = response.session {
833    ///     println!("OTP verified, user signed in");
834    /// }
835    /// # Ok(())
836    /// # }
837    /// ```
838    pub async fn verify_otp(
839        &self,
840        phone: &str,
841        token: &str,
842        verification_type: &str,
843    ) -> Result<AuthResponse> {
844        debug!("Verifying OTP for phone: {}", phone);
845
846        let payload = OTPVerificationRequest {
847            phone: phone.to_string(),
848            token: token.to_string(),
849            verification_type: verification_type.to_string(),
850        };
851
852        let response = self
853            .http_client
854            .post(format!("{}/auth/v1/verify", self.config.url))
855            .json(&payload)
856            .send()
857            .await?;
858
859        if !response.status().is_success() {
860            let status = response.status();
861            let error_msg = match response.text().await {
862                Ok(text) => text,
863                Err(_) => format!("OTP verification failed with status: {}", status),
864            };
865            return Err(Error::auth(error_msg));
866        }
867
868        let auth_response: AuthResponse = response.json().await?;
869
870        if let Some(ref session) = auth_response.session {
871            self.set_session(session.clone()).await?;
872            self.trigger_auth_event(AuthEvent::SignedIn);
873            info!("OTP verified successfully");
874        }
875
876        Ok(auth_response)
877    }
878
879    /// Send magic link for passwordless authentication
880    ///
881    /// # Example
882    ///
883    /// ```rust
884    /// # async fn example() -> supabase::Result<()> {
885    /// let client = supabase::Client::new("url", "key")?;
886    ///
887    /// client.auth()
888    ///     .sign_in_with_magic_link("user@example.com", Some("https://myapp.com/callback".to_string()), None)
889    ///     .await?;
890    ///
891    /// println!("Magic link sent to email");
892    /// # Ok(())
893    /// # }
894    /// ```
895    pub async fn sign_in_with_magic_link(
896        &self,
897        email: &str,
898        redirect_to: Option<String>,
899        data: Option<serde_json::Value>,
900    ) -> Result<()> {
901        debug!("Sending magic link to email: {}", email);
902
903        let payload = MagicLinkRequest {
904            email: email.to_string(),
905            redirect_to,
906            data,
907        };
908
909        let response = self
910            .http_client
911            .post(format!("{}/auth/v1/magiclink", self.config.url))
912            .json(&payload)
913            .send()
914            .await?;
915
916        if !response.status().is_success() {
917            let status = response.status();
918            let error_msg = match response.text().await {
919                Ok(text) => text,
920                Err(_) => format!("Magic link request failed with status: {}", status),
921            };
922            return Err(Error::auth(error_msg));
923        }
924
925        info!("Magic link sent successfully");
926        Ok(())
927    }
928
929    /// Sign in anonymously
930    ///
931    /// Creates a temporary anonymous user session that can be converted to a permanent account later.
932    ///
933    /// # Example
934    ///
935    /// ```rust
936    /// # async fn example() -> supabase::Result<()> {
937    /// let client = supabase::Client::new("url", "key")?;
938    ///
939    /// let response = client.auth()
940    ///     .sign_in_anonymously(None)
941    ///     .await?;
942    ///
943    /// if let Some(user) = response.user {
944    ///     println!("Anonymous user created: {}", user.id);
945    /// }
946    /// # Ok(())
947    /// # }
948    /// ```
949    pub async fn sign_in_anonymously(
950        &self,
951        data: Option<serde_json::Value>,
952    ) -> Result<AuthResponse> {
953        debug!("Creating anonymous user session");
954
955        let payload = AnonymousSignInRequest { data };
956
957        let response = self
958            .http_client
959            .post(format!("{}/auth/v1/signup", self.config.url))
960            .header("Authorization", format!("Bearer {}", self.config.key))
961            .json(&payload)
962            .send()
963            .await?;
964
965        if !response.status().is_success() {
966            let status = response.status();
967            let error_msg = match response.text().await {
968                Ok(text) => text,
969                Err(_) => format!("Anonymous sign in failed with status: {}", status),
970            };
971            return Err(Error::auth(error_msg));
972        }
973
974        let auth_response: AuthResponse = response.json().await?;
975
976        if let Some(ref session) = auth_response.session {
977            self.set_session(session.clone()).await?;
978            self.trigger_auth_event(AuthEvent::SignedIn);
979            info!("Anonymous user session created successfully");
980        }
981
982        Ok(auth_response)
983    }
984
985    /// Enhanced password recovery with custom redirect and options
986    ///
987    /// # Example
988    ///
989    /// ```rust
990    /// # async fn example() -> supabase::Result<()> {
991    /// let client = supabase::Client::new("url", "key")?;
992    ///
993    /// client.auth()
994    ///     .reset_password_for_email_enhanced("user@example.com", Some("https://myapp.com/reset".to_string()))
995    ///     .await?;
996    ///
997    /// println!("Password reset email sent");
998    /// # Ok(())
999    /// # }
1000    /// ```
1001    pub async fn reset_password_for_email_enhanced(
1002        &self,
1003        email: &str,
1004        redirect_to: Option<String>,
1005    ) -> Result<()> {
1006        debug!("Initiating enhanced password recovery for email: {}", email);
1007
1008        let payload = PasswordResetRequest {
1009            email: email.to_string(),
1010            redirect_to,
1011        };
1012
1013        let response = self
1014            .http_client
1015            .post(format!("{}/auth/v1/recover", self.config.url))
1016            .json(&payload)
1017            .send()
1018            .await?;
1019
1020        if !response.status().is_success() {
1021            let status = response.status();
1022            let error_msg = match response.text().await {
1023                Ok(text) => text,
1024                Err(_) => format!("Enhanced password recovery failed with status: {}", status),
1025            };
1026            return Err(Error::auth(error_msg));
1027        }
1028
1029        self.trigger_auth_event(AuthEvent::PasswordReset);
1030        info!("Enhanced password recovery email sent successfully");
1031        Ok(())
1032    }
1033
1034    /// Subscribe to authentication state changes
1035    ///
1036    /// Returns a handle that can be used to remove the listener later.
1037    ///
1038    /// # Example
1039    ///
1040    /// ```rust
1041    /// use supabase::auth::AuthEvent;
1042    ///
1043    /// # async fn example() -> supabase::Result<()> {
1044    /// let client = supabase::Client::new("url", "key")?;
1045    ///
1046    /// let handle = client.auth().on_auth_state_change(|event, session| {
1047    ///     match event {
1048    ///         AuthEvent::SignedIn => {
1049    ///             if let Some(session) = session {
1050    ///                 println!("User signed in: {}", session.user.email.unwrap_or_default());
1051    ///             }
1052    ///         }
1053    ///         AuthEvent::SignedOut => println!("User signed out"),
1054    ///         AuthEvent::TokenRefreshed => println!("Token refreshed"),
1055    ///         _ => {}
1056    ///     }
1057    /// });
1058    ///
1059    /// // Later remove the listener
1060    /// handle.remove();
1061    /// # Ok(())
1062    /// # }
1063    /// ```
1064    pub fn on_auth_state_change<F>(&self, callback: F) -> AuthEventHandle
1065    where
1066        F: Fn(AuthEvent, Option<Session>) + Send + Sync + 'static,
1067    {
1068        let id = Uuid::new_v4();
1069        let callback = Box::new(callback);
1070
1071        if let Ok(mut listeners) = self.event_listeners.write() {
1072            listeners.insert(id, callback);
1073        }
1074
1075        AuthEventHandle {
1076            id,
1077            auth: Arc::downgrade(&Arc::new(self.clone())),
1078        }
1079    }
1080
1081    /// Remove an authentication state listener
1082    pub fn remove_auth_listener(&self, id: Uuid) {
1083        if let Ok(mut listeners) = self.event_listeners.write() {
1084            listeners.remove(&id);
1085        }
1086    }
1087
1088    /// Trigger authentication state change event
1089    fn trigger_auth_event(&self, event: AuthEvent) {
1090        let session = match self.session.read() {
1091            Ok(guard) => guard.clone(),
1092            Err(_) => {
1093                warn!("Failed to read session for event trigger");
1094                return;
1095            }
1096        };
1097
1098        let listeners = match self.event_listeners.read() {
1099            Ok(guard) => guard,
1100            Err(_) => {
1101                warn!("Failed to read event listeners");
1102                return;
1103            }
1104        };
1105
1106        for callback in listeners.values() {
1107            callback(event.clone(), session.clone());
1108        }
1109    }
1110}