supabase/
auth.rs

1//! Authentication module for Supabase
2
3use crate::{
4    error::{Error, Result},
5    types::{SupabaseConfig, Timestamp},
6};
7use chrono::Utc;
8use reqwest::Client as HttpClient;
9use serde::{Deserialize, Serialize};
10use std::sync::{Arc, RwLock};
11use tracing::{debug, info, warn};
12use uuid::Uuid;
13
14/// Authentication client for handling user sessions and JWT tokens
15#[derive(Debug, Clone)]
16pub struct Auth {
17    http_client: Arc<HttpClient>,
18    config: Arc<SupabaseConfig>,
19    session: Arc<RwLock<Option<Session>>>,
20}
21
22/// User information from Supabase Auth
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct User {
25    pub id: Uuid,
26    pub email: Option<String>,
27    pub phone: Option<String>,
28    pub email_confirmed_at: Option<Timestamp>,
29    pub phone_confirmed_at: Option<Timestamp>,
30    pub created_at: Timestamp,
31    pub updated_at: Timestamp,
32    pub last_sign_in_at: Option<Timestamp>,
33    pub app_metadata: serde_json::Value,
34    pub user_metadata: serde_json::Value,
35    pub aud: String,
36    pub role: Option<String>,
37}
38
39/// Authentication session containing user and tokens
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Session {
42    pub access_token: String,
43    pub refresh_token: String,
44    pub expires_in: i64,
45    pub expires_at: Timestamp,
46    pub token_type: String,
47    pub user: User,
48}
49
50/// Response from authentication operations
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AuthResponse {
53    pub user: Option<User>,
54    pub session: Option<Session>,
55}
56
57/// Sign up request payload
58#[derive(Debug, Serialize)]
59struct SignUpRequest {
60    email: String,
61    password: String,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    data: Option<serde_json::Value>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    redirect_to: Option<String>,
66}
67
68/// Sign in request payload
69#[derive(Debug, Serialize)]
70struct SignInRequest {
71    email: String,
72    password: String,
73}
74
75/// Password reset request payload
76#[derive(Debug, Serialize)]
77struct PasswordResetRequest {
78    email: String,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    redirect_to: Option<String>,
81}
82
83/// Token refresh request payload
84#[derive(Debug, Serialize)]
85struct RefreshTokenRequest {
86    refresh_token: String,
87}
88
89/// Update user request payload
90#[derive(Debug, Serialize)]
91struct UpdateUserRequest {
92    #[serde(skip_serializing_if = "Option::is_none")]
93    email: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    password: Option<String>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    data: Option<serde_json::Value>,
98}
99
100impl Auth {
101    /// Create a new Auth instance
102    pub fn new(config: Arc<SupabaseConfig>, http_client: Arc<HttpClient>) -> Result<Self> {
103        debug!("Initializing Auth module");
104
105        Ok(Self {
106            http_client,
107            config,
108            session: Arc::new(RwLock::new(None)),
109        })
110    }
111
112    /// Sign up a new user with email and password
113    pub async fn sign_up_with_email_and_password(
114        &self,
115        email: &str,
116        password: &str,
117    ) -> Result<AuthResponse> {
118        self.sign_up_with_email_password_and_data(email, password, None, None)
119            .await
120    }
121
122    /// Sign up a new user with email, password, and optional metadata
123    pub async fn sign_up_with_email_password_and_data(
124        &self,
125        email: &str,
126        password: &str,
127        data: Option<serde_json::Value>,
128        redirect_to: Option<String>,
129    ) -> Result<AuthResponse> {
130        debug!("Signing up user with email: {}", email);
131
132        let payload = SignUpRequest {
133            email: email.to_string(),
134            password: password.to_string(),
135            data,
136            redirect_to,
137        };
138
139        let response = self
140            .http_client
141            .post(format!("{}/auth/v1/signup", self.config.url))
142            .json(&payload)
143            .send()
144            .await?;
145
146        if !response.status().is_success() {
147            let status = response.status();
148            let error_msg = match response.text().await {
149                Ok(text) => text,
150                Err(_) => format!("Sign up failed with status: {}", status),
151            };
152            return Err(Error::auth(error_msg));
153        }
154
155        let auth_response: AuthResponse = response.json().await?;
156
157        if let Some(ref session) = auth_response.session {
158            self.set_session(session.clone()).await?;
159            info!("User signed up successfully");
160        }
161
162        Ok(auth_response)
163    }
164
165    /// Sign in with email and password
166    pub async fn sign_in_with_email_and_password(
167        &self,
168        email: &str,
169        password: &str,
170    ) -> Result<AuthResponse> {
171        debug!("Signing in user with email: {}", email);
172
173        let payload = SignInRequest {
174            email: email.to_string(),
175            password: password.to_string(),
176        };
177
178        let response = self
179            .http_client
180            .post(format!(
181                "{}/auth/v1/token?grant_type=password",
182                self.config.url
183            ))
184            .json(&payload)
185            .send()
186            .await?;
187
188        if !response.status().is_success() {
189            let status = response.status();
190            let error_msg = match response.text().await {
191                Ok(text) => text,
192                Err(_) => format!("Sign in failed with status: {}", status),
193            };
194            return Err(Error::auth(error_msg));
195        }
196
197        let auth_response: AuthResponse = response.json().await?;
198
199        if let Some(ref session) = auth_response.session {
200            self.set_session(session.clone()).await?;
201            info!("User signed in successfully");
202        }
203
204        Ok(auth_response)
205    }
206
207    /// Sign out the current user
208    pub async fn sign_out(&self) -> Result<()> {
209        debug!("Signing out user");
210
211        let session = self.get_session()?;
212
213        let response = self
214            .http_client
215            .post(format!("{}/auth/v1/logout", self.config.url))
216            .header("Authorization", format!("Bearer {}", session.access_token))
217            .send()
218            .await?;
219
220        if !response.status().is_success() {
221            warn!("Sign out request failed with status: {}", response.status());
222        }
223
224        self.clear_session().await?;
225        info!("User signed out successfully");
226
227        Ok(())
228    }
229
230    /// Reset password via email
231    pub async fn reset_password_for_email(&self, email: &str) -> Result<()> {
232        self.reset_password_for_email_with_redirect(email, None)
233            .await
234    }
235
236    /// Reset password via email with optional redirect URL
237    pub async fn reset_password_for_email_with_redirect(
238        &self,
239        email: &str,
240        redirect_to: Option<String>,
241    ) -> Result<()> {
242        debug!("Requesting password reset for email: {}", email);
243
244        let payload = PasswordResetRequest {
245            email: email.to_string(),
246            redirect_to,
247        };
248
249        let response = self
250            .http_client
251            .post(format!("{}/auth/v1/recover", self.config.url))
252            .json(&payload)
253            .send()
254            .await?;
255
256        if !response.status().is_success() {
257            let status = response.status();
258            let error_msg = match response.text().await {
259                Ok(text) => text,
260                Err(_) => format!("Password reset failed with status: {}", status),
261            };
262            return Err(Error::auth(error_msg));
263        }
264
265        info!("Password reset email sent successfully");
266        Ok(())
267    }
268
269    /// Update the current user's information
270    pub async fn update_user(
271        &self,
272        email: Option<String>,
273        password: Option<String>,
274        data: Option<serde_json::Value>,
275    ) -> Result<AuthResponse> {
276        debug!("Updating user information");
277
278        let session = self.get_session()?;
279
280        let payload = UpdateUserRequest {
281            email,
282            password,
283            data,
284        };
285
286        let response = self
287            .http_client
288            .put(format!("{}/auth/v1/user", self.config.url))
289            .header("Authorization", format!("Bearer {}", session.access_token))
290            .json(&payload)
291            .send()
292            .await?;
293
294        if !response.status().is_success() {
295            let status = response.status();
296            let error_msg = match response.text().await {
297                Ok(text) => text,
298                Err(_) => format!("User update failed with status: {}", status),
299            };
300            return Err(Error::auth(error_msg));
301        }
302
303        let auth_response: AuthResponse = response.json().await?;
304
305        if let Some(ref session) = auth_response.session {
306            self.set_session(session.clone()).await?;
307        }
308
309        info!("User updated successfully");
310        Ok(auth_response)
311    }
312
313    /// Refresh the current session token
314    pub async fn refresh_session(&self) -> Result<AuthResponse> {
315        debug!("Refreshing session token");
316
317        let current_session = self.get_session()?;
318
319        let payload = RefreshTokenRequest {
320            refresh_token: current_session.refresh_token.clone(),
321        };
322
323        let response = self
324            .http_client
325            .post(format!(
326                "{}/auth/v1/token?grant_type=refresh_token",
327                self.config.url
328            ))
329            .json(&payload)
330            .send()
331            .await?;
332
333        if !response.status().is_success() {
334            let status = response.status();
335            let error_msg = match response.text().await {
336                Ok(text) => text,
337                Err(_) => format!("Token refresh failed with status: {}", status),
338            };
339            return Err(Error::auth(error_msg));
340        }
341
342        let auth_response: AuthResponse = response.json().await?;
343
344        if let Some(ref session) = auth_response.session {
345            self.set_session(session.clone()).await?;
346            info!("Session refreshed successfully");
347        }
348
349        Ok(auth_response)
350    }
351
352    /// Get the current user information
353    pub async fn current_user(&self) -> Result<Option<User>> {
354        let session_guard = self
355            .session
356            .read()
357            .map_err(|_| Error::auth("Failed to read session"))?;
358        Ok(session_guard.as_ref().map(|s| s.user.clone()))
359    }
360
361    /// Get the current session
362    pub fn get_session(&self) -> Result<Session> {
363        let session_guard = self
364            .session
365            .read()
366            .map_err(|_| Error::auth("Failed to read session"))?;
367        session_guard
368            .as_ref()
369            .cloned()
370            .ok_or_else(|| Error::auth("No active session"))
371    }
372
373    /// Set a new session
374    pub async fn set_session(&self, session: Session) -> Result<()> {
375        let mut session_guard = self
376            .session
377            .write()
378            .map_err(|_| Error::auth("Failed to write session"))?;
379        *session_guard = Some(session);
380        Ok(())
381    }
382
383    /// Set session from JWT token
384    pub async fn set_session_token(&self, token: &str) -> Result<()> {
385        debug!("Setting session from token");
386
387        let user_response = self
388            .http_client
389            .get(format!("{}/auth/v1/user", self.config.url))
390            .header("Authorization", format!("Bearer {}", token))
391            .send()
392            .await?;
393
394        if !user_response.status().is_success() {
395            return Err(Error::auth("Invalid token"));
396        }
397
398        let user: User = user_response.json().await?;
399
400        let session = Session {
401            access_token: token.to_string(),
402            refresh_token: String::new(),
403            expires_in: 3600,
404            expires_at: Utc::now() + chrono::Duration::seconds(3600),
405            token_type: "bearer".to_string(),
406            user,
407        };
408
409        self.set_session(session).await?;
410        Ok(())
411    }
412
413    /// Clear the current session
414    pub async fn clear_session(&self) -> Result<()> {
415        let mut session_guard = self
416            .session
417            .write()
418            .map_err(|_| Error::auth("Failed to write session"))?;
419        *session_guard = None;
420        Ok(())
421    }
422
423    /// Check if the user is authenticated
424    pub fn is_authenticated(&self) -> bool {
425        let session_guard = self.session.read().unwrap_or_else(|_| {
426            warn!("Failed to read session lock");
427            self.session.read().unwrap()
428        });
429
430        match session_guard.as_ref() {
431            Some(session) => {
432                let now = Utc::now();
433                session.expires_at > now
434            }
435            None => false,
436        }
437    }
438
439    /// Check if the current token needs refresh
440    pub fn needs_refresh(&self) -> bool {
441        let session_guard = match self.session.read() {
442            Ok(guard) => guard,
443            Err(_) => return false,
444        };
445
446        match session_guard.as_ref() {
447            Some(session) => {
448                let now = Utc::now();
449                let threshold =
450                    chrono::Duration::seconds(self.config.auth_config.refresh_threshold as i64);
451                session.expires_at - now < threshold
452            }
453            None => false,
454        }
455    }
456
457    /// Auto-refresh token if needed
458    pub async fn auto_refresh(&self) -> Result<()> {
459        if !self.config.auth_config.auto_refresh_token || !self.needs_refresh() {
460            return Ok(());
461        }
462
463        debug!("Auto-refreshing token");
464        self.refresh_session().await.map(|_| ())
465    }
466}