1use crate::auth::{AuthError, FirebaseAuth};
4use crate::core::middleware::AuthMiddleware;
5use reqwest::Client;
6use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
7use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
8use serde::{Deserialize, Serialize};
9use url::Url;
10
11const IDENTITY_TOOLKIT_URL: &str = "https://identitytoolkit.googleapis.com/v2";
12
13#[derive(Debug, Serialize, Deserialize, Default)]
15#[serde(rename_all = "camelCase")]
16pub struct Tenant {
17 pub name: String,
20
21 pub display_name: Option<String>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub allow_password_signup: Option<bool>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub enable_email_link_signin: Option<bool>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub disable_auth: Option<bool>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub enable_anonymous_user: Option<bool>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub mfa_config: Option<serde_json::Value>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub recaptcha_config: Option<serde_json::Value>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub sms_region_config: Option<serde_json::Value>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub monitoring: Option<serde_json::Value>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub password_policy_config: Option<serde_json::Value>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub email_privacy_config: Option<serde_json::Value>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub client: Option<serde_json::Value>,
71}
72
73#[derive(Debug, Serialize, Default)]
75#[serde(rename_all = "camelCase")]
76pub struct CreateTenantRequest {
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub display_name: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub allow_password_signup: Option<bool>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub enable_email_link_signin: Option<bool>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub disable_auth: Option<bool>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub enable_anonymous_user: Option<bool>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub mfa_config: Option<serde_json::Value>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub recaptcha_config: Option<serde_json::Value>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub sms_region_config: Option<serde_json::Value>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub monitoring: Option<serde_json::Value>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub password_policy_config: Option<serde_json::Value>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub email_privacy_config: Option<serde_json::Value>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub client: Option<serde_json::Value>,
121}
122
123#[derive(Debug, Serialize, Default)]
125#[serde(rename_all = "camelCase")]
126pub struct UpdateTenantRequest {
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub display_name: Option<String>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub allow_password_signup: Option<bool>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub enable_email_link_signin: Option<bool>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub disable_auth: Option<bool>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub enable_anonymous_user: Option<bool>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub mfa_config: Option<serde_json::Value>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub recaptcha_config: Option<serde_json::Value>,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub sms_region_config: Option<serde_json::Value>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub monitoring: Option<serde_json::Value>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub password_policy_config: Option<serde_json::Value>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub email_privacy_config: Option<serde_json::Value>,
168
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub client: Option<serde_json::Value>,
171}
172
173#[derive(Debug, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct ListTenantsResponse {
177 pub tenants: Option<Vec<Tenant>>,
179 pub next_page_token: Option<String>,
181}
182
183#[derive(Clone)]
185pub struct TenantAwareness {
186 client: ClientWithMiddleware,
187 base_url: String,
188 middleware: AuthMiddleware,
189}
190
191impl TenantAwareness {
192 pub(crate) fn new(middleware: AuthMiddleware) -> Self {
193 let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
194 let client = ClientBuilder::new(Client::new())
195 .with(RetryTransientMiddleware::new_with_policy(retry_policy))
196 .with(middleware.clone())
197 .build();
198
199 let project_id = middleware.key.project_id.clone().unwrap_or_default();
200 let base_url = format!("{}/projects/{}", IDENTITY_TOOLKIT_URL, project_id);
201
202 Self {
203 client,
204 base_url,
205 middleware,
206 }
207 }
208
209 pub fn auth_for_tenant(&self, tenant_id: &str) -> FirebaseAuth {
211 let middleware = self.middleware.with_tenant(tenant_id);
212 FirebaseAuth::new(middleware)
213 }
214
215 pub async fn create_tenant(&self, request: CreateTenantRequest) -> Result<Tenant, AuthError> {
217 let url = format!("{}/tenants", self.base_url);
218
219 let response = self
220 .client
221 .post(&url)
222 .json(&request)
223 .send()
224 .await?;
225
226 if !response.status().is_success() {
227 let status = response.status();
228 let text = response.text().await.unwrap_or_default();
229 return Err(AuthError::ApiError(format!(
230 "Create tenant failed {}: {}",
231 status, text
232 )));
233 }
234
235 let tenant: Tenant = response.json().await?;
236 Ok(tenant)
237 }
238
239 pub async fn get_tenant(&self, tenant_id: &str) -> Result<Tenant, AuthError> {
241 let url = format!("{}/tenants/{}", self.base_url, tenant_id);
242
243 let response = self.client.get(&url).send().await?;
244
245 if !response.status().is_success() {
246 let status = response.status();
247 let text = response.text().await.unwrap_or_default();
248 return Err(AuthError::ApiError(format!(
249 "Get tenant failed {}: {}",
250 status, text
251 )));
252 }
253
254 let tenant: Tenant = response.json().await?;
255 Ok(tenant)
256 }
257
258 pub async fn update_tenant(
260 &self,
261 tenant_id: &str,
262 request: UpdateTenantRequest,
263 ) -> Result<Tenant, AuthError> {
264 let url = format!("{}/tenants/{}", self.base_url, tenant_id);
265
266 let mut mask_parts = Vec::new();
267 if request.display_name.is_some() { mask_parts.push("displayName"); }
268 if request.allow_password_signup.is_some() { mask_parts.push("allowPasswordSignup"); }
269 if request.enable_email_link_signin.is_some() { mask_parts.push("enableEmailLinkSignin"); }
270 if request.disable_auth.is_some() { mask_parts.push("disableAuth"); }
271 if request.enable_anonymous_user.is_some() { mask_parts.push("enableAnonymousUser"); }
272 if request.test_phone_numbers.is_some() { mask_parts.push("testPhoneNumbers"); }
273 if request.mfa_config.is_some() { mask_parts.push("mfaConfig"); }
274 if request.recaptcha_config.is_some() { mask_parts.push("recaptchaConfig"); }
275 if request.sms_region_config.is_some() { mask_parts.push("smsRegionConfig"); }
276 if request.monitoring.is_some() { mask_parts.push("monitoring"); }
277 if request.password_policy_config.is_some() { mask_parts.push("passwordPolicyConfig"); }
278 if request.email_privacy_config.is_some() { mask_parts.push("emailPrivacyConfig"); }
279 if request.client.is_some() { mask_parts.push("client"); }
280
281 let update_mask = mask_parts.join(",");
282
283 let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
284 url_obj.query_pairs_mut().append_pair("updateMask", &update_mask);
285
286 let response = self
287 .client
288 .patch(url_obj)
289 .json(&request)
290 .send()
291 .await?;
292
293 if !response.status().is_success() {
294 let status = response.status();
295 let text = response.text().await.unwrap_or_default();
296 return Err(AuthError::ApiError(format!(
297 "Update tenant failed {}: {}",
298 status, text
299 )));
300 }
301
302 let tenant: Tenant = response.json().await?;
303 Ok(tenant)
304 }
305
306 pub async fn delete_tenant(&self, tenant_id: &str) -> Result<(), AuthError> {
308 let url = format!("{}/tenants/{}", self.base_url, tenant_id);
309
310 let response = self.client.delete(&url).send().await?;
311
312 if !response.status().is_success() {
313 let status = response.status();
314 let text = response.text().await.unwrap_or_default();
315 return Err(AuthError::ApiError(format!(
316 "Delete tenant failed {}: {}",
317 status, text
318 )));
319 }
320
321 Ok(())
322 }
323
324 pub async fn list_tenants(
326 &self,
327 max_results: Option<u32>,
328 page_token: Option<&str>,
329 ) -> Result<ListTenantsResponse, AuthError> {
330 let url = format!("{}/tenants", self.base_url);
331 let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
332
333 {
334 let mut query_pairs = url_obj.query_pairs_mut();
335 if let Some(max) = max_results {
336 query_pairs.append_pair("pageSize", &max.to_string());
337 }
338 if let Some(token) = page_token {
339 query_pairs.append_pair("pageToken", token);
340 }
341 }
342
343 let response = self.client.get(url_obj).send().await?;
344
345 if !response.status().is_success() {
346 let status = response.status();
347 let text = response.text().await.unwrap_or_default();
348 return Err(AuthError::ApiError(format!(
349 "List tenants failed {}: {}",
350 status, text
351 )));
352 }
353
354 let result: ListTenantsResponse = response.json().await?;
355 Ok(result)
356 }
357}