diff --git a/api/libs/database/Cargo.toml b/api/libs/database/Cargo.toml index d24a2f096..66441df2c 100644 --- a/api/libs/database/Cargo.toml +++ b/api/libs/database/Cargo.toml @@ -25,6 +25,8 @@ tokio-postgres = { workspace = true } tokio-postgres-rustls = { workspace = true } bb8-redis = { workspace = true } serde_yaml = { workspace = true } +dotenv = { workspace = true } +reqwest = { workspace = true } [dev-dependencies] diff --git a/api/libs/database/src/lib.rs b/api/libs/database/src/lib.rs index e4baf150f..2897e25b2 100644 --- a/api/libs/database/src/lib.rs +++ b/api/libs/database/src/lib.rs @@ -5,5 +5,7 @@ pub mod schema; pub mod vault; pub mod helpers; pub mod types; +pub mod supabase; -pub use helpers::*; \ No newline at end of file +pub use helpers::*; +pub use supabase::*; \ No newline at end of file diff --git a/api/libs/database/src/supabase/client.rs b/api/libs/database/src/supabase/client.rs new file mode 100644 index 000000000..c637db664 --- /dev/null +++ b/api/libs/database/src/supabase/client.rs @@ -0,0 +1,168 @@ +use std::env; + +use dotenv::dotenv; +use reqwest::{Client, Error}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +// Structs for JSON payloads and responses +#[derive(Serialize, Deserialize, Debug)] +struct CreateUserRequest { + id: String, + email: String, + #[serde(skip_serializing_if = "Option::is_none")] + email_confirm: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +struct InviteRequest { + email: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct RecoverRequest { + email: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct UpdateUserRequest { + password: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct User { + pub id: String, + pub email: String, + pub aud: String, + pub role: String, + pub created_at: String, + pub updated_at: String, + pub email_confirmed_at: Option, +} + +// Main Supabase client struct +pub struct SupabaseClient { + url: String, + service_role_key: String, + client: Client, +} + +impl SupabaseClient { + // Initialize the client with env variables + pub fn new() -> Result { + dotenv().ok(); // Load .env file + + let url = env::var("SUPABASE_URL")?; + let service_role_key = env::var("SUPABASE_SERVICE_ROLE_KEY")?; + + Ok(Self { + url, + service_role_key, + client: Client::new(), + }) + } + + // 1. Create a new user with a specific ID + pub async fn create_user(&self, id: Uuid, email: &str) -> Result { + let payload = CreateUserRequest { + id: id.to_string(), + email: email.to_string(), + email_confirm: Some(false), // Email unconfirmed initially + }; + + let response = self + .client + .post(format!("{}/auth/v1/admin/users", self.url)) + .header("Authorization", format!("Bearer {}", self.service_role_key)) + .header("Content-Type", "application/json") + .header("apikey", &self.service_role_key) + .json(&payload) + .send() + .await?; + + let user = response.json::().await?; + Ok(user) + } + + // 2. Send a verification email + pub async fn send_verification_email(&self, email: &str) -> Result<(), Error> { + let payload = InviteRequest { + email: email.to_string(), + }; + + self.client + .post(format!("{}/auth/v1/invite", self.url)) + .header("Authorization", format!("Bearer {}", self.service_role_key)) + .header("Content-Type", "application/json") + .header("apikey", &self.service_role_key) + .json(&payload) + .send() + .await?; + + Ok(()) + } + + // 3a. Trigger a password reset email (for user to set password) + pub async fn send_password_reset_email(&self, email: &str) -> Result<(), Error> { + let payload = RecoverRequest { + email: email.to_string(), + }; + + self.client + .post(format!("{}/auth/v1/recover", self.url)) + .header("Authorization", format!("Bearer {}", self.service_role_key)) + .header("Content-Type", "application/json") + .header("apikey", &self.service_role_key) + .json(&payload) + .send() + .await?; + + Ok(()) + } + + // 3b. Update user password (simulating user action with recovery token) + pub async fn update_user_password(&self, recovery_token: &str, password: &str) -> Result<(), Error> { + let payload = UpdateUserRequest { + password: password.to_string(), + }; + + self.client + .put(format!("{}/auth/v1/user", self.url)) + .header("Authorization", format!("Bearer {}", recovery_token)) + .header("Content-Type", "application/json") + .header("apikey", &self.service_role_key) + .json(&payload) + .send() + .await?; + + Ok(()) + } +} + +// Example usage in a main function (for testing) +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_supabase_client() { + let client = SupabaseClient::new().expect("Failed to initialize client"); + + // 1. Create a user + let user_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(); + let user = client.create_user(user_id, "newuser@example.com").await.unwrap(); + println!("Created user: {:?}", user); + + // 2. Send verification email + client.send_verification_email("newuser@example.com").await.unwrap(); + println!("Verification email sent. Check Inbucket at http://localhost:54324"); + + // 3. Simulate password reset (manual token extraction needed from email) + client.send_password_reset_email("newuser@example.com").await.unwrap(); + println!("Password reset email sent. Check Inbucket for recovery token"); + + // Normally, you'd extract the recovery token from the email and pass it here + // For testing, you'd need to manually get it from Inbucket + // client.update_user_password("", "newsecurepassword123").await.unwrap(); + } +} \ No newline at end of file diff --git a/api/libs/database/src/supabase/mod.rs b/api/libs/database/src/supabase/mod.rs new file mode 100644 index 000000000..393551766 --- /dev/null +++ b/api/libs/database/src/supabase/mod.rs @@ -0,0 +1 @@ +pub mod client; \ No newline at end of file