diff --git a/README.md b/README.md index f74a9f6..c78d25e 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,15 @@ - [x] Client creation - [x] Sign-in email/pass - [x] Signup email/pass -- [ ] Signup phone/pass +- [x] Signup phone/pass - [x] Token refresh - [x] Logout -- [ ] Verify one-time token +- [x] Verify one-time token - [ ] Authorize external OAuth provicder - [ ] Password recovery -- [ ] Resend one-time password over email or SMS +- [x] Resend one-time password over email or SMS - [ ] Magic link authentication -- [ ] One-time password authentication +- [x] One-time password authentication - [ ] Retrieval of user's information - [ ] Reauthentication of a password change - [ ] Enrollment of MFA diff --git a/src/auth.rs b/src/auth.rs index b1d6ff9..a3245f2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -35,6 +35,34 @@ impl std::fmt::Display for LogoutError { impl std::error::Error for LogoutError {} +#[derive(Serialize)] +struct PhoneCredentials<'a> { + phone: &'a str, + password: &'a str, +} + +#[derive(Serialize)] +struct OtpRequest<'a> { + phone: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + channel: Option<&'a str>, +} + +#[derive(Serialize)] +struct VerifyOtpRequest<'a> { + phone: &'a str, + token: &'a str, + #[serde(rename = "type")] + verification_type: &'a str, +} + +#[derive(Serialize)] +struct ResendOtpRequest<'a> { + phone: &'a str, + #[serde(rename = "type")] + verification_type: &'a str, +} + impl Supabase { /// Validates a JWT token and returns its claims. /// @@ -94,6 +122,86 @@ impl Supabase { .await) } + /// Signs up a new user with phone and password. + pub async fn signup_phone_password( + &self, + phone: &str, + password: &str, + ) -> Result { + let url = format!("{}/auth/v1/signup", self.url); + + self.client + .post(&url) + .header("apikey", &self.api_key) + .header("Content-Type", "application/json") + .json(&PhoneCredentials { phone, password }) + .send() + .await + } + + /// Sends a one-time password to the given phone number. + /// + /// The `channel` parameter can be `"sms"` or `"whatsapp"`. Defaults to SMS when `None`. + pub async fn sign_in_otp( + &self, + phone: &str, + channel: Option<&str>, + ) -> Result { + let url = format!("{}/auth/v1/otp", self.url); + + self.client + .post(&url) + .header("apikey", &self.api_key) + .header("Content-Type", "application/json") + .json(&OtpRequest { phone, channel }) + .send() + .await + } + + /// Verifies a one-time password token. + /// + /// Returns access and refresh tokens on success. + pub async fn verify_otp( + &self, + phone: &str, + token: &str, + verification_type: &str, + ) -> Result { + let url = format!("{}/auth/v1/verify", self.url); + + self.client + .post(&url) + .header("apikey", &self.api_key) + .header("Content-Type", "application/json") + .json(&VerifyOtpRequest { + phone, + token, + verification_type, + }) + .send() + .await + } + + /// Resends a one-time password to the given phone number. + pub async fn resend_otp( + &self, + phone: &str, + verification_type: &str, + ) -> Result { + let url = format!("{}/auth/v1/resend", self.url); + + self.client + .post(&url) + .header("apikey", &self.api_key) + .header("Content-Type", "application/json") + .json(&ResendOtpRequest { + phone, + verification_type, + }) + .send() + .await + } + /// Signs up a new user with email and password. pub async fn signup_email_password( &self, @@ -276,4 +384,70 @@ mod tests { // Verify the error type displays correctly assert_eq!(format!("{}", LogoutError), "bearer token required for logout"); } + + #[tokio::test] + async fn test_signup_phone_password() { + let client = client(); + + let response = match client.signup_phone_password("+10000000000", "test-password-123").await + { + Ok(resp) => resp, + Err(e) => { + println!("Test skipped due to network error: {e}"); + return; + } + }; + + let status = response.status().as_u16(); + assert!( + status == 200 || status == 422 || status == 401 || status == 403, + "unexpected status: {status}" + ); + } + + #[tokio::test] + async fn test_sign_in_otp() { + let client = client(); + + let response = match client.sign_in_otp("+10000000000", Some("sms")).await { + Ok(resp) => resp, + Err(e) => { + println!("Test skipped due to network error: {e}"); + return; + } + }; + + // OTP endpoint should return a response (success or error depending on config) + let _status = response.status(); + } + + #[tokio::test] + async fn test_verify_otp() { + let client = client(); + + let response = match client.verify_otp("+10000000000", "000000", "sms").await { + Ok(resp) => resp, + Err(e) => { + println!("Test skipped due to network error: {e}"); + return; + } + }; + + let _status = response.status(); + } + + #[tokio::test] + async fn test_resend_otp() { + let client = client(); + + let response = match client.resend_otp("+10000000000", "sms").await { + Ok(resp) => resp, + Err(e) => { + println!("Test skipped due to network error: {e}"); + return; + } + }; + + let _status = response.status(); + } }