firebase_admin/auth/users/
operations.rs1use crate::auth::error::{parse_identity_toolkit_response, AuthError};
4use crate::auth::identity_toolkit::requests::{
5 AccountsResponse, DeleteRequest, LookupRequest, SignUpRequest, UpdateRequest,
6};
7use crate::auth::identity_toolkit::IdentityToolkitEndpoints;
8use crate::auth::users::model::{CreateUserRequest, UpdateUserRequest, UserRecord};
9use crate::auth::users::query::UserPage;
10use crate::core::{CoreError, HttpClient};
11
12pub struct UserOperations<'a> {
20 http: &'a HttpClient,
21 endpoints: &'a IdentityToolkitEndpoints,
22 bearer_token: Option<&'a str>,
23 emulator_api_key: Option<&'static str>,
24}
25
26impl<'a> UserOperations<'a> {
27 pub fn new(
31 http: &'a HttpClient,
32 endpoints: &'a IdentityToolkitEndpoints,
33 bearer_token: Option<&'a str>,
34 emulator_api_key: Option<&'static str>,
35 ) -> Self {
36 Self {
37 http,
38 endpoints,
39 bearer_token,
40 emulator_api_key,
41 }
42 }
43
44 fn request(&self, url: &str) -> reqwest::RequestBuilder {
45 let mut builder = self.http.inner().post(url);
46 if let Some(token) = self.bearer_token {
47 builder = builder.bearer_auth(token);
48 }
49 if let Some(key) = self.emulator_api_key {
50 builder = builder.query(&[("key", key)]);
51 }
52 builder
53 }
54
55 pub async fn get_user(&self, uid: &str) -> Result<UserRecord, AuthError> {
57 let request = LookupRequest {
58 local_id: vec![uid.to_string()],
59 email: vec![],
60 };
61 let response = self
62 .request(&self.endpoints.lookup())
63 .json(&request)
64 .send()
65 .await?;
66 let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
67 parsed
68 .users
69 .into_iter()
70 .next()
71 .map(UserRecord::from)
72 .ok_or(AuthError::UserNotFound)
73 }
74
75 pub async fn get_user_by_email(&self, email: &str) -> Result<UserRecord, AuthError> {
77 let request = LookupRequest {
78 local_id: vec![],
79 email: vec![email.to_string()],
80 };
81 let response = self
82 .request(&self.endpoints.lookup())
83 .json(&request)
84 .send()
85 .await?;
86 let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
87 parsed
88 .users
89 .into_iter()
90 .next()
91 .map(UserRecord::from)
92 .ok_or(AuthError::UserNotFound)
93 }
94
95 pub async fn create_user(&self, request: CreateUserRequest) -> Result<UserRecord, AuthError> {
97 let body = SignUpRequest {
98 local_id: request.uid,
99 email: request.email,
100 password: request.password,
101 display_name: request.display_name,
102 disabled: request.disabled,
103 };
104 let response = self
105 .request(&self.endpoints.sign_up())
106 .json(&body)
107 .send()
108 .await?;
109 let local_id: serde_json::Value = parse_identity_toolkit_response(response).await?;
110 let uid = local_id
111 .get("localId")
112 .and_then(|v| v.as_str())
113 .ok_or_else(|| AuthError::Api {
114 status: 200,
115 message: "signUp response missing localId".to_string(),
116 error_code: None,
117 })?;
118 self.get_user(uid).await
119 }
120
121 pub async fn update_user(
123 &self,
124 uid: &str,
125 request: UpdateUserRequest,
126 ) -> Result<UserRecord, AuthError> {
127 let custom_attributes = request
128 .custom_claims
129 .map(|claims| serde_json::to_string(&claims))
130 .transpose()
131 .map_err(CoreError::Deserialize)?;
132
133 let body = UpdateRequest {
134 local_id: uid.to_string(),
135 email: request.email,
136 display_name: request.display_name,
137 disable_user: request.disabled,
138 custom_attributes,
139 };
140 let response = self
141 .request(&self.endpoints.update())
142 .json(&body)
143 .send()
144 .await?;
145 let _: serde_json::Value = parse_identity_toolkit_response(response).await?;
146 self.get_user(uid).await
147 }
148
149 pub async fn set_custom_user_claims(
151 &self,
152 uid: &str,
153 claims: serde_json::Map<String, serde_json::Value>,
154 ) -> Result<(), AuthError> {
155 self.update_user(
156 uid,
157 UpdateUserRequest {
158 custom_claims: Some(claims),
159 ..Default::default()
160 },
161 )
162 .await?;
163 Ok(())
164 }
165
166 pub async fn delete_user(&self, uid: &str) -> Result<(), AuthError> {
168 let body = DeleteRequest {
169 local_id: uid.to_string(),
170 };
171 let response = self
172 .request(&self.endpoints.delete())
173 .json(&body)
174 .send()
175 .await?;
176 let _: serde_json::Value = parse_identity_toolkit_response(response).await?;
177 Ok(())
178 }
179
180 pub async fn list_users(
182 &self,
183 max_results: u32,
184 page_token: Option<&str>,
185 ) -> Result<UserPage, AuthError> {
186 #[derive(serde::Serialize)]
187 struct BatchGetQuery<'a> {
188 #[serde(rename = "maxResults")]
189 max_results: u32,
190 #[serde(rename = "nextPageToken", skip_serializing_if = "Option::is_none")]
191 next_page_token: Option<&'a str>,
192 }
193
194 let response = self
195 .request(&self.endpoints.batch_get())
196 .json(&BatchGetQuery {
197 max_results,
198 next_page_token: page_token,
199 })
200 .send()
201 .await?;
202 let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
203
204 Ok(UserPage {
205 users: parsed.users.into_iter().map(UserRecord::from).collect(),
206 next_page_token: parsed.next_page_token,
207 })
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use serde_json::json;
215 use wiremock::matchers::{method, path};
216 use wiremock::{Mock, MockServer, ResponseTemplate};
217
218 async fn operations_against(server: &MockServer) -> (HttpClient, IdentityToolkitEndpoints) {
219 (
220 HttpClient::default(),
221 IdentityToolkitEndpoints::custom(server.uri()),
222 )
223 }
224
225 #[tokio::test]
226 async fn get_user_returns_the_matching_record() {
227 let server = MockServer::start().await;
228 Mock::given(method("POST"))
229 .and(path("/accounts:lookup"))
230 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
231 "users": [{
232 "localId": "uid-1",
233 "email": "user@example.com",
234 "emailVerified": true,
235 }]
236 })))
237 .mount(&server)
238 .await;
239
240 let (http, endpoints) = operations_against(&server).await;
241 let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
242
243 let user = ops.get_user("uid-1").await.unwrap();
244 assert_eq!(user.uid, "uid-1");
245 assert_eq!(user.email.as_deref(), Some("user@example.com"));
246 assert!(user.email_verified);
247 }
248
249 #[tokio::test]
250 async fn get_user_with_no_matches_is_user_not_found() {
251 let server = MockServer::start().await;
252 Mock::given(method("POST"))
253 .and(path("/accounts:lookup"))
254 .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "users": [] })))
255 .mount(&server)
256 .await;
257
258 let (http, endpoints) = operations_against(&server).await;
259 let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
260
261 let err = ops.get_user("missing-uid").await.unwrap_err();
262 assert!(matches!(err, AuthError::UserNotFound));
263 }
264
265 #[tokio::test]
266 async fn create_user_looks_up_the_new_user_after_sign_up() {
267 let server = MockServer::start().await;
268 Mock::given(method("POST"))
269 .and(path("/accounts:signUp"))
270 .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "localId": "new-uid" })))
271 .mount(&server)
272 .await;
273 Mock::given(method("POST"))
274 .and(path("/accounts:lookup"))
275 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
276 "users": [{ "localId": "new-uid", "email": "new@example.com" }]
277 })))
278 .mount(&server)
279 .await;
280
281 let (http, endpoints) = operations_against(&server).await;
282 let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
283
284 let user = ops
285 .create_user(CreateUserRequest {
286 email: Some("new@example.com".to_string()),
287 password: Some("correct horse battery staple".to_string()),
288 ..Default::default()
289 })
290 .await
291 .unwrap();
292 assert_eq!(user.uid, "new-uid");
293 }
294
295 #[tokio::test]
296 async fn delete_user_succeeds_on_a_200_response() {
297 let server = MockServer::start().await;
298 Mock::given(method("POST"))
299 .and(path("/accounts:delete"))
300 .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
301 .mount(&server)
302 .await;
303
304 let (http, endpoints) = operations_against(&server).await;
305 let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
306
307 ops.delete_user("uid-1").await.unwrap();
308 }
309
310 #[tokio::test]
311 async fn list_users_returns_records_and_next_page_token() {
312 let server = MockServer::start().await;
313 Mock::given(method("POST"))
314 .and(path("/accounts:batchGet"))
315 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
316 "users": [
317 { "localId": "uid-1" },
318 { "localId": "uid-2" },
319 ],
320 "nextPageToken": "page-2",
321 })))
322 .mount(&server)
323 .await;
324
325 let (http, endpoints) = operations_against(&server).await;
326 let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
327
328 let page = ops.list_users(10, None).await.unwrap();
329 assert_eq!(page.users.len(), 2);
330 assert_eq!(page.next_page_token.as_deref(), Some("page-2"));
331 }
332
333 #[tokio::test]
334 async fn a_structured_api_error_populates_error_code() {
335 let server = MockServer::start().await;
336 Mock::given(method("POST"))
337 .and(path("/accounts:signUp"))
338 .respond_with(ResponseTemplate::new(400).set_body_json(json!({
339 "error": {
340 "code": 400,
341 "message": "EMAIL_EXISTS",
342 "errors": [{ "message": "EMAIL_EXISTS", "reason": "EMAIL_EXISTS" }]
343 }
344 })))
345 .mount(&server)
346 .await;
347
348 let (http, endpoints) = operations_against(&server).await;
349 let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
350
351 let err = ops
352 .create_user(CreateUserRequest {
353 email: Some("taken@example.com".to_string()),
354 password: Some("correct horse battery staple".to_string()),
355 ..Default::default()
356 })
357 .await
358 .unwrap_err();
359
360 match err {
361 AuthError::Api {
362 status, error_code, ..
363 } => {
364 assert_eq!(status, 400);
365 assert_eq!(error_code.as_deref(), Some("EMAIL_EXISTS"));
366 }
367 other => panic!("expected AuthError::Api, got {other:?}"),
368 }
369 }
370}