use crate::auth::{AuthError, FirebaseAuth};
use crate::core::middleware::AuthMiddleware;
use reqwest::Client;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use serde::{Deserialize, Serialize};
use url::Url;
const IDENTITY_TOOLKIT_URL: &str = "https://identitytoolkit.googleapis.com/v2";
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Tenant {
pub name: String,
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_password_signup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_email_link_signin: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_auth: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_anonymous_user: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mfa_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recaptcha_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sms_region_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitoring: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password_policy_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_privacy_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CreateTenantRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_password_signup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_email_link_signin: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_auth: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_anonymous_user: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mfa_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recaptcha_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sms_region_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitoring: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password_policy_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_privacy_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UpdateTenantRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_password_signup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_email_link_signin: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_auth: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_anonymous_user: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mfa_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recaptcha_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sms_region_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitoring: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password_policy_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_privacy_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListTenantsResponse {
pub tenants: Option<Vec<Tenant>>,
pub next_page_token: Option<String>,
}
#[derive(Clone)]
pub struct TenantAwareness {
client: ClientWithMiddleware,
base_url: String,
middleware: AuthMiddleware,
}
impl TenantAwareness {
pub(crate) fn new(middleware: AuthMiddleware) -> Self {
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
let client = ClientBuilder::new(Client::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.with(middleware.clone())
.build();
let project_id = middleware.key.project_id.clone().unwrap_or_default();
let base_url = format!("{}/projects/{}", IDENTITY_TOOLKIT_URL, project_id);
Self {
client,
base_url,
middleware,
}
}
pub fn auth_for_tenant(&self, tenant_id: &str) -> FirebaseAuth {
let middleware = self.middleware.with_tenant(tenant_id);
FirebaseAuth::new(middleware)
}
pub async fn create_tenant(&self, request: CreateTenantRequest) -> Result<Tenant, AuthError> {
let url = format!("{}/tenants", self.base_url);
let response = self
.client
.post(&url)
.json(&request)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(AuthError::ApiError(format!(
"Create tenant failed {}: {}",
status, text
)));
}
let tenant: Tenant = response.json().await?;
Ok(tenant)
}
pub async fn get_tenant(&self, tenant_id: &str) -> Result<Tenant, AuthError> {
let url = format!("{}/tenants/{}", self.base_url, tenant_id);
let response = self.client.get(&url).send().await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(AuthError::ApiError(format!(
"Get tenant failed {}: {}",
status, text
)));
}
let tenant: Tenant = response.json().await?;
Ok(tenant)
}
pub async fn update_tenant(
&self,
tenant_id: &str,
request: UpdateTenantRequest,
) -> Result<Tenant, AuthError> {
let url = format!("{}/tenants/{}", self.base_url, tenant_id);
let mut mask_parts = Vec::new();
if request.display_name.is_some() { mask_parts.push("displayName"); }
if request.allow_password_signup.is_some() { mask_parts.push("allowPasswordSignup"); }
if request.enable_email_link_signin.is_some() { mask_parts.push("enableEmailLinkSignin"); }
if request.disable_auth.is_some() { mask_parts.push("disableAuth"); }
if request.enable_anonymous_user.is_some() { mask_parts.push("enableAnonymousUser"); }
if request.test_phone_numbers.is_some() { mask_parts.push("testPhoneNumbers"); }
if request.mfa_config.is_some() { mask_parts.push("mfaConfig"); }
if request.recaptcha_config.is_some() { mask_parts.push("recaptchaConfig"); }
if request.sms_region_config.is_some() { mask_parts.push("smsRegionConfig"); }
if request.monitoring.is_some() { mask_parts.push("monitoring"); }
if request.password_policy_config.is_some() { mask_parts.push("passwordPolicyConfig"); }
if request.email_privacy_config.is_some() { mask_parts.push("emailPrivacyConfig"); }
if request.client.is_some() { mask_parts.push("client"); }
let update_mask = mask_parts.join(",");
let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
url_obj.query_pairs_mut().append_pair("updateMask", &update_mask);
let response = self
.client
.patch(url_obj)
.json(&request)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(AuthError::ApiError(format!(
"Update tenant failed {}: {}",
status, text
)));
}
let tenant: Tenant = response.json().await?;
Ok(tenant)
}
pub async fn delete_tenant(&self, tenant_id: &str) -> Result<(), AuthError> {
let url = format!("{}/tenants/{}", self.base_url, tenant_id);
let response = self.client.delete(&url).send().await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(AuthError::ApiError(format!(
"Delete tenant failed {}: {}",
status, text
)));
}
Ok(())
}
pub async fn list_tenants(
&self,
max_results: Option<u32>,
page_token: Option<&str>,
) -> Result<ListTenantsResponse, AuthError> {
let url = format!("{}/tenants", self.base_url);
let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
{
let mut query_pairs = url_obj.query_pairs_mut();
if let Some(max) = max_results {
query_pairs.append_pair("pageSize", &max.to_string());
}
if let Some(token) = page_token {
query_pairs.append_pair("pageToken", token);
}
}
let response = self.client.get(url_obj).send().await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(AuthError::ApiError(format!(
"List tenants failed {}: {}",
status, text
)));
}
let result: ListTenantsResponse = response.json().await?;
Ok(result)
}
}