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!("{}/auth/v1/token", self.config.url))
181            .header("grant_type", "password")
182            .json(&payload)
183            .send()
184            .await?;
185
186        if !response.status().is_success() {
187            let status = response.status();
188            let error_msg = match response.text().await {
189                Ok(text) => text,
190                Err(_) => format!("Sign in failed with status: {}", status),
191            };
192            return Err(Error::auth(error_msg));
193        }
194
195        let auth_response: AuthResponse = response.json().await?;
196
197        if let Some(ref session) = auth_response.session {
198            self.set_session(session.clone()).await?;
199            info!("User signed in successfully");
200        }
201
202        Ok(auth_response)
203    }
204
205    /// Sign out the current user
206    pub async fn sign_out(&self) -> Result<()> {
207        debug!("Signing out user");
208
209        let session = self.get_session()?;
210
211        let response = self
212            .http_client
213            .post(format!("{}/auth/v1/logout", self.config.url))
214            .header("Authorization", format!("Bearer {}", session.access_token))
215            .send()
216            .await?;
217
218        if !response.status().is_success() {
219            warn!("Sign out request failed with status: {}", response.status());
220        }
221
222        self.clear_session().await?;
223        info!("User signed out successfully");
224
225        Ok(())
226    }
227
228    /// Reset password via email
229    pub async fn reset_password_for_email(&self, email: &str) -> Result<()> {
230        self.reset_password_for_email_with_redirect(email, None)
231            .await
232    }
233
234    /// Reset password via email with optional redirect URL
235    pub async fn reset_password_for_email_with_redirect(
236        &self,
237        email: &str,
238        redirect_to: Option<String>,
239    ) -> Result<()> {
240        debug!("Requesting password reset for email: {}", email);
241
242        let payload = PasswordResetRequest {
243            email: email.to_string(),
244            redirect_to,
245        };
246
247        let response = self
248            .http_client
249            .post(format!("{}/auth/v1/recover", self.config.url))
250            .json(&payload)
251            .send()
252            .await?;
253
254        if !response.status().is_success() {
255            let status = response.status();
256            let error_msg = match response.text().await {
257                Ok(text) => text,
258                Err(_) => format!("Password reset failed with status: {}", status),
259            };
260            return Err(Error::auth(error_msg));
261        }
262
263        info!("Password reset email sent successfully");
264        Ok(())
265    }
266
267    /// Update the current user's information
268    pub async fn update_user(
269        &self,
270        email: Option<String>,
271        password: Option<String>,
272        data: Option<serde_json::Value>,
273    ) -> Result<AuthResponse> {
274        debug!("Updating user information");
275
276        let session = self.get_session()?;
277
278        let payload = UpdateUserRequest {
279            email,
280            password,
281            data,
282        };
283
284        let response = self
285            .http_client
286            .put(format!("{}/auth/v1/user", self.config.url))
287            .header("Authorization", format!("Bearer {}", session.access_token))
288            .json(&payload)
289            .send()
290            .await?;
291
292        if !response.status().is_success() {
293            let status = response.status();
294            let error_msg = match response.text().await {
295                Ok(text) => text,
296                Err(_) => format!("User update failed with status: {}", status),
297            };
298            return Err(Error::auth(error_msg));
299        }
300
301        let auth_response: AuthResponse = response.json().await?;
302
303        if let Some(ref session) = auth_response.session {
304            self.set_session(session.clone()).await?;
305        }
306
307        info!("User updated successfully");
308        Ok(auth_response)
309    }
310
311    /// Refresh the current session token
312    pub async fn refresh_session(&self) -> Result<AuthResponse> {
313        debug!("Refreshing session token");
314
315        let current_session = self.get_session()?;
316
317        let payload = RefreshTokenRequest {
318            refresh_token: current_session.refresh_token.clone(),
319        };
320
321        let response = self
322            .http_client
323            .post(format!("{}/auth/v1/token", self.config.url))
324            .header("grant_type", "refresh_token")
325            .json(&payload)
326            .send()
327            .await?;
328
329        if !response.status().is_success() {
330            let status = response.status();
331            let error_msg = match response.text().await {
332                Ok(text) => text,
333                Err(_) => format!("Token refresh failed with status: {}", status),
334            };
335            return Err(Error::auth(error_msg));
336        }
337
338        let auth_response: AuthResponse = response.json().await?;
339
340        if let Some(ref session) = auth_response.session {
341            self.set_session(session.clone()).await?;
342            info!("Session refreshed successfully");
343        }
344
345        Ok(auth_response)
346    }
347
348    /// Get the current user information
349    pub async fn current_user(&self) -> Result<Option<User>> {
350        let session_guard = self
351            .session
352            .read()
353            .map_err(|_| Error::auth("Failed to read session"))?;
354        Ok(session_guard.as_ref().map(|s| s.user.clone()))
355    }
356
357    /// Get the current session
358    pub fn get_session(&self) -> Result<Session> {
359        let session_guard = self
360            .session
361            .read()
362            .map_err(|_| Error::auth("Failed to read session"))?;
363        session_guard
364            .as_ref()
365            .cloned()
366            .ok_or_else(|| Error::auth("No active session"))
367    }
368
369    /// Set a new session
370    pub async fn set_session(&self, session: Session) -> Result<()> {
371        let mut session_guard = self
372            .session
373            .write()
374            .map_err(|_| Error::auth("Failed to write session"))?;
375        *session_guard = Some(session);
376        Ok(())
377    }
378
379    /// Set session from JWT token
380    pub async fn set_session_token(&self, token: &str) -> Result<()> {
381        debug!("Setting session from token");
382
383        let user_response = self
384            .http_client
385            .get(format!("{}/auth/v1/user", self.config.url))
386            .header("Authorization", format!("Bearer {}", token))
387            .send()
388            .await?;
389
390        if !user_response.status().is_success() {
391            return Err(Error::auth("Invalid token"));
392        }
393
394        let user: User = user_response.json().await?;
395
396        let session = Session {
397            access_token: token.to_string(),
398            refresh_token: String::new(),
399            expires_in: 3600,
400            expires_at: Utc::now() + chrono::Duration::seconds(3600),
401            token_type: "bearer".to_string(),
402            user,
403        };
404
405        self.set_session(session).await?;
406        Ok(())
407    }
408
409    /// Clear the current session
410    pub async fn clear_session(&self) -> Result<()> {
411        let mut session_guard = self
412            .session
413            .write()
414            .map_err(|_| Error::auth("Failed to write session"))?;
415        *session_guard = None;
416        Ok(())
417    }
418
419    /// Check if the user is authenticated
420    pub fn is_authenticated(&self) -> bool {
421        let session_guard = self.session.read().unwrap_or_else(|_| {
422            warn!("Failed to read session lock");
423            self.session.read().unwrap()
424        });
425
426        match session_guard.as_ref() {
427            Some(session) => {
428                let now = Utc::now();
429                session.expires_at > now
430            }
431            None => false,
432        }
433    }
434
435    /// Check if the current token needs refresh
436    pub fn needs_refresh(&self) -> bool {
437        let session_guard = match self.session.read() {
438            Ok(guard) => guard,
439            Err(_) => return false,
440        };
441
442        match session_guard.as_ref() {
443            Some(session) => {
444                let now = Utc::now();
445                let threshold =
446                    chrono::Duration::seconds(self.config.auth_config.refresh_threshold as i64);
447                session.expires_at - now < threshold
448            }
449            None => false,
450        }
451    }
452
453    /// Auto-refresh token if needed
454    pub async fn auto_refresh(&self) -> Result<()> {
455        if !self.config.auth_config.auto_refresh_token || !self.needs_refresh() {
456            return Ok(());
457        }
458
459        debug!("Auto-refreshing token");
460        self.refresh_session().await.map(|_| ())
461    }
462}