Skip to main content

rs_firebase_admin_sdk/auth/
mod.rs

1//! Firebase Identity Provider management interface
2
3#[cfg(test)]
4mod test;
5
6pub mod claims;
7pub mod import;
8pub mod oob_code;
9
10use crate::api_uri::{ApiUriBuilder, FirebaseAuthEmulatorRestApi, FirebaseAuthRestApi};
11use crate::client::ApiHttpClient;
12use crate::client::error::ApiClientError;
13use crate::util::{I128EpochMs, StrEpochMs, StrEpochSec};
14pub use claims::Claims;
15use core::pin::Pin;
16use error_stack::Report;
17use http::Method;
18pub use import::{UserImportRecord, UserImportRecords};
19use oob_code::{OobCodeAction, OobCodeActionLink, OobCodeActionType};
20use serde::{Deserialize, Serialize};
21use std::collections::BTreeMap;
22use std::future::Future;
23use std::vec;
24use time::{Duration, OffsetDateTime};
25
26const FIREBASE_AUTH_REST_AUTHORITY: &str = "identitytoolkit.googleapis.com";
27
28#[derive(Serialize, Debug, Clone, Default)]
29#[serde(rename_all = "camelCase")]
30pub struct NewUser {
31    #[serde(skip_serializing_if = "Option::is_none")]
32    #[serde(rename = "localId")]
33    pub uid: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub email: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub password: Option<String>,
38}
39
40impl NewUser {
41    pub fn email_and_password(email: String, password: String) -> Self {
42        Self {
43            uid: None,
44            email: Some(email),
45            password: Some(password),
46        }
47    }
48}
49
50#[derive(Deserialize, Debug, Clone)]
51#[serde(rename_all = "camelCase")]
52pub struct ProviderUserInfo {
53    pub provider_id: String,
54    pub email: Option<String>,
55    pub phone_number: Option<String>,
56    pub federated_id: Option<String>,
57    pub raw_id: String,
58}
59
60#[derive(Deserialize, Debug, Clone)]
61#[serde(rename_all = "camelCase")]
62pub struct User {
63    #[serde(rename = "localId")]
64    pub uid: String,
65    pub email: Option<String>,
66    pub display_name: Option<String>,
67    pub photo_url: Option<String>,
68    pub phone_number: Option<String>,
69    pub last_login_at: Option<StrEpochMs>,
70    pub email_verified: Option<bool>,
71    pub password_updated_at: Option<I128EpochMs>,
72    pub valid_since: Option<StrEpochSec>,
73    pub created_at: Option<StrEpochMs>,
74    pub salt: Option<String>,
75    pub password_hash: Option<String>,
76    pub provider_user_info: Option<Vec<ProviderUserInfo>>,
77    #[serde(rename = "customAttributes")]
78    pub custom_claims: Option<Claims>,
79    pub disabled: Option<bool>,
80}
81
82#[derive(Deserialize, Debug, Clone)]
83#[serde(rename_all = "camelCase")]
84pub struct Users {
85    pub users: Option<Vec<User>>,
86}
87
88#[derive(Deserialize, Debug, Clone)]
89#[serde(rename_all = "camelCase")]
90pub struct UserList {
91    #[serde(default)]
92    pub users: Vec<User>,
93    pub next_page_token: Option<String>,
94}
95
96#[derive(Serialize, Debug, Clone)]
97#[serde(rename_all = "camelCase")]
98pub struct CreateSessionCookie {
99    pub id_token: String,
100    pub valid_duration: i64,
101}
102
103#[derive(Deserialize, Debug, Clone)]
104#[serde(rename_all = "camelCase")]
105pub struct SessionCookie {
106    pub session_cookie: String,
107}
108
109#[derive(Serialize, Debug, Clone)]
110#[serde(rename_all = "camelCase")]
111pub struct FederatedUserId {
112    pub provider_id: String,
113    pub raw_id: String,
114}
115
116#[derive(Serialize, Debug, Clone, Default)]
117#[serde(rename_all = "camelCase")]
118pub struct UserIdentifiers {
119    #[serde(skip_serializing_if = "Option::is_none")]
120    #[serde(rename = "localId")]
121    pub uid: Option<Vec<String>>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub email: Option<Vec<String>>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub phone_number: Option<Vec<String>>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub federated_user_id: Option<FederatedUserId>,
128}
129
130impl UserIdentifiers {
131    pub fn builder() -> UserIdentifiersBuilder {
132        UserIdentifiersBuilder::default()
133    }
134}
135
136#[derive(Clone, Default)]
137pub struct UserIdentifiersBuilder {
138    ids: UserIdentifiers,
139}
140
141impl UserIdentifiersBuilder {
142    pub fn with_email(mut self, email: String) -> Self {
143        match &mut self.ids.email {
144            Some(email_vec) => email_vec.push(email),
145            None => self.ids.email = Some(vec![email]),
146        };
147
148        self
149    }
150
151    pub fn with_uid(mut self, uid: String) -> Self {
152        match &mut self.ids.uid {
153            Some(uid_vec) => uid_vec.push(uid),
154            None => self.ids.uid = Some(vec![uid]),
155        };
156
157        self
158    }
159
160    pub fn with_phone_number(mut self, pnumber: String) -> Self {
161        match &mut self.ids.phone_number {
162            Some(pnumber_vec) => pnumber_vec.push(pnumber),
163            None => self.ids.phone_number = Some(vec![pnumber]),
164        };
165
166        self
167    }
168
169    pub fn build(self) -> UserIdentifiers {
170        self.ids
171    }
172}
173
174#[derive(Serialize, Debug, Clone)]
175pub enum DeleteAttribute {
176    #[serde(rename = "DISPLAY_NAME")]
177    DisplayName,
178    #[serde(rename = "PHOTO_URL")]
179    PhotoUrl,
180}
181
182#[derive(Serialize, Debug, Clone)]
183#[serde(rename_all = "camelCase")]
184pub enum DeleteProvider {
185    Phone,
186}
187
188#[derive(Serialize, Debug, Clone, Default)]
189#[serde(rename_all = "camelCase")]
190pub struct UserUpdate {
191    #[serde(rename = "localId")]
192    pub uid: String,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub email: Option<String>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub password: Option<String>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub valid_since: Option<OffsetDateTime>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub email_verified: Option<bool>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub disable_user: Option<bool>,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub display_name: Option<String>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub photo_url: Option<String>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub phone_number: Option<String>,
209    #[serde(rename = "customAttributes")]
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub custom_claims: Option<Claims>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub delete_attribute: Option<Vec<DeleteAttribute>>,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub delete_provider: Option<Vec<DeleteProvider>>,
216}
217
218impl UserUpdate {
219    pub fn builder(uid: String) -> UserUpdateBuilder {
220        UserUpdateBuilder::new(uid)
221    }
222}
223
224pub struct UserUpdateBuilder {
225    update: UserUpdate,
226}
227
228pub enum AttributeOp<T> {
229    Change(T),
230    Delete,
231}
232
233impl UserUpdateBuilder {
234    pub fn new(uid: String) -> Self {
235        Self {
236            update: UserUpdate {
237                uid,
238                ..Default::default()
239            },
240        }
241    }
242
243    pub fn display_name(mut self, value: AttributeOp<String>) -> Self {
244        match value {
245            AttributeOp::Change(new_display_name) => {
246                self.update.display_name = Some(new_display_name)
247            }
248            AttributeOp::Delete => self
249                .update
250                .delete_attribute
251                .get_or_insert(Vec::new())
252                .push(DeleteAttribute::DisplayName),
253        };
254
255        self
256    }
257
258    pub fn photo_url(mut self, value: AttributeOp<String>) -> Self {
259        match value {
260            AttributeOp::Change(new_photo_url) => self.update.photo_url = Some(new_photo_url),
261            AttributeOp::Delete => self
262                .update
263                .delete_attribute
264                .get_or_insert(Vec::new())
265                .push(DeleteAttribute::PhotoUrl),
266        };
267
268        self
269    }
270
271    pub fn phone_number(mut self, value: AttributeOp<String>) -> Self {
272        match value {
273            AttributeOp::Change(new_phone_number) => {
274                self.update.phone_number = Some(new_phone_number)
275            }
276            AttributeOp::Delete => self
277                .update
278                .delete_provider
279                .get_or_insert(Vec::new())
280                .push(DeleteProvider::Phone),
281        };
282
283        self
284    }
285
286    pub fn custom_claims(mut self, value: Claims) -> Self {
287        self.update.custom_claims = Some(value);
288
289        self
290    }
291
292    pub fn email(mut self, value: String) -> Self {
293        self.update.email = Some(value);
294
295        self
296    }
297
298    pub fn password(mut self, value: String) -> Self {
299        self.update.password = Some(value);
300
301        self
302    }
303
304    pub fn email_verified(mut self, value: bool) -> Self {
305        self.update.email_verified = Some(value);
306
307        self
308    }
309
310    pub fn disabled(mut self, is_disabled: bool) -> Self {
311        self.update.disable_user = Some(is_disabled);
312
313        self
314    }
315
316    pub fn build(self) -> UserUpdate {
317        self.update
318    }
319}
320
321#[derive(Serialize, Debug, Clone, Default)]
322#[serde(rename_all = "camelCase")]
323struct UserId {
324    #[serde(rename = "localId")]
325    pub uid: String,
326}
327
328#[derive(Serialize, Debug, Clone, Default)]
329#[serde(rename_all = "camelCase")]
330struct UserIds {
331    #[serde(rename = "localIds")]
332    pub uids: Vec<String>,
333    pub force: bool,
334}
335
336pub trait FirebaseAuthService<C: ApiHttpClient>: Send + Sync + 'static {
337    fn get_client(&self) -> &C;
338    fn get_auth_uri_builder(&self) -> &ApiUriBuilder;
339
340    /// Creates a new user account with the specified properties.
341    /// # Example
342    /// ```rust
343    /// let new_user = auth.create_user(
344    ///     NewUser::email_and_password(
345    ///        "test@example.com".into(),
346    ///        "123ABC".into(),
347    ///     )
348    /// ).await.unwrap();
349    /// ```
350    fn create_user(
351        &self,
352        user: NewUser,
353    ) -> Pin<Box<impl Future<Output = Result<User, Report<ApiClientError>>> + Send>> {
354        let client = self.get_client();
355        let uri = self
356            .get_auth_uri_builder()
357            .build(FirebaseAuthRestApi::CreateUser);
358
359        Box::pin(client.send_request_body(uri, Method::POST, user))
360    }
361
362    /// Get first user that matches given identifier filter
363    /// # Example
364    /// ```rust
365    /// let user = auth.get_user(
366    ///     UserIdentifiers {
367    ///         email: Some(vec!["me@example.com".into()]),
368    ///         ..Default::default()
369    ///     }
370    /// ).await.unwrap();
371    /// ```
372    fn get_user(
373        &self,
374        indentifiers: UserIdentifiers,
375    ) -> Pin<Box<impl Future<Output = Result<Option<User>, Report<ApiClientError>>> + Send>> {
376        Box::pin(async move {
377            if let Some(users) = self.get_users(indentifiers).await? {
378                return Ok(users.into_iter().next());
379            }
380
381            Ok(None)
382        })
383    }
384
385    /// Get all users that match a given identifier filter
386    /// # Example
387    /// ```rust
388    /// let users = auth.get_users(
389    ///     UserIdentifiers {
390    ///         email: Some(vec!["me@example.com".into()]),
391    ///         uid: Some(vec!["A123456".into()]),
392    ///         ..Default::default()
393    ///     }
394    /// ).await.unwrap().unwrap();
395    /// ```
396    fn get_users(
397        &self,
398        indentifiers: UserIdentifiers,
399    ) -> Pin<Box<impl Future<Output = Result<Option<Vec<User>>, Report<ApiClientError>>> + Send>>
400    {
401        Box::pin(async move {
402            let client = self.get_client();
403            let uri_builder = self.get_auth_uri_builder();
404
405            let users: Users = client
406                .send_request_body(
407                    uri_builder.build(FirebaseAuthRestApi::GetUsers),
408                    Method::POST,
409                    indentifiers,
410                )
411                .await?;
412
413            Ok(users.users)
414        })
415    }
416
417    /// Fetch all users in batches of `users_per_page`, to progress pass previous page into the method's `prev`.
418    /// # Example
419    /// ```rust
420    /// let mut user_page: Option<UserList> = None;
421    /// loop {
422    ///     user_page = auth.list_users(10, user_page).await.unwrap();
423    ///
424    ///     if let Some(user_page) = &user_page {
425    ///         for user in &user_page.users {
426    ///             println!("User: {user:?}");
427    ///         }
428    ///     } else {
429    ///         break;
430    ///     }
431    /// }
432    /// ```
433    fn list_users(
434        &self,
435        users_per_page: usize,
436        prev: Option<UserList>,
437    ) -> Pin<Box<impl Future<Output = Result<Option<UserList>, Report<ApiClientError>>> + Send>>
438    {
439        Box::pin(async move {
440            let client = self.get_client();
441            let uri_builder = self.get_auth_uri_builder();
442            let mut params = vec![("maxResults".to_string(), users_per_page.clone().to_string())];
443
444            if let Some(prev) = prev {
445                if let Some(next_page_token) = prev.next_page_token {
446                    params.push(("nextPageToken".to_string(), next_page_token));
447                } else {
448                    return Ok(None);
449                }
450            }
451
452            let users: UserList = client
453                .send_request_with_params(
454                    uri_builder.build(FirebaseAuthRestApi::ListUsers),
455                    params.into_iter(),
456                    Method::GET,
457                )
458                .await?;
459
460            Ok(Some(users))
461        })
462    }
463
464    /// Delete user with given ID
465    fn delete_user(
466        &self,
467        uid: String,
468    ) -> Pin<Box<impl Future<Output = Result<(), Report<ApiClientError>>> + Send>> {
469        Box::pin(async move {
470            let client = self.get_client();
471            let uri_builder = self.get_auth_uri_builder();
472
473            client
474                .send_request_body_empty_response(
475                    uri_builder.build(FirebaseAuthRestApi::DeleteUser),
476                    Method::POST,
477                    UserId { uid },
478                )
479                .await
480        })
481    }
482
483    /// Delete all users with given list of IDs
484    fn delete_users(
485        &self,
486        uids: Vec<String>,
487        force: bool,
488    ) -> Pin<Box<impl Future<Output = Result<(), Report<ApiClientError>>> + Send>> {
489        Box::pin(async move {
490            let client = self.get_client();
491            let uri_builder = self.get_auth_uri_builder();
492
493            client
494                .send_request_body_empty_response(
495                    uri_builder.build(FirebaseAuthRestApi::DeleteUsers),
496                    Method::POST,
497                    UserIds { uids, force },
498                )
499                .await
500        })
501    }
502
503    /// Update user with given changes
504    /// # Example
505    /// ```rust
506    /// let update = UserUpdate::builder("ID123".into())
507    ///     .display_name(AttributeOp::Change("My new name".into()))
508    ///     .phone_number(AttributeOp::Delete)
509    ///     .email("new@example.com".into())
510    ///     .build();
511    /// auth.update_user(update).await.unwrap();
512    /// ```
513    fn update_user(
514        &self,
515        update: UserUpdate,
516    ) -> Pin<Box<impl Future<Output = Result<User, Report<ApiClientError>>> + Send>> {
517        Box::pin(async move {
518            let client = self.get_client();
519            let uri_builder = self.get_auth_uri_builder();
520
521            client
522                .send_request_body(
523                    uri_builder.build(FirebaseAuthRestApi::UpdateUser),
524                    Method::POST,
525                    update,
526                )
527                .await
528        })
529    }
530
531    /// Create users in bulk
532    /// # Example
533    /// ```rust
534    /// let records = vec![
535    ///     UserImportRecord::builder()
536    ///         .with_email("me@example.com".into(), true)
537    ///         .with_display_name("My Name".into())
538    ///         .build()
539    /// ];
540    /// auth.import_users(records).await.unwrap();
541    /// ```
542    fn import_users(
543        &self,
544        users: Vec<UserImportRecord>,
545    ) -> Pin<Box<impl Future<Output = Result<(), Report<ApiClientError>>> + Send>> {
546        Box::pin(async move {
547            let client = self.get_client();
548            let uri_builder = self.get_auth_uri_builder();
549
550            client
551                .send_request_body_empty_response(
552                    uri_builder.build(FirebaseAuthRestApi::ImportUsers),
553                    Method::POST,
554                    UserImportRecords { users },
555                )
556                .await?;
557
558            Ok(())
559        })
560    }
561
562    /// Send email with OOB code action
563    /// # Example
564    /// ```rust
565    /// let oob_action = OobCodeAction::builder(
566    ///     OobCodeActionType::PasswordReset,
567    ///     "me@example.com".into()
568    /// ).build();
569    ///
570    /// let link = auth.generate_email_action_link(oob_action).await.unwrap();
571    /// ```
572    fn generate_email_action_link(
573        &self,
574        oob_action: OobCodeAction,
575    ) -> Pin<Box<impl Future<Output = Result<String, Report<ApiClientError>>> + Send>> {
576        Box::pin(async move {
577            let client = self.get_client();
578            let uri_builder = self.get_auth_uri_builder();
579
580            let oob_link: OobCodeActionLink = client
581                .send_request_body(
582                    uri_builder.build(FirebaseAuthRestApi::SendOobCode),
583                    Method::POST,
584                    oob_action,
585                )
586                .await?;
587
588            Ok(oob_link.oob_link)
589        })
590    }
591
592    /// Create session cookie
593    /// that then can be verified and parsed with `App::live().cookie_token_verifier()`
594    fn create_session_cookie(
595        &self,
596        id_token: String,
597        expires_in: Duration,
598    ) -> Pin<Box<impl Future<Output = Result<String, Report<ApiClientError>>> + Send>> {
599        Box::pin(async move {
600            let client = self.get_client();
601            let uri_builder = self.get_auth_uri_builder();
602
603            let create_cookie = CreateSessionCookie {
604                id_token,
605                valid_duration: expires_in.whole_seconds(),
606            };
607
608            let session_cookie: SessionCookie = client
609                .send_request_body(
610                    uri_builder.build(FirebaseAuthRestApi::CreateSessionCookie),
611                    Method::POST,
612                    create_cookie,
613                )
614                .await?;
615
616            Ok(session_cookie.session_cookie)
617        })
618    }
619}
620
621#[derive(Serialize, Deserialize, Debug, Clone)]
622#[serde(rename_all = "camelCase")]
623pub struct EmulatorConfigurationSignIn {
624    allow_duplicate_emails: bool,
625}
626
627#[derive(Serialize, Deserialize, Debug, Clone)]
628#[serde(rename_all = "camelCase")]
629pub struct EmulatorConfiguration {
630    sign_in: EmulatorConfigurationSignIn,
631}
632
633#[derive(Deserialize, Debug, Clone)]
634#[serde(rename_all = "camelCase")]
635pub struct OobCode {
636    pub email: String,
637    pub oob_code: String,
638    pub oob_link: String,
639    pub request_type: OobCodeActionType,
640}
641
642#[derive(Deserialize, Debug, Clone)]
643#[serde(rename_all = "camelCase")]
644pub struct OobCodes {
645    pub oob_codes: Vec<OobCode>,
646}
647
648#[derive(Deserialize, Debug, Clone)]
649#[serde(rename_all = "camelCase")]
650pub struct SmsVerificationCode {
651    pub phone_number: String,
652    pub session_code: String,
653}
654
655#[derive(Deserialize, Debug, Clone)]
656#[serde(rename_all = "camelCase")]
657pub struct SmsVerificationCodes {
658    pub verification_codes: Vec<SmsVerificationCode>,
659}
660
661pub trait FirebaseEmulatorAuthService<ApiHttpClientT>
662where
663    Self: Send + Sync,
664    ApiHttpClientT: ApiHttpClient + Send + Sync,
665{
666    fn get_emulator_client(&self) -> &ApiHttpClientT;
667    fn get_emulator_auth_uri_builder(&self) -> &ApiUriBuilder;
668
669    /// Delete all users within emulator
670    fn clear_all_users(
671        &self,
672    ) -> Pin<Box<impl Future<Output = Result<(), Report<ApiClientError>>> + Send>> {
673        Box::pin(async move {
674            let client = self.get_emulator_client();
675            let uri_builder = self.get_emulator_auth_uri_builder();
676
677            let _result: BTreeMap<String, String> = client
678                .send_request(
679                    uri_builder.build(FirebaseAuthEmulatorRestApi::ClearUserAccounts),
680                    Method::DELETE,
681                )
682                .await?;
683
684            Ok(())
685        })
686    }
687
688    /// Get current emulator configuration
689    fn get_emulator_configuration(
690        &self,
691    ) -> Pin<Box<impl Future<Output = Result<EmulatorConfiguration, Report<ApiClientError>>> + Send>>
692    {
693        Box::pin(async move {
694            let client = self.get_emulator_client();
695            let uri_builder = self.get_emulator_auth_uri_builder();
696
697            client
698                .send_request(
699                    uri_builder.build(FirebaseAuthEmulatorRestApi::Configuration),
700                    Method::GET,
701                )
702                .await
703        })
704    }
705
706    /// Update emulator configuration
707    fn patch_emulator_configuration(
708        &self,
709        configuration: EmulatorConfiguration,
710    ) -> Pin<Box<impl Future<Output = Result<EmulatorConfiguration, Report<ApiClientError>>> + Send>>
711    {
712        Box::pin(async move {
713            let client = self.get_emulator_client();
714            let uri_builder = self.get_emulator_auth_uri_builder();
715
716            client
717                .send_request_body(
718                    uri_builder.build(FirebaseAuthEmulatorRestApi::Configuration),
719                    Method::PATCH,
720                    configuration,
721                )
722                .await
723        })
724    }
725
726    /// Fetch all OOB codes within emulator
727    fn get_oob_codes(
728        &self,
729    ) -> Pin<Box<impl Future<Output = Result<Vec<OobCode>, Report<ApiClientError>>> + Send>> {
730        Box::pin(async move {
731            let client = self.get_emulator_client();
732            let uri_builder = self.get_emulator_auth_uri_builder();
733
734            let oob_codes: OobCodes = client
735                .send_request(
736                    uri_builder.build(FirebaseAuthEmulatorRestApi::OobCodes),
737                    Method::GET,
738                )
739                .await?;
740
741            Ok(oob_codes.oob_codes)
742        })
743    }
744
745    /// Fetch all SMS codes within emulator
746    fn get_sms_verification_codes(
747        &self,
748    ) -> Pin<Box<impl Future<Output = Result<SmsVerificationCodes, Report<ApiClientError>>> + Send>>
749    {
750        Box::pin(async move {
751            let client = self.get_emulator_client();
752            let uri_builder = self.get_emulator_auth_uri_builder();
753
754            client
755                .send_request(
756                    uri_builder.build(FirebaseAuthEmulatorRestApi::SmsVerificationCodes),
757                    Method::GET,
758                )
759                .await
760        })
761    }
762}
763
764pub struct FirebaseAuth<ApiHttpClientT> {
765    client: ApiHttpClientT,
766    auth_uri_builder: ApiUriBuilder,
767    emulator_auth_uri_builder: Option<ApiUriBuilder>,
768}
769
770impl<ApiHttpClientT> FirebaseAuth<ApiHttpClientT>
771where
772    ApiHttpClientT: ApiHttpClient + Send + Sync,
773{
774    /// Create Firebase Authentication manager for emulator
775    pub fn emulated(emulator_url: String, project_id: &str, client: ApiHttpClientT) -> Self {
776        let fb_auth_root = emulator_url.clone()
777            + &format!("/{FIREBASE_AUTH_REST_AUTHORITY}/v1/projects/{project_id}");
778        let fb_emu_root = emulator_url + &format!("/emulator/v1/projects/{project_id}");
779
780        Self {
781            client,
782            auth_uri_builder: ApiUriBuilder::new(fb_auth_root),
783            emulator_auth_uri_builder: Some(ApiUriBuilder::new(fb_emu_root)),
784        }
785    }
786
787    /// Create Firebase Authentication manager for live project
788    pub fn live(project_id: &str, client: ApiHttpClientT) -> Self {
789        let fb_auth_root = "https://".to_string()
790            + FIREBASE_AUTH_REST_AUTHORITY
791            + &format!("/v1/projects/{project_id}");
792
793        Self {
794            client,
795            auth_uri_builder: ApiUriBuilder::new(fb_auth_root),
796            emulator_auth_uri_builder: None,
797        }
798    }
799}
800
801impl<ApiHttpClientT> FirebaseAuthService<ApiHttpClientT> for FirebaseAuth<ApiHttpClientT>
802where
803    ApiHttpClientT: ApiHttpClient + Send + Sync,
804{
805    fn get_client(&self) -> &ApiHttpClientT {
806        &self.client
807    }
808
809    fn get_auth_uri_builder(&self) -> &ApiUriBuilder {
810        &self.auth_uri_builder
811    }
812}
813
814impl<ApiHttpClientT> FirebaseEmulatorAuthService<ApiHttpClientT> for FirebaseAuth<ApiHttpClientT>
815where
816    ApiHttpClientT: ApiHttpClient + Send + Sync,
817{
818    fn get_emulator_client(&self) -> &ApiHttpClientT {
819        &self.client
820    }
821
822    fn get_emulator_auth_uri_builder(&self) -> &ApiUriBuilder {
823        self.emulator_auth_uri_builder
824            .as_ref()
825            .expect("Auth emulator URI builder is unset")
826    }
827}