use crate::core::{Session, SupabaseClient, User};
use crate::error::{Result, SupaError};
use chrono::Utc;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OtpType {
Signup,
Invite,
Recovery,
Magiclink,
Sms,
Email,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MfaFactorType {
Totp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaFactor {
pub id: String,
pub status: String,
pub friendly_name: Option<String>,
pub factor_type: MfaFactorType,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaEnrollResponse {
pub id: String,
#[serde(rename = "type")]
pub factor_type: String,
pub totp: Option<MfaTotpDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaTotpDetails {
pub qr_code: String,
pub secret: String,
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaChallengeResponse {
pub id: String,
pub expires_at: String,
}
#[derive(Debug, Clone)]
pub enum SignUpResult {
Session(Session),
ConfirmationPending {
user: User,
},
}
impl SignUpResult {
pub fn session(&self) -> Option<&Session> {
match self {
SignUpResult::Session(s) => Some(s),
SignUpResult::ConfirmationPending { .. } => None,
}
}
pub fn user(&self) -> Option<&User> {
match self {
SignUpResult::Session(s) => s.user.as_ref(),
SignUpResult::ConfirmationPending { user } => Some(user),
}
}
pub fn is_confirmation_pending(&self) -> bool {
matches!(self, SignUpResult::ConfirmationPending { .. })
}
}
#[derive(Clone)]
pub struct AuthService {
client: SupabaseClient,
}
impl AuthService {
pub fn new(client: SupabaseClient) -> Self {
Self { client }
}
pub async fn sign_in_with_password(&self, email: &str, password: &str) -> Result<Session> {
let payload = serde_json::json!({
"email": email,
"password": password,
});
let response = self
.client
.inner
.http
.post(format!(
"{}/auth/v1/token?grant_type=password",
self.client.inner.url
))
.header("apikey", &self.client.inner.key)
.json(&payload)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Sign in with password failed: {}", e),
})?;
let session: Session = self.client.handle_response(response).await?;
Ok(session)
}
pub async fn sign_up(&self, email: &str, password: &str) -> Result<SignUpResult> {
let payload = serde_json::json!({
"email": email,
"password": password,
});
let response = self
.client
.inner
.http
.post(format!("{}/auth/v1/signup", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.json(&payload)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Sign up failed: {}", e),
})?;
if !response.status().is_success() {
let status = response.status().as_u16();
let error_text = response.text().await.unwrap_or_default();
return Err(SupaError::ApiError {
code: status,
message: error_text.clone(),
details: Some(error_text),
});
}
let json: serde_json::Value = response.json().await?;
if json.get("access_token").is_some() {
let session: Session = serde_json::from_value(json)?;
Ok(SignUpResult::Session(session))
} else {
let user: User = serde_json::from_value(json)?;
Ok(SignUpResult::ConfirmationPending { user })
}
}
pub async fn sign_in_with_otp(
&self,
email: Option<&str>,
phone: Option<&str>,
otp_type: OtpType,
) -> Result<()> {
let mut payload = serde_json::json!({
"options": {
"email_redirect_to": None::<String>, }
});
if let Some(e) = email {
payload["email"] = serde_json::to_value(e)?;
}
if let Some(p) = phone {
payload["phone"] = serde_json::to_value(p)?;
}
payload["type"] = serde_json::to_value(otp_type)?;
let resp = self
.client
.inner
.http
.post(format!("{}/auth/v1/otp", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.json(&payload)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Sign in with OTP failed: {}", e),
})?;
let _: serde_json::Value = self.client.handle_response(resp).await?;
Ok(())
}
pub async fn reset_password_for_email(
&self,
email: &str,
redirect_to: Option<&str>,
) -> Result<()> {
let mut payload = serde_json::json!({
"email": email,
});
if let Some(r) = redirect_to {
payload["options"] = serde_json::json!({
"email_redirect_to": r
});
}
let resp = self
.client
.inner
.http
.post(format!("{}/auth/v1/recover", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.json(&payload)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Reset password for email failed: {}", e),
})?;
let _: serde_json::Value = self.client.handle_response(resp).await?;
Ok(())
}
pub async fn sign_out(&self) -> Result<()> {
let token = {
self.client
.inner
.session
.read()
.ok()
.and_then(|lock| lock.as_ref().map(|s| s.access_token.clone()))
};
if let Some(token) = token {
let _ = self
.client
.inner
.http
.post(format!("{}/auth/v1/logout", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Sign out failed: {}", e),
})?;
}
self.client.reset_auth_token();
Ok(())
}
pub async fn get_user(&self) -> Result<User> {
let token = self
.client
.inner
.session
.read()
.ok()
.and_then(|lock| lock.as_ref().map(|s| s.access_token.clone()))
.ok_or(SupaError::ClientError {
message: "Not authenticated".to_string(),
})?;
let resp = self
.client
.inner
.http
.get(format!("{}/auth/v1/user", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Get user failed: {}", e),
})?;
let user: User = self.client.handle_response(resp).await?;
Ok(user)
}
pub async fn update_user(&self, attributes: serde_json::Value) -> Result<User> {
let token = self
.client
.inner
.session
.read()
.ok()
.and_then(|lock| lock.as_ref().map(|s| s.access_token.clone()))
.ok_or(SupaError::ClientError {
message: "Not authenticated".to_string(),
})?;
let resp = self
.client
.inner
.http
.put(format!("{}/auth/v1/user", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", token))
.json(&attributes)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Update user failed: {}", e),
})?;
let user: User = self.client.handle_response(resp).await?;
Ok(user)
}
pub async fn refresh_access_token(&self, refresh_token: &str) -> Result<Session> {
let payload = serde_json::json!({
"refresh_token": refresh_token,
});
let resp = self
.client
.inner
.http
.post(format!(
"{}/auth/v1/token?grant_type=refresh_token",
self.client.inner.url
))
.header("apikey", &self.client.inner.key)
.json(&payload)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Refresh token failed: {}", e),
})?;
let session: Session = self.client.handle_response(resp).await?;
self.client.set_session(session.clone());
Ok(session)
}
pub async fn mfa_enroll(&self, friendly_name: Option<String>) -> Result<MfaEnrollResponse> {
let token = self.client.get_access_token().await?;
let payload = serde_json::json!({
"factor_type": "totp",
"friendly_name": friendly_name
});
let resp = self
.client
.inner
.http
.post(format!("{}/auth/v1/factors", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", token))
.json(&payload)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("MFA enroll failed: {}", e),
})?;
self.client.handle_response(resp).await
}
pub async fn mfa_challenge(&self, factor_id: &str) -> Result<MfaChallengeResponse> {
let token = self.client.get_access_token().await?;
let resp = self
.client
.inner
.http
.post(format!(
"{}/auth/v1/factors/{}/challenge",
self.client.inner.url, factor_id
))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("MFA challenge failed: {}", e),
})?;
self.client.handle_response(resp).await
}
pub async fn mfa_verify(
&self,
factor_id: &str,
challenge_id: &str,
code: &str,
) -> Result<Session> {
let token = self.client.get_access_token().await?;
let payload = serde_json::json!({
"challenge_id": challenge_id,
"code": code
});
let resp = self
.client
.inner
.http
.post(format!(
"{}/auth/v1/factors/{}/verify",
self.client.inner.url, factor_id
))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", token))
.json(&payload)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Verify OTP failed: {}", e),
})?;
let mut session: Session = self.client.handle_response(resp).await?;
if session.expires_at.is_none() {
let now = Utc::now().timestamp();
session.expires_at = Some(now + session.expires_in);
}
self.client.set_session(session.clone());
Ok(session)
}
pub async fn mfa_unenroll(&self, factor_id: &str) -> Result<MfaFactor> {
let token = self.client.get_access_token().await?;
let response = self
.client
.inner
.http
.delete(format!(
"{}/auth/v1/factors/{}",
self.client.inner.url, factor_id
))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("MFA unenroll failed: {}", e),
})?;
self.client.handle_response(response).await
}
pub async fn mfa_list_factors(&self) -> Result<Vec<MfaFactor>> {
let token = self.client.get_access_token().await?;
let response = self
.client
.inner
.http
.get(format!("{}/auth/v1/factors", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("MFA list failed: {}", e),
})?;
self.client.handle_response(response).await
}
pub fn sign_in_with_oauth(
&self,
provider: Provider,
redirect_to: Option<&str>,
scopes: Option<&str>,
) -> Result<String> {
let mut url = self
.client
.inner
.url
.join(&format!("auth/v1/authorize?provider={}", provider))?;
if let Some(r) = redirect_to {
url.query_pairs_mut().append_pair("redirect_to", r);
}
if let Some(s) = scopes {
url.query_pairs_mut().append_pair("scopes", s);
}
Ok(url.to_string())
}
pub fn admin(&self) -> AdminAuthService {
AdminAuthService::new(self.client.clone())
}
}
#[derive(Clone)]
pub struct AdminAuthService {
client: SupabaseClient,
}
impl AdminAuthService {
pub fn new(client: SupabaseClient) -> Self {
Self { client }
}
pub async fn list_users(&self, page: Option<u32>, per_page: Option<u32>) -> Result<Vec<User>> {
let mut url = self
.client
.inner
.url
.join("auth/v1/admin/users")
.map_err(SupaError::UrlError)?;
if let Some(p) = page {
url.query_pairs_mut().append_pair("page", &p.to_string());
}
if let Some(pp) = per_page {
url.query_pairs_mut()
.append_pair("per_page", &pp.to_string());
}
let resp = self
.client
.inner
.http
.get(url)
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", self.client.inner.key))
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("List users failed: {}", e),
})?;
#[derive(Deserialize)]
struct UserListResponse {
users: Vec<User>,
}
let wrapper: UserListResponse = self.client.handle_response(resp).await?;
Ok(wrapper.users)
}
pub async fn get_user_by_id(&self, uid: &str) -> Result<User> {
let resp = self
.client
.inner
.http
.get(format!(
"{}/auth/v1/admin/users/{}",
self.client.inner.url, uid
))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", self.client.inner.key))
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Get user by ID failed: {}", e),
})?;
self.client.handle_response(resp).await
}
pub async fn create_user(&self, params: serde_json::Value) -> Result<User> {
let resp = self
.client
.inner
.http
.post(format!("{}/auth/v1/admin/users", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", self.client.inner.key))
.json(¶ms)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Create user failed: {}", e),
})?;
self.client.handle_response(resp).await
}
pub async fn delete_user(&self, uid: &str) -> Result<()> {
let resp = self
.client
.inner
.http
.delete(format!(
"{}/auth/v1/admin/users/{}",
self.client.inner.url, uid
))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", self.client.inner.key))
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Delete user failed: {}", e),
})?;
let _: serde_json::Value = self.client.handle_response(resp).await?;
Ok(())
}
pub async fn invite_user_by_email(&self, email: &str) -> Result<User> {
let payload = serde_json::json!({
"email": email
});
let resp = self
.client
.inner
.http
.post(format!("{}/auth/v1/invite", self.client.inner.url))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", self.client.inner.key))
.json(&payload)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Invite user failed: {}", e),
})?;
self.client.handle_response(resp).await
}
pub async fn update_user_by_id(
&self,
uid: &str,
attributes: serde_json::Value,
) -> Result<User> {
let resp = self
.client
.inner
.http
.put(format!(
"{}/auth/v1/admin/users/{}",
self.client.inner.url, uid
))
.header("apikey", &self.client.inner.key)
.header("Authorization", format!("Bearer {}", self.client.inner.key))
.json(&attributes)
.send()
.await
.map_err(|e| SupaError::AuthError {
message: format!("Update user by ID failed: {}", e),
})?;
self.client.handle_response(resp).await
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Provider {
Apple,
Azure,
Bitbucket,
Discord,
Facebook,
Github,
Gitlab,
Google,
Keycloak,
Linkedin,
Notion,
Slack,
Spotify,
Twitch,
Twitter,
Workos,
Zoom,
#[serde(untagged)]
External(String),
}
impl std::fmt::Display for Provider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Provider::Apple => write!(f, "apple"),
Provider::Azure => write!(f, "azure"),
Provider::Bitbucket => write!(f, "bitbucket"),
Provider::Discord => write!(f, "discord"),
Provider::Facebook => write!(f, "facebook"),
Provider::Github => write!(f, "github"),
Provider::Gitlab => write!(f, "gitlab"),
Provider::Google => write!(f, "google"),
Provider::Keycloak => write!(f, "keycloak"),
Provider::Linkedin => write!(f, "linkedin"),
Provider::Notion => write!(f, "notion"),
Provider::Slack => write!(f, "slack"),
Provider::Spotify => write!(f, "spotify"),
Provider::Twitch => write!(f, "twitch"),
Provider::Twitter => write!(f, "twitter"),
Provider::Workos => write!(f, "workos"),
Provider::Zoom => write!(f, "zoom"),
Provider::External(s) => write!(f, "{}", s),
}
}
}