1use 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
24use base32;
26use phonenumber::Mode;
27use qrcode::QrCode;
28use totp_rs::{Algorithm, TOTP};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum AuthEvent {
33 SignedIn,
35 SignedOut,
37 TokenRefreshed,
39 UserUpdated,
41 PasswordReset,
43 MfaChallengeRequired,
45 MfaChallengeCompleted,
47 MfaEnabled,
49 MfaDisabled,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub enum MfaMethod {
56 #[serde(rename = "totp")]
58 Totp,
59 #[serde(rename = "sms")]
61 Sms,
62 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub enum MfaChallengeStatus {
80 #[serde(rename = "pending")]
82 Pending,
83 #[serde(rename = "completed")]
85 Completed,
86 #[serde(rename = "expired")]
88 Expired,
89 #[serde(rename = "cancelled")]
91 Cancelled,
92}
93
94#[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, 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#[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#[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#[derive(Debug, Serialize)]
130pub struct MfaVerificationRequest {
131 pub factor_id: Uuid,
132 pub challenge_id: Uuid,
133 pub code: String,
134}
135
136#[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 pub fn new(phone: &str, _default_region: Option<&str>) -> Result<Self> {
149 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 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#[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#[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
197pub type AuthStateCallback = Box<dyn Fn(AuthEvent, Option<Session>) + Send + Sync + 'static>;
199
200#[derive(Debug, Clone)]
202pub struct AuthEventHandle {
203 id: Uuid,
204 auth: Weak<Auth>,
205}
206
207impl AuthEventHandle {
208 pub fn remove(&self) {
210 if let Some(auth) = self.auth.upgrade() {
211 auth.remove_auth_listener(self.id);
212 }
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub enum OAuthProvider {
219 #[serde(rename = "google")]
221 Google,
222 #[serde(rename = "github")]
224 GitHub,
225 #[serde(rename = "discord")]
227 Discord,
228 #[serde(rename = "apple")]
230 Apple,
231 #[serde(rename = "twitter")]
233 Twitter,
234 #[serde(rename = "facebook")]
236 Facebook,
237 #[serde(rename = "azure")]
239 Microsoft,
240 #[serde(rename = "linkedin_oidc")]
242 LinkedIn,
243}
244
245impl OAuthProvider {
246 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#[derive(Debug, Clone, Default)]
263pub struct OAuthOptions {
264 pub redirect_to: Option<String>,
266 pub scopes: Option<Vec<String>>,
268 pub query_params: Option<std::collections::HashMap<String, String>>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct OAuthResponse {
275 pub url: String,
277}
278
279#[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#[derive(Debug, Serialize)]
290struct PhoneSignInRequest {
291 phone: String,
292 password: String,
293}
294
295#[derive(Debug, Serialize)]
297struct OTPVerificationRequest {
298 phone: String,
299 token: String,
300 #[serde(rename = "type")]
301 verification_type: String,
302}
303
304#[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#[derive(Debug, Serialize)]
316struct AnonymousSignInRequest {
317 #[serde(skip_serializing_if = "Option::is_none")]
318 data: Option<serde_json::Value>,
319}
320
321pub 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct Session {
377 pub access_token: String,
378 pub refresh_token: String,
379 pub expires_in: i64,
380 #[serde(with = "chrono::serde::ts_seconds")]
381 pub expires_at: Timestamp,
382 pub token_type: String,
383 pub user: User,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct AuthResponse {
389 pub user: Option<User>,
390 pub session: Option<Session>,
391}
392
393#[derive(Debug, Serialize)]
395struct SignUpRequest {
396 email: String,
397 password: String,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 data: Option<serde_json::Value>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 redirect_to: Option<String>,
402}
403
404#[derive(Debug, Serialize)]
406struct SignInRequest {
407 email: String,
408 password: String,
409}
410
411#[derive(Debug, Serialize)]
413struct PasswordResetRequest {
414 email: String,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 redirect_to: Option<String>,
417}
418
419#[derive(Debug, Serialize)]
421struct RefreshTokenRequest {
422 refresh_token: String,
423}
424
425#[derive(Debug, Serialize)]
427struct UpdateUserRequest {
428 #[serde(skip_serializing_if = "Option::is_none")]
429 email: Option<String>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 password: Option<String>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 data: Option<serde_json::Value>,
434}
435
436impl Auth {
437 pub fn new(config: Arc<SupabaseConfig>, http_client: Arc<HttpClient>) -> Result<Self> {
439 debug!("Initializing Auth module");
440
441 Ok(Self {
442 http_client,
443 config,
444 session: Arc::new(RwLock::new(None)),
445 event_listeners: Arc::new(RwLock::new(HashMap::new())),
446 })
447 }
448
449 pub async fn sign_up_with_email_and_password(
451 &self,
452 email: &str,
453 password: &str,
454 ) -> Result<AuthResponse> {
455 self.sign_up_with_email_password_and_data(email, password, None, None)
456 .await
457 }
458
459 pub async fn sign_up_with_email_password_and_data(
461 &self,
462 email: &str,
463 password: &str,
464 data: Option<serde_json::Value>,
465 redirect_to: Option<String>,
466 ) -> Result<AuthResponse> {
467 debug!("Signing up user with email: {}", email);
468
469 let payload = SignUpRequest {
470 email: email.to_string(),
471 password: password.to_string(),
472 data,
473 redirect_to,
474 };
475
476 let response = self
477 .http_client
478 .post(format!("{}/auth/v1/signup", self.config.url))
479 .json(&payload)
480 .send()
481 .await?;
482
483 if !response.status().is_success() {
484 let status = response.status();
485 let error_msg = match response.text().await {
486 Ok(text) => text,
487 Err(_) => format!("Sign up failed with status: {}", status),
488 };
489 return Err(Error::auth(error_msg));
490 }
491
492 let auth_response_body = response.text().await?;
493
494 let mut auth_response = serde_json::from_str::<AuthResponse>(auth_response_body.as_str())?;
495 auth_response.session = serde_json::from_str::<Session>(auth_response_body.as_str())
496 .inspect_err(|err| warn!("No session: {}", err.to_string()))
497 .ok();
498
499 if let Some(ref session) = auth_response.session {
500 self.set_session(session.clone()).await?;
501 self.trigger_auth_event(AuthEvent::SignedIn);
502 info!("User signed up successfully");
503 }
504
505 Ok(auth_response)
506 }
507
508 pub async fn sign_in_with_email_and_password(
510 &self,
511 email: &str,
512 password: &str,
513 ) -> Result<AuthResponse> {
514 debug!("Signing in user with email: {}", email);
515
516 let payload = SignInRequest {
517 email: email.to_string(),
518 password: password.to_string(),
519 };
520
521 let response = self
522 .http_client
523 .post(format!(
524 "{}/auth/v1/token?grant_type=password",
525 self.config.url
526 ))
527 .json(&payload)
528 .send()
529 .await?;
530
531 if !response.status().is_success() {
532 let status = response.status();
533 let error_msg = match response.text().await {
534 Ok(text) => text,
535 Err(_) => format!("Sign in failed with status: {}", status),
536 };
537 return Err(Error::auth(error_msg));
538 }
539
540 let auth_response_body = response.text().await?;
541
542 let mut auth_response = serde_json::from_str::<AuthResponse>(auth_response_body.as_str())?;
543 auth_response.session = serde_json::from_str::<Session>(auth_response_body.as_str())
544 .inspect_err(|err| warn!("No session: {}", err.to_string()))
545 .ok();
546
547 if let Some(ref session) = auth_response.session {
548 self.set_session(session.clone()).await?;
549 self.trigger_auth_event(AuthEvent::SignedIn);
550 info!("User signed in successfully");
551 }
552
553 Ok(auth_response)
554 }
555
556 pub async fn sign_out(&self) -> Result<()> {
558 debug!("Signing out user");
559
560 let session = self.get_session()?;
561
562 let response = self
563 .http_client
564 .post(format!("{}/auth/v1/logout", self.config.url))
565 .header("Authorization", format!("Bearer {}", session.access_token))
566 .send()
567 .await?;
568
569 if !response.status().is_success() {
570 warn!("Sign out request failed with status: {}", response.status());
571 }
572
573 self.clear_session().await?;
574 self.trigger_auth_event(AuthEvent::SignedOut);
575 info!("User signed out successfully");
576
577 Ok(())
578 }
579
580 pub async fn reset_password_for_email(&self, email: &str) -> Result<()> {
582 self.reset_password_for_email_with_redirect(email, None)
583 .await
584 }
585
586 pub async fn reset_password_for_email_with_redirect(
588 &self,
589 email: &str,
590 redirect_to: Option<String>,
591 ) -> Result<()> {
592 debug!("Requesting password reset for email: {}", email);
593
594 let payload = PasswordResetRequest {
595 email: email.to_string(),
596 redirect_to,
597 };
598
599 let response = self
600 .http_client
601 .post(format!("{}/auth/v1/recover", self.config.url))
602 .json(&payload)
603 .send()
604 .await?;
605
606 if !response.status().is_success() {
607 let status = response.status();
608 let error_msg = match response.text().await {
609 Ok(text) => text,
610 Err(_) => format!("Password reset failed with status: {}", status),
611 };
612 return Err(Error::auth(error_msg));
613 }
614
615 info!("Password reset email sent successfully");
616 Ok(())
617 }
618
619 pub async fn update_user(
621 &self,
622 email: Option<String>,
623 password: Option<String>,
624 data: Option<serde_json::Value>,
625 ) -> Result<AuthResponse> {
626 debug!("Updating user information");
627
628 let session = self.get_session()?;
629
630 let payload = UpdateUserRequest {
631 email,
632 password,
633 data,
634 };
635
636 let response = self
637 .http_client
638 .put(format!("{}/auth/v1/user", self.config.url))
639 .header("Authorization", format!("Bearer {}", session.access_token))
640 .json(&payload)
641 .send()
642 .await?;
643
644 if !response.status().is_success() {
645 let status = response.status();
646 let error_msg = match response.text().await {
647 Ok(text) => text,
648 Err(_) => format!("User update failed with status: {}", status),
649 };
650 return Err(Error::auth(error_msg));
651 }
652
653 let auth_response: AuthResponse = response.json().await?;
654
655 if let Some(ref session) = auth_response.session {
656 self.set_session(session.clone()).await?;
657 }
658
659 info!("User updated successfully");
660 Ok(auth_response)
661 }
662
663 pub async fn refresh_session(&self) -> Result<AuthResponse> {
665 debug!("Refreshing session token");
666
667 let current_session = self.get_session()?;
668
669 let payload = RefreshTokenRequest {
670 refresh_token: current_session.refresh_token.clone(),
671 };
672
673 let response = self
674 .http_client
675 .post(format!(
676 "{}/auth/v1/token?grant_type=refresh_token",
677 self.config.url
678 ))
679 .json(&payload)
680 .send()
681 .await?;
682
683 if !response.status().is_success() {
684 let status = response.status();
685 let error_msg = match response.text().await {
686 Ok(text) => text,
687 Err(_) => format!("Token refresh failed with status: {}", status),
688 };
689 return Err(Error::auth(error_msg));
690 }
691
692 let auth_response_body = response.text().await?;
693
694 let mut auth_response = serde_json::from_str::<AuthResponse>(auth_response_body.as_str())?;
695 auth_response.session = serde_json::from_str::<Session>(auth_response_body.as_str())
696 .inspect_err(|err| warn!("No session: {}", err.to_string()))
697 .ok();
698
699 if let Some(ref session) = auth_response.session {
700 self.set_session(session.clone()).await?;
701 self.trigger_auth_event(AuthEvent::TokenRefreshed);
702 info!("Session refreshed successfully");
703 }
704
705 Ok(auth_response)
706 }
707
708 pub async fn current_user(&self) -> Result<Option<User>> {
710 let session_guard = self
711 .session
712 .read()
713 .map_err(|_| Error::auth("Failed to read session"))?;
714 Ok(session_guard.as_ref().map(|s| s.user.clone()))
715 }
716
717 pub fn get_session(&self) -> Result<Session> {
719 let session_guard = self
720 .session
721 .read()
722 .map_err(|_| Error::auth("Failed to read session"))?;
723 session_guard
724 .as_ref()
725 .cloned()
726 .ok_or_else(|| Error::auth("No active session"))
727 }
728
729 pub async fn set_session(&self, session: Session) -> Result<()> {
731 let mut session_guard = self
732 .session
733 .write()
734 .map_err(|_| Error::auth("Failed to write session"))?;
735 *session_guard = Some(session);
736 Ok(())
737 }
738
739 pub async fn set_session_token(&self, token: &str) -> Result<()> {
741 debug!("Setting session from token");
742
743 let user_response = self
744 .http_client
745 .get(format!("{}/auth/v1/user", self.config.url))
746 .header("Authorization", format!("Bearer {}", token))
747 .send()
748 .await?;
749
750 if !user_response.status().is_success() {
751 return Err(Error::auth("Invalid token"));
752 }
753
754 let user: User = user_response.json().await?;
755
756 let session = Session {
757 access_token: token.to_string(),
758 refresh_token: String::new(),
759 expires_in: 3600,
760 expires_at: Utc::now() + chrono::Duration::seconds(3600),
761 token_type: "bearer".to_string(),
762 user,
763 };
764
765 self.set_session(session).await?;
766 Ok(())
767 }
768
769 pub async fn clear_session(&self) -> Result<()> {
771 let mut session_guard = self
772 .session
773 .write()
774 .map_err(|_| Error::auth("Failed to write session"))?;
775 *session_guard = None;
776 Ok(())
777 }
778
779 pub fn is_authenticated(&self) -> bool {
781 let session_guard = self.session.read().unwrap_or_else(|_| {
782 warn!("Failed to read session lock");
783 self.session.read().unwrap()
784 });
785
786 match session_guard.as_ref() {
787 Some(session) => {
788 let now = Utc::now();
789 session.expires_at > now
790 }
791 None => false,
792 }
793 }
794
795 pub fn needs_refresh(&self) -> bool {
797 let session_guard = match self.session.read() {
798 Ok(guard) => guard,
799 Err(_) => {
800 warn!("Failed to read session lock");
801 return false;
802 }
803 };
804
805 match session_guard.as_ref() {
806 Some(session) => {
807 let now = Utc::now();
808 let buffer = chrono::Duration::minutes(5); session.expires_at < (now + buffer)
810 }
811 None => false,
812 }
813 }
814
815 pub async fn sign_in_with_oauth(
840 &self,
841 provider: OAuthProvider,
842 options: Option<OAuthOptions>,
843 ) -> Result<OAuthResponse> {
844 debug!("Initiating OAuth sign-in with provider: {:?}", provider);
845
846 let mut url = format!(
847 "{}/auth/v1/authorize?provider={}",
848 self.config.url,
849 provider.as_str()
850 );
851
852 if let Some(opts) = options {
853 if let Some(redirect_to) = opts.redirect_to {
854 url.push_str(&format!(
855 "&redirect_to={}",
856 urlencoding::encode(&redirect_to)
857 ));
858 }
859
860 if let Some(scopes) = opts.scopes {
861 let scope_str = scopes.join(" ");
862 url.push_str(&format!("&scope={}", urlencoding::encode(&scope_str)));
863 }
864
865 if let Some(query_params) = opts.query_params {
866 for (key, value) in query_params {
867 url.push_str(&format!(
868 "&{}={}",
869 urlencoding::encode(&key),
870 urlencoding::encode(&value)
871 ));
872 }
873 }
874 }
875
876 Ok(OAuthResponse { url })
877 }
878
879 pub async fn sign_up_with_phone(
898 &self,
899 phone: &str,
900 password: &str,
901 data: Option<serde_json::Value>,
902 ) -> Result<AuthResponse> {
903 debug!("Signing up user with phone: {}", phone);
904
905 let payload = PhoneSignUpRequest {
906 phone: phone.to_string(),
907 password: password.to_string(),
908 data,
909 };
910
911 let response = self
912 .http_client
913 .post(format!("{}/auth/v1/signup", self.config.url))
914 .json(&payload)
915 .send()
916 .await?;
917
918 if !response.status().is_success() {
919 let status = response.status();
920 let error_msg = match response.text().await {
921 Ok(text) => text,
922 Err(_) => format!("Phone sign up failed with status: {}", status),
923 };
924 return Err(Error::auth(error_msg));
925 }
926
927 let auth_response: AuthResponse = response.json().await?;
928
929 if let Some(ref session) = auth_response.session {
930 self.set_session(session.clone()).await?;
931 self.trigger_auth_event(AuthEvent::SignedIn);
932 info!("User signed up with phone successfully");
933 }
934
935 Ok(auth_response)
936 }
937
938 pub async fn sign_in_with_phone(&self, phone: &str, password: &str) -> Result<AuthResponse> {
957 debug!("Signing in user with phone: {}", phone);
958
959 let payload = PhoneSignInRequest {
960 phone: phone.to_string(),
961 password: password.to_string(),
962 };
963
964 let response = self
965 .http_client
966 .post(format!(
967 "{}/auth/v1/token?grant_type=password",
968 self.config.url
969 ))
970 .json(&payload)
971 .send()
972 .await?;
973
974 if !response.status().is_success() {
975 let status = response.status();
976 let error_msg = match response.text().await {
977 Ok(text) => text,
978 Err(_) => format!("Phone sign in failed with status: {}", status),
979 };
980 return Err(Error::auth(error_msg));
981 }
982
983 let auth_response_body = response.text().await?;
984
985 let mut auth_response = serde_json::from_str::<AuthResponse>(auth_response_body.as_str())?;
986 auth_response.session = serde_json::from_str::<Session>(auth_response_body.as_str())
987 .inspect_err(|err| warn!("No session: {}", err.to_string()))
988 .ok();
989
990 if let Some(ref session) = auth_response.session {
991 self.set_session(session.clone()).await?;
992 self.trigger_auth_event(AuthEvent::SignedIn);
993 info!("User signed in with phone successfully");
994 }
995
996 Ok(auth_response)
997 }
998
999 pub async fn verify_otp(
1018 &self,
1019 phone: &str,
1020 token: &str,
1021 verification_type: &str,
1022 ) -> Result<AuthResponse> {
1023 debug!("Verifying OTP for phone: {}", phone);
1024
1025 let payload = OTPVerificationRequest {
1026 phone: phone.to_string(),
1027 token: token.to_string(),
1028 verification_type: verification_type.to_string(),
1029 };
1030
1031 let response = self
1032 .http_client
1033 .post(format!("{}/auth/v1/verify", self.config.url))
1034 .json(&payload)
1035 .send()
1036 .await?;
1037
1038 if !response.status().is_success() {
1039 let status = response.status();
1040 let error_msg = match response.text().await {
1041 Ok(text) => text,
1042 Err(_) => format!("OTP verification failed with status: {}", status),
1043 };
1044 return Err(Error::auth(error_msg));
1045 }
1046
1047 let auth_response: AuthResponse = response.json().await?;
1048
1049 if let Some(ref session) = auth_response.session {
1050 self.set_session(session.clone()).await?;
1051 self.trigger_auth_event(AuthEvent::SignedIn);
1052 info!("OTP verified successfully");
1053 }
1054
1055 Ok(auth_response)
1056 }
1057
1058 pub async fn sign_in_with_magic_link(
1075 &self,
1076 email: &str,
1077 redirect_to: Option<String>,
1078 data: Option<serde_json::Value>,
1079 ) -> Result<()> {
1080 debug!("Sending magic link to email: {}", email);
1081
1082 let payload = MagicLinkRequest {
1083 email: email.to_string(),
1084 redirect_to,
1085 data,
1086 };
1087
1088 let response = self
1089 .http_client
1090 .post(format!("{}/auth/v1/magiclink", self.config.url))
1091 .json(&payload)
1092 .send()
1093 .await?;
1094
1095 if !response.status().is_success() {
1096 let status = response.status();
1097 let error_msg = match response.text().await {
1098 Ok(text) => text,
1099 Err(_) => format!("Magic link request failed with status: {}", status),
1100 };
1101 return Err(Error::auth(error_msg));
1102 }
1103
1104 info!("Magic link sent successfully");
1105 Ok(())
1106 }
1107
1108 pub async fn sign_in_anonymously(
1129 &self,
1130 data: Option<serde_json::Value>,
1131 ) -> Result<AuthResponse> {
1132 debug!("Creating anonymous user session");
1133
1134 let payload = AnonymousSignInRequest { data };
1135
1136 let response = self
1137 .http_client
1138 .post(format!("{}/auth/v1/signup", self.config.url))
1139 .header("Authorization", format!("Bearer {}", self.config.key))
1140 .json(&payload)
1141 .send()
1142 .await?;
1143
1144 if !response.status().is_success() {
1145 let status = response.status();
1146 let error_msg = match response.text().await {
1147 Ok(text) => text,
1148 Err(_) => format!("Anonymous sign in failed with status: {}", status),
1149 };
1150 return Err(Error::auth(error_msg));
1151 }
1152
1153 let auth_response: AuthResponse = response.json().await?;
1154
1155 if let Some(ref session) = auth_response.session {
1156 self.set_session(session.clone()).await?;
1157 self.trigger_auth_event(AuthEvent::SignedIn);
1158 info!("Anonymous user session created successfully");
1159 }
1160
1161 Ok(auth_response)
1162 }
1163
1164 pub async fn reset_password_for_email_enhanced(
1181 &self,
1182 email: &str,
1183 redirect_to: Option<String>,
1184 ) -> Result<()> {
1185 debug!("Initiating enhanced password recovery for email: {}", email);
1186
1187 let payload = PasswordResetRequest {
1188 email: email.to_string(),
1189 redirect_to,
1190 };
1191
1192 let response = self
1193 .http_client
1194 .post(format!("{}/auth/v1/recover", self.config.url))
1195 .json(&payload)
1196 .send()
1197 .await?;
1198
1199 if !response.status().is_success() {
1200 let status = response.status();
1201 let error_msg = match response.text().await {
1202 Ok(text) => text,
1203 Err(_) => format!("Enhanced password recovery failed with status: {}", status),
1204 };
1205 return Err(Error::auth(error_msg));
1206 }
1207
1208 self.trigger_auth_event(AuthEvent::PasswordReset);
1209 info!("Enhanced password recovery email sent successfully");
1210 Ok(())
1211 }
1212
1213 pub fn on_auth_state_change<F>(&self, callback: F) -> AuthEventHandle
1244 where
1245 F: Fn(AuthEvent, Option<Session>) + Send + Sync + 'static,
1246 {
1247 let id = Uuid::new_v4();
1248 let callback = Box::new(callback);
1249
1250 if let Ok(mut listeners) = self.event_listeners.write() {
1251 listeners.insert(id, callback);
1252 }
1253
1254 AuthEventHandle {
1255 id,
1256 auth: Arc::downgrade(&Arc::new(self.clone())),
1257 }
1258 }
1259
1260 pub fn remove_auth_listener(&self, id: Uuid) {
1262 if let Ok(mut listeners) = self.event_listeners.write() {
1263 listeners.remove(&id);
1264 }
1265 }
1266
1267 fn trigger_auth_event(&self, event: AuthEvent) {
1269 let session = match self.session.read() {
1270 Ok(guard) => guard.clone(),
1271 Err(_) => {
1272 warn!("Failed to read session for event trigger");
1273 return;
1274 }
1275 };
1276
1277 let listeners = match self.event_listeners.read() {
1278 Ok(guard) => guard,
1279 Err(_) => {
1280 warn!("Failed to read event listeners");
1281 return;
1282 }
1283 };
1284
1285 for callback in listeners.values() {
1286 callback(event.clone(), session.clone());
1287 }
1288 }
1289
1290 pub async fn list_mfa_factors(&self) -> Result<Vec<MfaFactor>> {
1308 debug!("Listing MFA factors for user");
1309
1310 let session = self.get_session()?;
1311 let response = self
1312 .http_client
1313 .get(format!("{}/auth/v1/factors", self.config.url))
1314 .header("Authorization", format!("Bearer {}", session.access_token))
1315 .send()
1316 .await?;
1317
1318 if !response.status().is_success() {
1319 return Err(Error::auth("Failed to list MFA factors"));
1320 }
1321
1322 let factors: Vec<MfaFactor> = response.json().await?;
1323 Ok(factors)
1324 }
1325
1326 pub async fn setup_totp(&self, friendly_name: &str) -> Result<TotpSetupResponse> {
1346 debug!("Setting up TOTP factor: {}", friendly_name);
1347
1348 let session = self.get_session()?;
1349
1350 let request_body = serde_json::json!({
1351 "friendly_name": friendly_name,
1352 "factor_type": "totp"
1353 });
1354
1355 let response = self
1356 .http_client
1357 .post(format!("{}/auth/v1/factors", self.config.url))
1358 .header("Authorization", format!("Bearer {}", session.access_token))
1359 .json(&request_body)
1360 .send()
1361 .await?;
1362
1363 if !response.status().is_success() {
1364 return Err(Error::auth("Failed to setup TOTP"));
1365 }
1366
1367 let setup_response: TotpSetupResponse = response.json().await?;
1368
1369 let qr = QrCode::new(&setup_response.uri)
1371 .map_err(|e| Error::auth(format!("Failed to generate QR code: {}", e)))?;
1372
1373 let qr_string = qr
1375 .render::<char>()
1376 .quiet_zone(false)
1377 .module_dimensions(2, 1)
1378 .build();
1379
1380 Ok(TotpSetupResponse {
1381 secret: setup_response.secret,
1382 qr_code: qr_string,
1383 uri: setup_response.uri,
1384 factor_id: setup_response.factor_id,
1385 })
1386 }
1387
1388 pub async fn setup_sms_mfa(
1407 &self,
1408 phone: &str,
1409 friendly_name: &str,
1410 default_region: Option<&str>,
1411 ) -> Result<MfaFactor> {
1412 debug!("Setting up SMS MFA factor: {} for {}", friendly_name, phone);
1413
1414 let enhanced_phone = EnhancedPhoneNumber::new(phone, default_region)?;
1416
1417 if !enhanced_phone.is_valid {
1418 return Err(Error::auth("Invalid phone number provided"));
1419 }
1420
1421 let session = self.get_session()?;
1422
1423 let request_body = serde_json::json!({
1424 "friendly_name": friendly_name,
1425 "factor_type": "sms",
1426 "phone": enhanced_phone.formatted
1427 });
1428
1429 let response = self
1430 .http_client
1431 .post(format!("{}/auth/v1/factors", self.config.url))
1432 .header("Authorization", format!("Bearer {}", session.access_token))
1433 .json(&request_body)
1434 .send()
1435 .await?;
1436
1437 if !response.status().is_success() {
1438 return Err(Error::auth("Failed to setup SMS MFA"));
1439 }
1440
1441 let factor: MfaFactor = response.json().await?;
1442 self.trigger_auth_event(AuthEvent::MfaEnabled);
1443
1444 Ok(factor)
1445 }
1446
1447 pub async fn create_mfa_challenge(&self, factor_id: Uuid) -> Result<MfaChallenge> {
1467 debug!("Creating MFA challenge for factor: {}", factor_id);
1468
1469 let session = self.get_session()?;
1470
1471 let request_body = serde_json::json!({
1472 "factor_id": factor_id
1473 });
1474
1475 let response = self
1476 .http_client
1477 .post(format!(
1478 "{}/auth/v1/factors/{}/challenge",
1479 self.config.url, factor_id
1480 ))
1481 .header("Authorization", format!("Bearer {}", session.access_token))
1482 .json(&request_body)
1483 .send()
1484 .await?;
1485
1486 if !response.status().is_success() {
1487 return Err(Error::auth("Failed to create MFA challenge"));
1488 }
1489
1490 let challenge: MfaChallenge = response.json().await?;
1491 self.trigger_auth_event(AuthEvent::MfaChallengeRequired);
1492
1493 Ok(challenge)
1494 }
1495
1496 pub async fn verify_mfa_challenge(
1519 &self,
1520 factor_id: Uuid,
1521 challenge_id: Uuid,
1522 code: &str,
1523 ) -> Result<AuthResponse> {
1524 debug!("Verifying MFA challenge: {}", challenge_id);
1525
1526 let session = self.get_session()?;
1527
1528 let request_body = serde_json::json!({
1529 "factor_id": factor_id,
1530 "challenge_id": challenge_id,
1531 "code": code
1532 });
1533
1534 let response = self
1535 .http_client
1536 .post(format!(
1537 "{}/auth/v1/factors/{}/verify",
1538 self.config.url, factor_id
1539 ))
1540 .header("Authorization", format!("Bearer {}", session.access_token))
1541 .json(&request_body)
1542 .send()
1543 .await?;
1544
1545 if !response.status().is_success() {
1546 return Err(Error::auth("Failed to verify MFA challenge"));
1547 }
1548
1549 let auth_response: AuthResponse = response.json().await?;
1550
1551 if let Some(session) = &auth_response.session {
1553 self.set_session(session.clone()).await?;
1554 }
1555
1556 self.trigger_auth_event(AuthEvent::MfaChallengeCompleted);
1557 info!("MFA verification successful");
1558
1559 Ok(auth_response)
1560 }
1561
1562 pub async fn delete_mfa_factor(&self, factor_id: Uuid) -> Result<()> {
1580 debug!("Deleting MFA factor: {}", factor_id);
1581
1582 let session = self.get_session()?;
1583
1584 let response = self
1585 .http_client
1586 .delete(format!("{}/auth/v1/factors/{}", self.config.url, factor_id))
1587 .header("Authorization", format!("Bearer {}", session.access_token))
1588 .send()
1589 .await?;
1590
1591 if !response.status().is_success() {
1592 return Err(Error::auth("Failed to delete MFA factor"));
1593 }
1594
1595 self.trigger_auth_event(AuthEvent::MfaDisabled);
1596 info!("MFA factor deleted successfully");
1597
1598 Ok(())
1599 }
1600
1601 pub fn generate_totp_code(&self, secret: &str) -> Result<String> {
1621 debug!("Generating TOTP code for testing");
1622
1623 let decoded_secret = base32::decode(base32::Alphabet::Rfc4648 { padding: true }, secret)
1625 .ok_or_else(|| Error::auth("Invalid base32 secret"))?;
1626
1627 let totp = TOTP::new(
1629 Algorithm::SHA1,
1630 6, 1, 30, decoded_secret,
1634 )
1635 .map_err(|e| Error::auth(format!("Failed to create TOTP: {}", e)))?;
1636
1637 let code = totp
1639 .generate_current()
1640 .map_err(|e| Error::auth(format!("Failed to generate TOTP code: {}", e)))?;
1641
1642 Ok(code)
1643 }
1644
1645 pub fn get_token_metadata(&self) -> Result<Option<TokenMetadata>> {
1666 debug!("Getting token metadata");
1667
1668 let session = self
1669 .session
1670 .read()
1671 .map_err(|_| Error::auth("Failed to read session"))?;
1672
1673 if let Some(session) = session.as_ref() {
1674 let metadata = TokenMetadata {
1676 issued_at: Utc::now() - chrono::Duration::seconds(session.expires_in),
1677 expires_at: session.expires_at,
1678 refresh_count: 0, last_refresh_at: None,
1680 scopes: vec![], device_id: None, };
1683
1684 Ok(Some(metadata))
1685 } else {
1686 Ok(None)
1687 }
1688 }
1689
1690 pub async fn refresh_token_advanced(&self) -> Result<Session> {
1719 debug!("Refreshing token with advanced handling");
1720
1721 let current_session = self
1722 .session
1723 .read()
1724 .map_err(|_| Error::auth("Failed to read session"))?
1725 .clone();
1726
1727 let session = match current_session {
1728 Some(session) => session,
1729 None => return Err(Error::auth("No active session to refresh")),
1730 };
1731
1732 let request_body = serde_json::json!({
1733 "refresh_token": session.refresh_token
1734 });
1735
1736 let response = self
1737 .http_client
1738 .post(format!(
1739 "{}/auth/v1/token?grant_type=refresh_token",
1740 self.config.url
1741 ))
1742 .header("apikey", &self.config.key)
1743 .header("Authorization", format!("Bearer {}", &self.config.key))
1744 .json(&request_body)
1745 .send()
1746 .await;
1747
1748 match response {
1749 Ok(response) => {
1750 if response.status().is_success() {
1751 let auth_response_body = response.text().await?;
1752
1753 let mut auth_response =
1754 serde_json::from_str::<AuthResponse>(auth_response_body.as_str())?;
1755 auth_response.session =
1756 serde_json::from_str::<Session>(auth_response_body.as_str())
1757 .inspect_err(|err| warn!("No session: {}", err.to_string()))
1758 .ok();
1759
1760 if let Some(new_session) = auth_response.session {
1761 self.set_session(new_session.clone()).await?;
1762 self.trigger_auth_event(AuthEvent::TokenRefreshed);
1763 info!("Token refreshed successfully");
1764 Ok(new_session)
1765 } else {
1766 Err(Error::auth("No session in refresh response"))
1767 }
1768 } else {
1769 let status = response.status();
1770 let error_text = response.text().await.unwrap_or_default();
1771
1772 let context = crate::error::ErrorContext {
1774 platform: Some(crate::error::detect_platform_context()),
1775 http: Some(crate::error::HttpErrorContext {
1776 status_code: Some(status.as_u16()),
1777 headers: None,
1778 response_body: Some(error_text.clone()),
1779 url: Some(format!("{}/auth/v1/token", self.config.url)),
1780 method: Some("POST".to_string()),
1781 }),
1782 retry: if status.is_server_error() {
1783 Some(crate::error::RetryInfo {
1784 retryable: true,
1785 retry_after: Some(60), attempts: 0,
1787 })
1788 } else {
1789 None
1790 },
1791 metadata: std::collections::HashMap::new(),
1792 timestamp: chrono::Utc::now(),
1793 };
1794
1795 Err(Error::auth_with_context(
1796 format!("Token refresh failed: {} - {}", status, error_text),
1797 context,
1798 ))
1799 }
1800 }
1801 Err(e) => {
1802 let context = crate::error::ErrorContext {
1803 platform: Some(crate::error::detect_platform_context()),
1804 http: Some(crate::error::HttpErrorContext {
1805 status_code: None,
1806 headers: None,
1807 response_body: None,
1808 url: Some(format!("{}/auth/v1/token", self.config.url)),
1809 method: Some("POST".to_string()),
1810 }),
1811 retry: Some(crate::error::RetryInfo {
1812 retryable: true,
1813 retry_after: Some(30),
1814 attempts: 0,
1815 }),
1816 metadata: std::collections::HashMap::new(),
1817 timestamp: chrono::Utc::now(),
1818 };
1819
1820 Err(Error::auth_with_context(
1821 format!("Network error during token refresh: {}", e),
1822 context,
1823 ))
1824 }
1825 }
1826 }
1827
1828 pub fn needs_refresh_with_buffer(&self, buffer_seconds: i64) -> Result<bool> {
1846 let session_guard = self
1847 .session
1848 .read()
1849 .map_err(|_| Error::auth("Failed to read session"))?;
1850
1851 match session_guard.as_ref() {
1852 Some(session) => {
1853 let now = Utc::now();
1854 let refresh_threshold =
1855 session.expires_at - chrono::Duration::seconds(buffer_seconds);
1856 Ok(now >= refresh_threshold)
1857 }
1858 None => Ok(false), }
1860 }
1861
1862 pub fn time_until_expiry(&self) -> Result<Option<i64>> {
1884 let session_guard = self
1885 .session
1886 .read()
1887 .map_err(|_| Error::auth("Failed to read session"))?;
1888
1889 match session_guard.as_ref() {
1890 Some(session) => {
1891 let now = Utc::now();
1892 let duration = session.expires_at.signed_duration_since(now);
1893 Ok(Some(duration.num_seconds()))
1894 }
1895 None => Ok(None),
1896 }
1897 }
1898
1899 pub fn validate_token_local(&self) -> Result<bool> {
1917 let session_guard = self
1918 .session
1919 .read()
1920 .map_err(|_| Error::auth("Failed to read session"))?;
1921
1922 match session_guard.as_ref() {
1923 Some(session) => {
1924 let now = Utc::now();
1925 Ok(session.expires_at > now && !session.access_token.is_empty())
1926 }
1927 None => Ok(false),
1928 }
1929 }
1930}
1931
1932#[cfg(test)]
1933mod tests {
1934 use super::*;
1935 use crate::types::SupabaseConfig;
1936 use std::sync::Arc;
1937
1938 fn mock_config() -> Arc<SupabaseConfig> {
1939 Arc::new(SupabaseConfig {
1940 url: "https://test.supabase.co".to_string(),
1941 key: "test-key".to_string(),
1942 service_role_key: None,
1943 http_config: crate::types::HttpConfig::default(),
1944 auth_config: crate::types::AuthConfig::default(),
1945 database_config: crate::types::DatabaseConfig::default(),
1946 storage_config: crate::types::StorageConfig::default(),
1947 })
1948 }
1949
1950 #[test]
1951 fn test_enhanced_phone_number_creation() {
1952 let phone = EnhancedPhoneNumber::new("+1-555-123-4567", Some("US"));
1953 assert!(phone.is_ok());
1954
1955 let phone = phone.unwrap();
1956 assert!(!phone.raw.is_empty());
1957 assert!(!phone.formatted.is_empty());
1958 assert!(!phone.country_code.is_empty());
1959 }
1960
1961 #[test]
1962 fn test_mfa_method_serialization() {
1963 let totp = MfaMethod::Totp;
1964 let sms = MfaMethod::Sms;
1965 let email = MfaMethod::Email;
1966
1967 let totp_json = serde_json::to_string(&totp).unwrap();
1968 let sms_json = serde_json::to_string(&sms).unwrap();
1969 let email_json = serde_json::to_string(&email).unwrap();
1970
1971 assert_eq!(totp_json, r#""totp""#);
1972 assert_eq!(sms_json, r#""sms""#);
1973 assert_eq!(email_json, r#""email""#);
1974 }
1975
1976 #[test]
1977 fn test_mfa_challenge_status() {
1978 let pending = MfaChallengeStatus::Pending;
1979 let completed = MfaChallengeStatus::Completed;
1980 let expired = MfaChallengeStatus::Expired;
1981 let cancelled = MfaChallengeStatus::Cancelled;
1982
1983 assert_eq!(pending, MfaChallengeStatus::Pending);
1984 assert_eq!(completed, MfaChallengeStatus::Completed);
1985 assert_eq!(expired, MfaChallengeStatus::Expired);
1986 assert_eq!(cancelled, MfaChallengeStatus::Cancelled);
1987 }
1988
1989 #[test]
1990 fn test_auth_event_variants() {
1991 let events = vec![
1992 AuthEvent::SignedIn,
1993 AuthEvent::SignedOut,
1994 AuthEvent::TokenRefreshed,
1995 AuthEvent::UserUpdated,
1996 AuthEvent::PasswordReset,
1997 AuthEvent::MfaChallengeRequired,
1998 AuthEvent::MfaChallengeCompleted,
1999 AuthEvent::MfaEnabled,
2000 AuthEvent::MfaDisabled,
2001 ];
2002
2003 assert_eq!(events.len(), 9);
2004
2005 let cloned_events: Vec<AuthEvent> = events.to_vec();
2007 assert_eq!(cloned_events, events);
2008 }
2009
2010 #[test]
2011 fn test_parsing_auth_from_response() {
2012 let json_body = r#"{
2013 "access_token": "eyJhbGciOiJIUzI1NiIsImtpZCI6IkhxWTFsZ3pmbGhOQUx3NTAiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3Fiamx4cWFyb2l0eHNvdml3bmRsLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiIzMTgxNWM1NS1mNTUzLTQxZjQtYjU0Zi0xNGQ2YWM2MGRlMTYiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzYwMDQ1MTk1LCJpYXQiOjE3NjAwNDE1OTUsImVtYWlsIjoic29tZW9uZUBlbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsIjoic29tZW9uZUBlbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGhvbmVfdmVyaWZpZWQiOmZhbHNlLCJzdWIiOiIzMTgxNWM1NS1mNTUzLTQxZjQtYjU0Zi0xNGQ2YWM2MGRlMTYifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc2MDA0MTU5NX1dLCJzZXNzaW9uX2lkIjoiMzc0OTc0OGUtMmUyMy00Nzk0LTllNmQtNjg0MzU5ZDc3M2RjIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.HCNEZQjnpzBkfEJ_6gJwxcfubRDGez8SRlM6Ni63X_k",
2014 "token_type": "bearer",
2015 "expires_in": 3600,
2016 "expires_at": 1760045195,
2017 "refresh_token": "pwou3cigit5u",
2018 "user": {
2019 "id": "31815c55-f553-41f4-b54f-14d6ac60de16",
2020 "aud": "authenticated",
2021 "role": "authenticated",
2022 "email": "someone@email.com",
2023 "email_confirmed_at": "2025-10-09T14:53:09.962028Z",
2024 "phone": "",
2025 "confirmed_at": "2025-10-09T14:53:09.962028Z",
2026 "last_sign_in_at": "2025-10-09T20:26:35.136870325Z",
2027 "app_metadata": {
2028 "provider": "email",
2029 "providers": ["email"]
2030 },
2031 "user_metadata": {
2032 "email": "someone@email.com",
2033 "email_verified": true,
2034 "phone_verified": false,
2035 "sub": "31815c55-f553-41f4-b54f-14d6ac60de16"
2036 },
2037 "identities": [{
2038 "identity_id": "5fe7caa2-1dc3-449b-b910-33bd7df0d616",
2039 "id": "31815c55-f553-41f4-b54f-14d6ac60de16",
2040 "user_id": "31815c55-f553-41f4-b54f-14d6ac60de16",
2041 "identity_data": {
2042 "email": "someone@email.com",
2043 "email_verified": false,
2044 "phone_verified": false,
2045 "sub": "31815c55-f553-41f4-b54f-14d6ac60de16"
2046 },
2047 "provider": "email",
2048 "last_sign_in_at": "2025-10-09T14:53:09.958178Z",
2049 "created_at": "2025-10-09T14:53:09.958225Z",
2050 "updated_at": "2025-10-09T14:53:09.958225Z",
2051 "email": "someone@email.com"
2052 }],
2053 "created_at": "2025-10-09T14:53:09.953727Z",
2054 "updated_at": "2025-10-09T20:26:35.13964Z",
2055 "is_anonymous": false
2056 },
2057 "weak_password": null
2058}"#;
2059 let mut auth = serde_json::from_str::<AuthResponse>(json_body).unwrap();
2060 assert!(auth.session.is_none());
2061
2062 auth.session = serde_json::from_str::<Session>(json_body).ok();
2063 assert!(auth.session.is_some());
2064 }
2065
2066 #[test]
2067 fn test_parsing_auth_with_missing_session() {
2068 let json_body = r#"{
2069 "user": {
2070 "id": "31815c55-f553-41f4-b54f-14d6ac60de16",
2071 "aud": "authenticated",
2072 "role": "authenticated",
2073 "email": "someone@email.com",
2074 "email_confirmed_at": "2025-10-09T14:53:09.962028Z",
2075 "phone": "",
2076 "confirmed_at": "2025-10-09T14:53:09.962028Z",
2077 "last_sign_in_at": "2025-10-09T20:26:35.136870325Z",
2078 "app_metadata": {
2079 "provider": "email",
2080 "providers": ["email"]
2081 },
2082 "user_metadata": {
2083 "email": "someone@email.com",
2084 "email_verified": true,
2085 "phone_verified": false,
2086 "sub": "31815c55-f553-41f4-b54f-14d6ac60de16"
2087 },
2088 "identities": [{
2089 "identity_id": "5fe7caa2-1dc3-449b-b910-33bd7df0d616",
2090 "id": "31815c55-f553-41f4-b54f-14d6ac60de16",
2091 "user_id": "31815c55-f553-41f4-b54f-14d6ac60de16",
2092 "identity_data": {
2093 "email": "someone@email.com",
2094 "email_verified": false,
2095 "phone_verified": false,
2096 "sub": "31815c55-f553-41f4-b54f-14d6ac60de16"
2097 },
2098 "provider": "email",
2099 "last_sign_in_at": "2025-10-09T14:53:09.958178Z",
2100 "created_at": "2025-10-09T14:53:09.958225Z",
2101 "updated_at": "2025-10-09T14:53:09.958225Z",
2102 "email": "someone@email.com"
2103 }],
2104 "created_at": "2025-10-09T14:53:09.953727Z",
2105 "updated_at": "2025-10-09T20:26:35.13964Z",
2106 "is_anonymous": false
2107 },
2108 "weak_password": null
2109}"#;
2110 let mut auth = serde_json::from_str::<AuthResponse>(json_body).unwrap();
2111 assert!(auth.session.is_none());
2112
2113 auth.session = serde_json::from_str::<Session>(json_body).ok();
2114 assert!(auth.session.is_none());
2115 }
2116
2117 #[tokio::test]
2118 async fn test_auth_creation() {
2119 let config = mock_config();
2120 let http_client = Arc::new(reqwest::Client::new());
2121
2122 let auth = Auth::new(config, http_client);
2123 assert!(auth.is_ok());
2124
2125 let auth = auth.unwrap();
2126 assert!(!auth.is_authenticated());
2127 }
2128
2129 #[test]
2130 fn test_totp_code_generation() {
2131 let config = mock_config();
2132 let http_client = Arc::new(reqwest::Client::new());
2133 let auth = Auth::new(config, http_client).unwrap();
2134
2135 let secret = "JBSWY3DPEHPK3PXP"; let result = auth.generate_totp_code(secret);
2138
2139 match &result {
2140 Ok(code) => {
2141 println!("Generated TOTP code: {}", code);
2142 assert_eq!(code.len(), 6);
2143 assert!(code.chars().all(|c| c.is_ascii_digit()));
2144 }
2145 Err(e) => {
2146 println!("TOTP generation error: {}", e);
2147 assert!(e.to_string().contains("base32") || e.to_string().contains("TOTP"));
2149 }
2150 }
2151 }
2152
2153 #[test]
2154 fn test_totp_code_generation_invalid_secret() {
2155 let config = mock_config();
2156 let http_client = Arc::new(reqwest::Client::new());
2157 let auth = Auth::new(config, http_client).unwrap();
2158
2159 let result = auth.generate_totp_code("invalid-secret");
2161 assert!(result.is_err());
2162 }
2163
2164 #[tokio::test]
2165 async fn test_token_validation_no_session() {
2166 let config = mock_config();
2167 let http_client = Arc::new(reqwest::Client::new());
2168 let auth = Auth::new(config, http_client).unwrap();
2169
2170 let is_valid = auth.validate_token_local().unwrap();
2172 assert!(!is_valid);
2173 }
2174
2175 #[test]
2176 fn test_time_until_expiry_no_session() {
2177 let config = mock_config();
2178 let http_client = Arc::new(reqwest::Client::new());
2179 let auth = Auth::new(config, http_client).unwrap();
2180
2181 let time = auth.time_until_expiry().unwrap();
2183 assert!(time.is_none());
2184 }
2185
2186 #[test]
2187 fn test_needs_refresh_no_session() {
2188 let config = mock_config();
2189 let http_client = Arc::new(reqwest::Client::new());
2190 let auth = Auth::new(config, http_client).unwrap();
2191
2192 let needs_refresh = auth.needs_refresh_with_buffer(300).unwrap();
2194 assert!(!needs_refresh);
2195 }
2196
2197 #[test]
2198 fn test_get_token_metadata_no_session() {
2199 let config = mock_config();
2200 let http_client = Arc::new(reqwest::Client::new());
2201 let auth = Auth::new(config, http_client).unwrap();
2202
2203 let metadata = auth.get_token_metadata().unwrap();
2205 assert!(metadata.is_none());
2206 }
2207
2208 #[test]
2209 fn test_token_metadata_structure() {
2210 let metadata = TokenMetadata {
2211 issued_at: Utc::now(),
2212 expires_at: Utc::now() + chrono::Duration::hours(1),
2213 refresh_count: 5,
2214 last_refresh_at: Some(Utc::now()),
2215 scopes: vec!["read".to_string(), "write".to_string()],
2216 device_id: Some("device-123".to_string()),
2217 };
2218
2219 assert_eq!(metadata.refresh_count, 5);
2220 assert!(metadata.last_refresh_at.is_some());
2221 assert_eq!(metadata.scopes.len(), 2);
2222 assert!(metadata.device_id.is_some());
2223 }
2224
2225 #[test]
2226 fn test_mfa_factor_structure() {
2227 let factor = MfaFactor {
2228 id: uuid::Uuid::new_v4(),
2229 factor_type: MfaMethod::Totp,
2230 friendly_name: "My Authenticator".to_string(),
2231 status: "verified".to_string(),
2232 created_at: Utc::now(),
2233 updated_at: Utc::now(),
2234 phone: None,
2235 };
2236
2237 assert_eq!(factor.factor_type, MfaMethod::Totp);
2238 assert_eq!(factor.friendly_name, "My Authenticator");
2239 assert_eq!(factor.status, "verified");
2240 assert!(factor.phone.is_none());
2241 }
2242
2243 #[test]
2244 fn test_enhanced_session_structure() {
2245 let user = User {
2246 id: uuid::Uuid::new_v4(),
2247 email: Some("user@example.com".to_string()),
2248 phone: None,
2249 email_confirmed_at: Some(Utc::now()),
2250 phone_confirmed_at: None,
2251 created_at: Utc::now(),
2252 updated_at: Utc::now(),
2253 last_sign_in_at: Some(Utc::now()),
2254 app_metadata: serde_json::json!({}),
2255 user_metadata: serde_json::json!({}),
2256 aud: "authenticated".to_string(),
2257 role: Some("authenticated".to_string()),
2258 };
2259
2260 let enhanced_session = EnhancedSession {
2261 access_token: "access-token".to_string(),
2262 refresh_token: "refresh-token".to_string(),
2263 expires_in: 3600,
2264 expires_at: Utc::now() + chrono::Duration::hours(1),
2265 token_type: "bearer".to_string(),
2266 user,
2267 token_metadata: None,
2268 mfa_verified: true,
2269 active_factors: vec![],
2270 };
2271
2272 assert!(enhanced_session.mfa_verified);
2273 assert_eq!(enhanced_session.active_factors.len(), 0);
2274 assert_eq!(enhanced_session.token_type, "bearer");
2275 }
2276}