Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/auth/credential/api_auth_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ impl ApiAuthTokenManager {
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion={jwt}"
);

let url = format!(
"https://{GOOGLE_AUTH_TOKEN_HOST}{GOOGLE_AUTH_TOKEN_PATH}"
);
let url = format!("https://{GOOGLE_AUTH_TOKEN_HOST}{GOOGLE_AUTH_TOKEN_PATH}");

let res = self
.http_client
Expand Down
171 changes: 163 additions & 8 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::collections::HashMap;

use anyhow::Context;
use reqwest::Response;
use serde::{de::DeserializeOwned, Deserialize, Serialize};

use crate::{
auth::{
error::AuthApiErrorResponse,
models::{UpdateUserBody, UpdateUserValues},
models::{BatchGetResponse, UpdateUserBody, UpdateUserValues},
},
error::FirebaseError,
ServiceAccount,
Expand All @@ -26,6 +28,7 @@ pub struct FirebaseAuthClient {
api_url: String,
user_token_manager: UserTokenManager,
api_auth_token_manager: ApiAuthTokenManager,
project_id: String,
}

impl FirebaseAuthClient {
Expand All @@ -36,35 +39,52 @@ impl FirebaseAuthClient {
.context("Failed to create HTTP client")?;

let credential_manager = ApiAuthTokenManager::new(service_account.clone());
let project_id = service_account.project_id.clone();
let token_handler = UserTokenManager::new(service_account, client.clone());

Ok(Self {
user_token_manager: token_handler,
client,
api_url: "https://identitytoolkit.googleapis.com/v1".to_string(),
api_auth_token_manager: credential_manager,
project_id,
})
}

fn url(&self, path: impl AsRef<str>) -> String {
format!("{}{}", self.api_url, path.as_ref())
}

/// Creates a new `POST` request builder with the `Authorization` header set
/// to an authorized admin access token.
async fn auth_post(
&self,
url: impl AsRef<str>,
) -> Result<reqwest::RequestBuilder, FirebaseError> {
fn project_url(&self, path: impl AsRef<str>) -> String {
format!(
"{}/projects/{}{}",
self.api_url,
&self.project_id,
path.as_ref()
)
}

async fn get_access_token(&self) -> Result<String, FirebaseError> {
let access_token = self
.api_auth_token_manager
.get_access_token()
.await
.map_err(|e| {
tracing::error!("Failed to get access token: {}", e);
tracing::error!("Failed to get access token: {e}");
e
})?;

Ok(access_token)
}
Comment on lines +67 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Minor: avoid double-logging errors.

You log in map_err and again via response_error at call sites. Consider downgrading this log to debug to reduce noise.

-                tracing::error!("Failed to get access token: {e}");
+                tracing::debug!("Failed to get access token: {e}");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn get_access_token(&self) -> Result<String, FirebaseError> {
let access_token = self
.api_auth_token_manager
.get_access_token()
.await
.map_err(|e| {
tracing::error!("Failed to get access token: {}", e);
tracing::error!("Failed to get access token: {e}");
e
})?;
Ok(access_token)
}
async fn get_access_token(&self) -> Result<String, FirebaseError> {
let access_token = self
.api_auth_token_manager
.get_access_token()
.await
.map_err(|e| {
tracing::debug!("Failed to get access token: {e}");
e
})?;
Ok(access_token)
}
🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 67 to 78, the map_err closure currently logs the
error at error level which causes duplicate noisy logs at call sites; change
that tracing::error! call to tracing::debug! (or remove the log entirely) so the
error is still mapped through unchanged but only gets logged at callers or in
debug mode, keeping the map_err closure as a pass-through that does not produce
redundant error-level logs.


/// Creates a new `POST` request builder with the `Authorization` header set
/// to an authorized admin access token.
async fn auth_post(
&self,
url: impl AsRef<str>,
) -> Result<reqwest::RequestBuilder, FirebaseError> {
let access_token = self.get_access_token().await?;

let builder = self
.client
.post(url.as_ref())
Expand All @@ -73,6 +93,22 @@ impl FirebaseAuthClient {
Ok(builder)
}

/// Creates a new `GET` request builder with the `Authorization` header set
/// to an authorized admin access token.
async fn auth_get(
&self,
url: impl AsRef<str>,
) -> Result<reqwest::RequestBuilder, FirebaseError> {
let access_token = self.get_access_token().await?;

let builder = self
.client
.get(url.as_ref())
.header("Authorization", format!("Bearer {access_token}"));

Ok(builder)
}

Comment on lines +96 to +111

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Optional: set Accept header for JSON responses.

Not required, but can make intent explicit.

-            .get(url.as_ref())
+            .get(url.as_ref())
+            .header(reqwest::header::ACCEPT, "application/json")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Creates a new `GET` request builder with the `Authorization` header set
/// to an authorized admin access token.
async fn auth_get(
&self,
url: impl AsRef<str>,
) -> Result<reqwest::RequestBuilder, FirebaseError> {
let access_token = self.get_access_token().await?;
let builder = self
.client
.get(url.as_ref())
.header("Authorization", format!("Bearer {access_token}"));
Ok(builder)
}
/// Creates a new `GET` request builder with the `Authorization` header set
/// to an authorized admin access token.
async fn auth_get(
&self,
url: impl AsRef<str>,
) -> Result<reqwest::RequestBuilder, FirebaseError> {
let access_token = self.get_access_token().await?;
let builder = self
.client
.get(url.as_ref())
.header(reqwest::header::ACCEPT, "application/json")
.header("Authorization", format!("Bearer {access_token}"));
Ok(builder)
}
🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 96 to 111, the auth_get builder sets the
Authorization header but does not declare the expected response content type;
add an Accept: application/json header to the request builder so intent is
explicit. Update the chain that builds `builder` to include an
`.header("Accept", "application/json")` alongside the existing Authorization
header and return the resulting builder unchanged.

/// Decodes an ID token and returns its claims. Only succeeds if the token
/// is valid. The token is valid if it:
///
Expand Down Expand Up @@ -306,6 +342,125 @@ impl FirebaseAuthClient {
Ok(user)
}

/// Retrieve all users in Firebase Auth. By default, this will send multiple batch requests to
/// the Firebase Auth API sequentially until all users have been retrieved. If you have
/// thousands of users, this can take a while. See
/// [`get_all_users_concurrently`](Self::get_all_users_concurrently) if you want these requests
/// to be sent concurrently by `N` workers.
#[tracing::instrument(name = "Get all users", skip_all)]
pub async fn get_all_users(&self) -> Result<Vec<User>, FirebaseError> {
self.get_all_users_concurrently(1).await
}

/// Retrieve all users in Firebase Auth using `num_workers` concurrent workers. See
/// [`get_all_users`](Self::get_all_users) for more info.
///
/// The number of workers should be between 1 and 62. If you pass a number outside
/// this range, it will be clamped.
///
/// Keep in mind that, at the time of writing, the Identity Toolkit API limit is 500
/// requests/second per service account. This method will fire off multiple requests
/// simultaneously. See the up-to-date limits in the [official docs](https://firebase.google.com/docs/auth/limits).
#[tracing::instrument(name = "Get all users concurrently", skip_all)]
Comment on lines +355 to +364

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Docs: adjust wording after dropping sharding.

The docstring still advertises concurrent workers. Update to reflect sequential paging or document why concurrency isn’t supported for listing.

🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 355 to 364, the docstring still describes
concurrent workers and sharding for listing users; update it to reflect the
current sequential paging implementation (or explicitly state why concurrency
was removed/not supported). Replace references to "num_workers" and "concurrent
workers" with wording that explains the method pages sequentially through
results, mention the API rate limits and that requests are issued one page at a
time, and/or add a brief note explaining why concurrent listing isn't used
(e.g., lack of sharded endpoints or to respect per-service-account rate limits).

Comment on lines +361 to +364

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider bounding maximum workers even if reintroducing concurrency later.

If you bring back concurrency for other flows, cap it (e.g., min(num_workers, 16)) and use a Semaphore to avoid bursts.

🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 361 to 364, the comment and instrumented method
currently allow unbounded concurrent requests; when reintroducing concurrency
cap the maximum workers to avoid exceeding API limits (for example set
max_workers = min(num_workers, 16)) and protect concurrent launches with a
tokio::sync::Semaphore (acquire a permit per task and release after completion)
so bursts are limited and the request rate stays within quota.

🛠️ Refactor suggestion

Add retries/backoff and a per-request timeout to respect API limits.

You note 500 req/s, but there’s no retry/backoff or timeout. Add exponential backoff on 429/5xx and a sane timeout to avoid hung tasks.

Would you like a small helper (with jitter) wired into auth_get/auth_post?

pub async fn get_all_users_concurrently(
&self,
num_workers: u8,
) -> Result<Vec<User>, FirebaseError> {
let base_url = self.project_url("/accounts:batchGet");

fn make_pagination_url(
base_url: &str,
max_results: usize,
next_page_token: Option<&str>,
) -> String {
format!(
"{base_url}?maxResults={max_results}{}",
next_page_token
.map(|token| format!("&nextPageToken={token}"))
.unwrap_or_else(|| { "".to_string() })
)
}

Comment on lines +371 to +383

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t hand-roll query strings; URL-encode tokens or use .query().

Manually formatting nextPageToken risks broken URLs if the token contains reserved chars. Use reqwest’s .query(...) instead.

Apply this minimal change if you keep the current structure:

-            let url = make_pagination_url(base_url, MAX_RESULTS, next_page_token.as_deref());
-            let res = client
-                .auth_get(url)
-                .await?
-                .header("Content-Type", "application/json")
-                .send()
+            let mut req = client.auth_get(base_url).await?;
+            req = req.query(&[("maxResults", MAX_RESULTS)]);
+            if let Some(ref t) = next_page_token { req = req.query(&[("nextPageToken", t)]); }
+            let res = req
+                .send()
                 .await
                 .context("Failed to send get all users request")?;

Committable suggestion skipped: line range outside the PR's diff.

let legal = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
.chars()
.collect::<Vec<_>>();

let num_workers = (num_workers as usize).clamp(1, legal.len());

let chunk_size = legal.len() / num_workers;
let mut ranges = Vec::new();

for i in 0..num_workers {
let min = legal[i * chunk_size];
let max = if i < num_workers - 1 {
legal[(i + 1) * chunk_size]
} else {
(*legal.last().unwrap() as u8 + 1) as char
};

ranges.push((min, max));
}

async fn run_worker(
client: &FirebaseAuthClient,
range: (char, char),
base_url: &str,
) -> Result<Vec<User>, FirebaseError> {
const MAX_RESULTS: usize = 1000;
let (min, max) = (range.0.to_string(), range.1.to_string());

let mut all_users = Vec::new();
let mut next_page_token = Some(min);
loop {
let url = make_pagination_url(base_url, MAX_RESULTS, next_page_token.as_deref());

let res = client
.auth_get(url)
.await?
.header("Content-Type", "application/json")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Drop Content-Type on GET.

GETs don’t have a body; setting Content-Type is misleading. If anything, set Accept: application/json.

-                    .header("Content-Type", "application/json")
+                    // Optionally: .header(reqwest::header::ACCEPT, "application/json")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.header("Content-Type", "application/json")
// Optionally: .header(reqwest::header::ACCEPT, "application/json")
🤖 Prompt for AI Agents
In src/auth/mod.rs around line 420, the request builder sets "Content-Type:
application/json" on a GET which is incorrect; remove the
.header("Content-Type", "application/json") call and, if the server expects JSON
responses, add .header("Accept", "application/json") instead so the GET is not
advertising a body content type but still expresses it expects JSON.

.send()
.await
.context("Failed to send get all users request")?;

if !res.status().is_success() {
return Err(response_error("Failed to get all users", res).await);
}

let res_body: BatchGetResponse =
res.json().await.context("Failed to read response JSON")?;

if let Some(mut users) = res_body.users {
all_users.append(&mut users);
}

next_page_token = res_body.next_page_token.map(|t| t.to_string());

if next_page_token.is_none() || next_page_token.as_deref() >= Some(&max) {
break;
}
}

Ok(all_users)
}

let futures = ranges
.into_iter()
.map(|range| run_worker(self, range, &base_url));

let worker_results = futures::future::try_join_all(futures).await?;

let mut deduped = HashMap::new();
for users in worker_results {
for user in users {
deduped.insert(user.uid.clone(), user);
}
}

let all_users = deduped.into_values().collect();

Ok(all_users)
}
Comment on lines +355 to +462

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Concurrency approach is incorrect: nextPageToken is opaque (this will miss/duplicate users).

You're sharding by ASCII ranges and seeding nextPageToken with arbitrary chars. Per Google APIs, nextPageToken (or pageToken) is an opaque cursor returned by the server; you can't manufacture starting tokens. This will either 400, start all workers at the beginning, or silently drop/dup pages. Replace with sequential pagination.

Apply this diff to switch to a single sequential pager (keeps the public API, ignores num_workers):

@@
-    #[tracing::instrument(name = "Get all users concurrently", skip_all)]
-    pub async fn get_all_users_concurrently(
-        &self,
-        num_workers: u8,
-    ) -> Result<Vec<User>, FirebaseError> {
-        let base_url = self.project_url("/accounts:batchGet");
-
-        fn make_pagination_url(
-            base_url: &str,
-            max_results: usize,
-            next_page_token: Option<&str>,
-        ) -> String {
-            format!(
-                "{base_url}?maxResults={max_results}{}",
-                next_page_token
-                    .map(|token| format!("&nextPageToken={token}"))
-                    .unwrap_or_else(|| { "".to_string() })
-            )
-        }
-
-        let legal = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-            .chars()
-            .collect::<Vec<_>>();
-
-        let num_workers = (num_workers as usize).clamp(1, legal.len());
-
-        let chunk_size = legal.len() / num_workers;
-        let mut ranges = Vec::new();
-
-        for i in 0..num_workers {
-            let min = legal[i * chunk_size];
-            let max = if i < num_workers - 1 {
-                legal[(i + 1) * chunk_size]
-            } else {
-                (*legal.last().unwrap() as u8 + 1) as char
-            };
-
-            ranges.push((min, max));
-        }
-
-        async fn run_worker(
-            client: &FirebaseAuthClient,
-            range: (char, char),
-            base_url: &str,
-        ) -> Result<Vec<User>, FirebaseError> {
-            const MAX_RESULTS: usize = 1000;
-            let (min, max) = (range.0.to_string(), range.1.to_string());
-
-            let mut all_users = Vec::new();
-            let mut next_page_token = Some(min);
-            loop {
-                let url = make_pagination_url(base_url, MAX_RESULTS, next_page_token.as_deref());
-
-                let res = client
-                    .auth_get(url)
-                    .await?
-                    .header("Content-Type", "application/json")
-                    .send()
-                    .await
-                    .context("Failed to send get all users request")?;
-
-                if !res.status().is_success() {
-                    return Err(response_error("Failed to get all users", res).await);
-                }
-
-                let res_body: BatchGetResponse =
-                    res.json().await.context("Failed to read response JSON")?;
-
-                if let Some(mut users) = res_body.users {
-                    all_users.append(&mut users);
-                }
-
-                next_page_token = res_body.next_page_token.map(|t| t.to_string());
-
-                if next_page_token.is_none() || next_page_token.as_deref() >= Some(&max) {
-                    break;
-                }
-            }
-
-            Ok(all_users)
-        }
-
-        let futures = ranges
-            .into_iter()
-            .map(|range| run_worker(self, range, &base_url));
-
-        let worker_results = futures::future::try_join_all(futures).await?;
-
-        let mut deduped = HashMap::new();
-        for users in worker_results {
-            for user in users {
-                deduped.insert(user.uid.clone(), user);
-            }
-        }
-
-        let all_users = deduped.into_values().collect();
-
-        Ok(all_users)
-    }
+    #[tracing::instrument(name = "Get all users concurrently", skip_all)]
+    pub async fn get_all_users_concurrently(
+        &self,
+        _num_workers: u8,
+    ) -> Result<Vec<User>, FirebaseError> {
+        let base_url = self.project_url("/accounts:batchGet");
+        const MAX_RESULTS: usize = 1000;
+
+        let mut all_users = Vec::new();
+        let mut next_page_token: Option<String> = None;
+
+        loop {
+            let mut req = self.auth_get(&base_url).await?;
+            req = req.query(&[("maxResults", MAX_RESULTS)]);
+            if let Some(ref token) = next_page_token {
+                req = req.query(&[("nextPageToken", token)]);
+            }
+
+            let res = req
+                .send()
+                .await
+                .context("Failed to send get all users request")?;
+
+            if !res.status().is_success() {
+                return Err(response_error("Failed to get all users", res).await);
+            }
+
+            let res_body: BatchGetResponse =
+                res.json().await.context("Failed to read response JSON")?;
+
+            if let Some(mut users) = res_body.users {
+                all_users.append(&mut users);
+            }
+
+            next_page_token = res_body.next_page_token;
+            if next_page_token.is_none() {
+                break;
+            }
+        }
+
+        Ok(all_users)
+    }

If you still want concurrency, you’ll need a different strategy (e.g., a single pager that feeds a bounded channel to N workers for per-user follow-up work). The listing itself can’t be sharded by token.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Retrieve all users in Firebase Auth using `num_workers` concurrent workers. See
/// [`get_all_users`](Self::get_all_users) for more info.
///
/// The number of workers should be between 1 and 62. If you pass a number outside
/// this range, it will be clamped.
///
/// Keep in mind that, at the time of writing, the Identity Toolkit API limit is 500
/// requests/second per service account. This method will fire off multiple requests
/// simultaneously. See the up-to-date limits in the [official docs](https://firebase.google.com/docs/auth/limits).
#[tracing::instrument(name = "Get all users concurrently", skip_all)]
pub async fn get_all_users_concurrently(
&self,
num_workers: u8,
) -> Result<Vec<User>, FirebaseError> {
let base_url = self.project_url("/accounts:batchGet");
fn make_pagination_url(
base_url: &str,
max_results: usize,
next_page_token: Option<&str>,
) -> String {
format!(
"{base_url}?maxResults={max_results}{}",
next_page_token
.map(|token| format!("&nextPageToken={token}"))
.unwrap_or_else(|| { "".to_string() })
)
}
let legal = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
.chars()
.collect::<Vec<_>>();
let num_workers = (num_workers as usize).clamp(1, legal.len());
let chunk_size = legal.len() / num_workers;
let mut ranges = Vec::new();
for i in 0..num_workers {
let min = legal[i * chunk_size];
let max = if i < num_workers - 1 {
legal[(i + 1) * chunk_size]
} else {
(*legal.last().unwrap() as u8 + 1) as char
};
ranges.push((min, max));
}
async fn run_worker(
client: &FirebaseAuthClient,
range: (char, char),
base_url: &str,
) -> Result<Vec<User>, FirebaseError> {
const MAX_RESULTS: usize = 1000;
let (min, max) = (range.0.to_string(), range.1.to_string());
let mut all_users = Vec::new();
let mut next_page_token = Some(min);
loop {
let url = make_pagination_url(base_url, MAX_RESULTS, next_page_token.as_deref());
let res = client
.auth_get(url)
.await?
.header("Content-Type", "application/json")
.send()
.await
.context("Failed to send get all users request")?;
if !res.status().is_success() {
return Err(response_error("Failed to get all users", res).await);
}
let res_body: BatchGetResponse =
res.json().await.context("Failed to read response JSON")?;
if let Some(mut users) = res_body.users {
all_users.append(&mut users);
}
next_page_token = res_body.next_page_token.map(|t| t.to_string());
if next_page_token.is_none() || next_page_token.as_deref() >= Some(&max) {
break;
}
}
Ok(all_users)
}
let futures = ranges
.into_iter()
.map(|range| run_worker(self, range, &base_url));
let worker_results = futures::future::try_join_all(futures).await?;
let mut deduped = HashMap::new();
for users in worker_results {
for user in users {
deduped.insert(user.uid.clone(), user);
}
}
let all_users = deduped.into_values().collect();
Ok(all_users)
}
#[tracing::instrument(name = "Get all users concurrently", skip_all)]
pub async fn get_all_users_concurrently(
&self,
_num_workers: u8,
) -> Result<Vec<User>, FirebaseError> {
let base_url = self.project_url("/accounts:batchGet");
const MAX_RESULTS: usize = 1000;
let mut all_users = Vec::new();
let mut next_page_token: Option<String> = None;
loop {
let mut req = self.auth_get(&base_url).await?;
req = req.query(&[("maxResults", MAX_RESULTS)]);
if let Some(ref token) = next_page_token {
req = req.query(&[("nextPageToken", token)]);
}
let res = req
.send()
.await
.context("Failed to send get all users request")?;
if !res.status().is_success() {
return Err(response_error("Failed to get all users", res).await);
}
let res_body: BatchGetResponse =
res.json().await.context("Failed to read response JSON")?;
if let Some(mut users) = res_body.users {
all_users.append(&mut users);
}
next_page_token = res_body.next_page_token;
if next_page_token.is_none() {
break;
}
}
Ok(all_users)
}
🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 355 to 462, the current implementation shards by
ASCII ranges and seeds nextPageToken with fabricated values which is invalid
because nextPageToken is an opaque server-issued cursor and this will
miss/duplicate pages or error; replace the whole concurrent sharding logic with
a single sequential pager that ignores num_workers: call the same
/accounts:batchGet endpoint in a loop, starting with no page token, build each
page URL with the returned next_page_token until it is None, accumulate users
into a HashMap to dedupe by uid, and return the collected values; remove the
legal/ranges/chunking and run_worker logic and update the docstring to note
num_workers is ignored (or clamp it but not used), or alternatively implement a
producer-consumer where one pager feeds a bounded channel to workers if you need
concurrency for per-user processing.


/// Creates a new user in Firebase Auth using the email/password provider.
///
/// # Examples
Expand Down
7 changes: 7 additions & 0 deletions src/auth/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ pub(crate) struct GetAccountInfoResponse {
pub users: Option<Vec<User>>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct BatchGetResponse {
pub users: Option<Vec<User>>,
pub next_page_token: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
Expand Down
Loading