1use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
2use reqwest::{Error, Response};
3use serde::{Deserialize, Serialize};
4
5use crate::Supabase;
6
7#[derive(Serialize)]
8struct Credentials<'a> {
9 email: &'a str,
10 password: &'a str,
11}
12
13#[derive(Serialize)]
14struct RefreshTokenRequest<'a> {
15 refresh_token: &'a str,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Claims {
21 pub sub: String,
22 pub email: String,
23 pub exp: usize,
24}
25
26#[derive(Debug)]
28pub struct LogoutError;
29
30impl std::fmt::Display for LogoutError {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 write!(f, "bearer token required for logout")
33 }
34}
35
36impl std::error::Error for LogoutError {}
37
38impl Supabase {
39 pub fn jwt_valid(&self, jwt: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
43 let decoding_key = DecodingKey::from_secret(self.jwt.as_bytes());
44 let validation = Validation::new(Algorithm::HS256);
45 let token_data = decode::<Claims>(jwt, &decoding_key, &validation)?;
46 Ok(token_data.claims)
47 }
48
49 pub async fn sign_in_password(&self, email: &str, password: &str) -> Result<Response, Error> {
53 let url = format!("{}/auth/v1/token?grant_type=password", self.url);
54
55 self.client
56 .post(&url)
57 .header("apikey", &self.api_key)
58 .header("Content-Type", "application/json")
59 .json(&Credentials { email, password })
60 .send()
61 .await
62 }
63
64 pub async fn refresh_token(&self, refresh_token: &str) -> Result<Response, Error> {
68 let url = format!("{}/auth/v1/token?grant_type=refresh_token", self.url);
69
70 self.client
71 .post(&url)
72 .header("apikey", &self.api_key)
73 .header("Content-Type", "application/json")
74 .json(&RefreshTokenRequest { refresh_token })
75 .send()
76 .await
77 }
78
79 pub async fn logout(&self) -> Result<Result<Response, Error>, LogoutError> {
84 let token = self.bearer_token.as_ref().ok_or(LogoutError)?;
85 let url = format!("{}/auth/v1/logout", self.url);
86
87 Ok(self
88 .client
89 .post(&url)
90 .header("apikey", &self.api_key)
91 .header("Content-Type", "application/json")
92 .bearer_auth(token)
93 .send()
94 .await)
95 }
96
97 pub async fn signup_email_password(
99 &self,
100 email: &str,
101 password: &str,
102 ) -> Result<Response, Error> {
103 let url = format!("{}/auth/v1/signup", self.url);
104
105 self.client
106 .post(&url)
107 .header("apikey", &self.api_key)
108 .header("Content-Type", "application/json")
109 .json(&Credentials { email, password })
110 .send()
111 .await
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn client() -> Supabase {
120 Supabase::new(None, None, None)
121 }
122
123 async fn sign_in_password() -> Result<Response, Error> {
124 let client = client();
125 let test_email = std::env::var("SUPABASE_TEST_EMAIL").unwrap_or_default();
126 let test_pass = std::env::var("SUPABASE_TEST_PASS").unwrap_or_default();
127 client.sign_in_password(&test_email, &test_pass).await
128 }
129
130 #[tokio::test]
131 async fn test_token_with_password() {
132 let response = match sign_in_password().await {
133 Ok(resp) => resp,
134 Err(e) => {
135 println!("Test skipped due to network error: {e}");
136 return;
137 }
138 };
139
140 let json: serde_json::Value = response.json().await.unwrap();
141
142 let Some(token) = json["access_token"].as_str() else {
143 println!("Test skipped: invalid credentials or server response");
144 return;
145 };
146 let Some(refresh) = json["refresh_token"].as_str() else {
147 println!("Test skipped: invalid credentials or server response");
148 return;
149 };
150
151 assert!(!token.is_empty());
152 assert!(!refresh.is_empty());
153 }
154
155 #[tokio::test]
156 async fn test_refresh() {
157 let response = match sign_in_password().await {
158 Ok(resp) => resp,
159 Err(e) => {
160 println!("Test skipped due to network error: {e}");
161 return;
162 }
163 };
164
165 let json: serde_json::Value = response.json().await.unwrap();
166 let Some(refresh_token) = json["refresh_token"].as_str() else {
167 println!("Test skipped: no refresh token in response");
168 return;
169 };
170
171 let response = match client().refresh_token(refresh_token).await {
172 Ok(resp) => resp,
173 Err(e) => {
174 println!("Test skipped due to network error: {e}");
175 return;
176 }
177 };
178
179 if response.status() == 400 {
180 println!("Skipping: automatic reuse detection is enabled");
181 return;
182 }
183
184 let json: serde_json::Value = response.json().await.unwrap();
185 let Some(token) = json["access_token"].as_str() else {
186 println!("Test skipped: no access token in refresh response");
187 return;
188 };
189
190 assert!(!token.is_empty());
191 }
192
193 #[tokio::test]
194 async fn test_logout() {
195 let response = match sign_in_password().await {
196 Ok(resp) => resp,
197 Err(e) => {
198 println!("Test skipped due to network error: {e}");
199 return;
200 }
201 };
202
203 let json: serde_json::Value = response.json().await.unwrap();
204 let Some(access_token) = json["access_token"].as_str() else {
205 println!("Test skipped: no access token in response");
206 return;
207 };
208
209 let mut client = client();
210 client.set_bearer_token(access_token);
211
212 let response = match client.logout().await {
213 Ok(Ok(resp)) => resp,
214 Ok(Err(e)) => {
215 println!("Test skipped due to network error: {e}");
216 return;
217 }
218 Err(e) => {
219 println!("Test skipped: {e}");
220 return;
221 }
222 };
223
224 assert_eq!(response.status(), 204);
225 }
226
227 #[tokio::test]
228 async fn test_signup_email_password() {
229 use rand::distr::Alphanumeric;
230 use rand::{rng, Rng};
231
232 let client = client();
233
234 let rand_string: String = rng()
235 .sample_iter(&Alphanumeric)
236 .take(20)
237 .map(char::from)
238 .collect();
239
240 let email = format!("{rand_string}@a-rust-domain-that-does-not-exist.com");
241
242 let response = match client.signup_email_password(&email, &rand_string).await {
243 Ok(resp) => resp,
244 Err(e) => {
245 println!("Test skipped due to network error: {e}");
246 return;
247 }
248 };
249
250 assert_eq!(response.status(), 200);
251 }
252
253 #[tokio::test]
254 async fn test_authenticate_token() {
255 let client = client();
256
257 let response = match sign_in_password().await {
258 Ok(resp) => resp,
259 Err(e) => {
260 println!("Test skipped due to network error: {e}");
261 return;
262 }
263 };
264
265 let json: serde_json::Value = response.json().await.unwrap();
266 let Some(token) = json["access_token"].as_str() else {
267 println!("Test skipped: no access token in response");
268 return;
269 };
270
271 assert!(client.jwt_valid(token).is_ok());
272 }
273
274 #[test]
275 fn test_logout_requires_bearer_token() {
276 assert_eq!(format!("{}", LogoutError), "bearer token required for logout");
278 }
279}