@@ -34,6 +34,12 @@ const (
34
34
maxLenPayloadCC = 1000
35
35
defaultProviderID = "firebase"
36
36
idToolkitV1Endpoint = "https://identitytoolkit.googleapis.com/v1"
37
+
38
+ // Maximum number of users allowed to batch get at a time.
39
+ maxGetAccountsBatchSize = 100
40
+
41
+ // Maximum number of users allowed to batch delete at a time.
42
+ maxDeleteAccountsBatchSize = 1000
37
43
)
38
44
39
45
// 'REDACTED', encoded as a base64 string.
@@ -57,6 +63,9 @@ type UserInfo struct {
57
63
type UserMetadata struct {
58
64
CreationTimestamp int64
59
65
LastLogInTimestamp int64
66
+ // The time at which the user was last active (ID token refreshed), or 0 if
67
+ // the user was never active.
68
+ LastRefreshTimestamp int64
60
69
}
61
70
62
71
// UserRecord contains metadata associated with a Firebase user account.
@@ -491,6 +500,15 @@ func validatePhone(phone string) error {
491
500
return nil
492
501
}
493
502
503
+ func validateProvider (providerID string , providerUID string ) error {
504
+ if providerID == "" {
505
+ return fmt .Errorf ("providerID must be a non-empty string" )
506
+ } else if providerUID == "" {
507
+ return fmt .Errorf ("providerUID must be a non-empty string" )
508
+ }
509
+ return nil
510
+ }
511
+
494
512
// End of validators
495
513
496
514
// GetUser gets the user data corresponding to the specified user ID.
@@ -545,12 +563,13 @@ func (q *userQuery) build() map[string]interface{} {
545
563
}
546
564
}
547
565
566
+ type getAccountInfoResponse struct {
567
+ Users []* userQueryResponse `json:"users"`
568
+ }
569
+
548
570
func (c * baseClient ) getUser (ctx context.Context , query * userQuery ) (* UserRecord , error ) {
549
- var parsed struct {
550
- Users []* userQueryResponse `json:"users"`
551
- }
552
- _ , err := c .post (ctx , "/accounts:lookup" , query .build (), & parsed )
553
- if err != nil {
571
+ var parsed getAccountInfoResponse
572
+ if _ , err := c .post (ctx , "/accounts:lookup" , query .build (), & parsed ); err != nil {
554
573
return nil , err
555
574
}
556
575
@@ -561,6 +580,195 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord
561
580
return parsed .Users [0 ].makeUserRecord ()
562
581
}
563
582
583
+ // A UserIdentifier identifies a user to be looked up.
584
+ type UserIdentifier interface {
585
+ matches (ur * UserRecord ) bool
586
+ populate (req * getAccountInfoRequest )
587
+ }
588
+
589
+ // A UIDIdentifier is used for looking up an account by uid.
590
+ //
591
+ // See GetUsers function.
592
+ type UIDIdentifier struct {
593
+ UID string
594
+ }
595
+
596
+ func (id UIDIdentifier ) matches (ur * UserRecord ) bool {
597
+ return id .UID == ur .UID
598
+ }
599
+
600
+ func (id UIDIdentifier ) populate (req * getAccountInfoRequest ) {
601
+ req .LocalID = append (req .LocalID , id .UID )
602
+ }
603
+
604
+ // An EmailIdentifier is used for looking up an account by email.
605
+ //
606
+ // See GetUsers function.
607
+ type EmailIdentifier struct {
608
+ Email string
609
+ }
610
+
611
+ func (id EmailIdentifier ) matches (ur * UserRecord ) bool {
612
+ return id .Email == ur .Email
613
+ }
614
+
615
+ func (id EmailIdentifier ) populate (req * getAccountInfoRequest ) {
616
+ req .Email = append (req .Email , id .Email )
617
+ }
618
+
619
+ // A PhoneIdentifier is used for looking up an account by phone number.
620
+ //
621
+ // See GetUsers function.
622
+ type PhoneIdentifier struct {
623
+ PhoneNumber string
624
+ }
625
+
626
+ func (id PhoneIdentifier ) matches (ur * UserRecord ) bool {
627
+ return id .PhoneNumber == ur .PhoneNumber
628
+ }
629
+
630
+ func (id PhoneIdentifier ) populate (req * getAccountInfoRequest ) {
631
+ req .PhoneNumber = append (req .PhoneNumber , id .PhoneNumber )
632
+ }
633
+
634
+ // A ProviderIdentifier is used for looking up an account by federated provider.
635
+ //
636
+ // See GetUsers function.
637
+ type ProviderIdentifier struct {
638
+ ProviderID string
639
+ ProviderUID string
640
+ }
641
+
642
+ func (id ProviderIdentifier ) matches (ur * UserRecord ) bool {
643
+ for _ , userInfo := range ur .ProviderUserInfo {
644
+ if id .ProviderID == userInfo .ProviderID && id .ProviderUID == userInfo .UID {
645
+ return true
646
+ }
647
+ }
648
+ return false
649
+ }
650
+
651
+ func (id ProviderIdentifier ) populate (req * getAccountInfoRequest ) {
652
+ req .FederatedUserID = append (
653
+ req .FederatedUserID ,
654
+ federatedUserIdentifier {ProviderID : id .ProviderID , RawID : id .ProviderUID })
655
+ }
656
+
657
+ // A GetUsersResult represents the result of the GetUsers() API.
658
+ type GetUsersResult struct {
659
+ // Set of UserRecords corresponding to the set of users that were requested.
660
+ // Only users that were found are listed here. The result set is unordered.
661
+ Users []* UserRecord
662
+
663
+ // Set of UserIdentifiers that were requested, but not found.
664
+ NotFound []UserIdentifier
665
+ }
666
+
667
+ type federatedUserIdentifier struct {
668
+ ProviderID string `json:"providerId,omitempty"`
669
+ RawID string `json:"rawId,omitempty"`
670
+ }
671
+
672
+ type getAccountInfoRequest struct {
673
+ LocalID []string `json:"localId,omitempty"`
674
+ Email []string `json:"email,omitempty"`
675
+ PhoneNumber []string `json:"phoneNumber,omitempty"`
676
+ FederatedUserID []federatedUserIdentifier `json:"federatedUserId,omitempty"`
677
+ }
678
+
679
+ func (req * getAccountInfoRequest ) validate () error {
680
+ for i := range req .LocalID {
681
+ if err := validateUID (req .LocalID [i ]); err != nil {
682
+ return err
683
+ }
684
+ }
685
+
686
+ for i := range req .Email {
687
+ if err := validateEmail (req .Email [i ]); err != nil {
688
+ return err
689
+ }
690
+ }
691
+
692
+ for i := range req .PhoneNumber {
693
+ if err := validatePhone (req .PhoneNumber [i ]); err != nil {
694
+ return err
695
+ }
696
+ }
697
+
698
+ for i := range req .FederatedUserID {
699
+ id := & req .FederatedUserID [i ]
700
+ if err := validateProvider (id .ProviderID , id .RawID ); err != nil {
701
+ return err
702
+ }
703
+ }
704
+
705
+ return nil
706
+ }
707
+
708
+ func isUserFound (id UserIdentifier , urs [](* UserRecord )) bool {
709
+ for i := range urs {
710
+ if id .matches (urs [i ]) {
711
+ return true
712
+ }
713
+ }
714
+ return false
715
+ }
716
+
717
+ // GetUsers returns the user data corresponding to the specified identifiers.
718
+ //
719
+ // There are no ordering guarantees; in particular, the nth entry in the users
720
+ // result list is not guaranteed to correspond to the nth entry in the input
721
+ // parameters list.
722
+ //
723
+ // A maximum of 100 identifiers may be supplied. If more than 100
724
+ // identifiers are supplied, this method returns an error.
725
+ //
726
+ // Returns the corresponding user records. An error is returned instead if any
727
+ // of the identifiers are invalid or if more than 100 identifiers are
728
+ // specified.
729
+ func (c * baseClient ) GetUsers (
730
+ ctx context.Context , identifiers []UserIdentifier ,
731
+ ) (* GetUsersResult , error ) {
732
+ if len (identifiers ) == 0 {
733
+ return & GetUsersResult {[](* UserRecord ){}, [](UserIdentifier ){}}, nil
734
+ } else if len (identifiers ) > maxGetAccountsBatchSize {
735
+ return nil , fmt .Errorf (
736
+ "`identifiers` parameter must have <= %d entries" , maxGetAccountsBatchSize )
737
+ }
738
+
739
+ var request getAccountInfoRequest
740
+ for i := range identifiers {
741
+ identifiers [i ].populate (& request )
742
+ }
743
+
744
+ if err := request .validate (); err != nil {
745
+ return nil , err
746
+ }
747
+
748
+ var parsed getAccountInfoResponse
749
+ if _ , err := c .post (ctx , "/accounts:lookup" , request , & parsed ); err != nil {
750
+ return nil , err
751
+ }
752
+
753
+ var userRecords [](* UserRecord )
754
+ for _ , user := range parsed .Users {
755
+ userRecord , err := user .makeUserRecord ()
756
+ if err != nil {
757
+ return nil , err
758
+ }
759
+ userRecords = append (userRecords , userRecord )
760
+ }
761
+
762
+ var notFound []UserIdentifier
763
+ for i := range identifiers {
764
+ if ! isUserFound (identifiers [i ], userRecords ) {
765
+ notFound = append (notFound , identifiers [i ])
766
+ }
767
+ }
768
+
769
+ return & GetUsersResult {userRecords , notFound }, nil
770
+ }
771
+
564
772
type userQueryResponse struct {
565
773
UID string `json:"localId,omitempty"`
566
774
DisplayName string `json:"displayName,omitempty"`
@@ -569,6 +777,7 @@ type userQueryResponse struct {
569
777
PhotoURL string `json:"photoUrl,omitempty"`
570
778
CreationTimestamp int64 `json:"createdAt,string,omitempty"`
571
779
LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"`
780
+ LastRefreshAt string `json:"lastRefreshAt,omitempty"`
572
781
ProviderID string `json:"providerId,omitempty"`
573
782
CustomAttributes string `json:"customAttributes,omitempty"`
574
783
Disabled bool `json:"disabled,omitempty"`
@@ -592,8 +801,7 @@ func (r *userQueryResponse) makeUserRecord() (*UserRecord, error) {
592
801
func (r * userQueryResponse ) makeExportedUserRecord () (* ExportedUserRecord , error ) {
593
802
var customClaims map [string ]interface {}
594
803
if r .CustomAttributes != "" {
595
- err := json .Unmarshal ([]byte (r .CustomAttributes ), & customClaims )
596
- if err != nil {
804
+ if err := json .Unmarshal ([]byte (r .CustomAttributes ), & customClaims ); err != nil {
597
805
return nil , err
598
806
}
599
807
if len (customClaims ) == 0 {
@@ -609,6 +817,15 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
609
817
hash = ""
610
818
}
611
819
820
+ var lastRefreshTimestamp int64
821
+ if r .LastRefreshAt != "" {
822
+ t , err := time .Parse (time .RFC3339 , r .LastRefreshAt )
823
+ if err != nil {
824
+ return nil , err
825
+ }
826
+ lastRefreshTimestamp = t .Unix () * 1000
827
+ }
828
+
612
829
return & ExportedUserRecord {
613
830
UserRecord : & UserRecord {
614
831
UserInfo : & UserInfo {
@@ -626,8 +843,9 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
626
843
TenantID : r .TenantID ,
627
844
TokensValidAfterMillis : r .ValidSinceSeconds * 1000 ,
628
845
UserMetadata : & UserMetadata {
629
- LastLogInTimestamp : r .LastLogInTimestamp ,
630
- CreationTimestamp : r .CreationTimestamp ,
846
+ LastLogInTimestamp : r .LastLogInTimestamp ,
847
+ CreationTimestamp : r .CreationTimestamp ,
848
+ LastRefreshTimestamp : lastRefreshTimestamp ,
631
849
},
632
850
},
633
851
PasswordHash : hash ,
@@ -728,6 +946,91 @@ func (c *baseClient) DeleteUser(ctx context.Context, uid string) error {
728
946
return err
729
947
}
730
948
949
+ // A DeleteUsersResult represents the result of the DeleteUsers() call.
950
+ type DeleteUsersResult struct {
951
+ // The number of users that were deleted successfully (possibly zero). Users
952
+ // that did not exist prior to calling DeleteUsers() are considered to be
953
+ // successfully deleted.
954
+ SuccessCount int
955
+
956
+ // The number of users that failed to be deleted (possibly zero).
957
+ FailureCount int
958
+
959
+ // A list of DeleteUsersErrorInfo instances describing the errors that were
960
+ // encountered during the deletion. Length of this list is equal to the value
961
+ // of FailureCount.
962
+ Errors []* DeleteUsersErrorInfo
963
+ }
964
+
965
+ // DeleteUsersErrorInfo represents an error encountered while deleting a user
966
+ // account.
967
+ //
968
+ // The Index field corresponds to the index of the failed user in the uids
969
+ // array that was passed to DeleteUsers().
970
+ type DeleteUsersErrorInfo struct {
971
+ Index int `json:"index,omitEmpty"`
972
+ Reason string `json:"message,omitEmpty"`
973
+ }
974
+
975
+ // DeleteUsers deletes the users specified by the given identifiers.
976
+ //
977
+ // Deleting a non-existing user won't generate an error. (i.e. this method is
978
+ // idempotent.) Non-existing users are considered to be successfully
979
+ // deleted, and are therefore counted in the DeleteUsersResult.SuccessCount
980
+ // value.
981
+ //
982
+ // A maximum of 1000 identifiers may be supplied. If more than 1000
983
+ // identifiers are supplied, this method returns an error.
984
+ //
985
+ // This API is currently rate limited at the server to 1 QPS. If you exceed
986
+ // this, you may get a quota exceeded error. Therefore, if you want to delete
987
+ // more than 1000 users, you may need to add a delay to ensure you don't go
988
+ // over this limit.
989
+ //
990
+ // Returns the total number of successful/failed deletions, as well as the
991
+ // array of errors that correspond to the failed deletions. An error is
992
+ // returned if any of the identifiers are invalid or if more than 1000
993
+ // identifiers are specified.
994
+ func (c * baseClient ) DeleteUsers (ctx context.Context , uids []string ) (* DeleteUsersResult , error ) {
995
+ if len (uids ) == 0 {
996
+ return & DeleteUsersResult {}, nil
997
+ } else if len (uids ) > maxDeleteAccountsBatchSize {
998
+ return nil , fmt .Errorf (
999
+ "`uids` parameter must have <= %d entries" , maxDeleteAccountsBatchSize )
1000
+ }
1001
+
1002
+ var payload struct {
1003
+ LocalIds []string `json:"localIds"`
1004
+ Force bool `json:"force"`
1005
+ }
1006
+ payload .Force = true
1007
+
1008
+ for i := range uids {
1009
+ if err := validateUID (uids [i ]); err != nil {
1010
+ return nil , err
1011
+ }
1012
+
1013
+ payload .LocalIds = append (payload .LocalIds , uids [i ])
1014
+ }
1015
+
1016
+ type batchDeleteAccountsResponse struct {
1017
+ Errors []* DeleteUsersErrorInfo `json:"errors"`
1018
+ }
1019
+
1020
+ resp := batchDeleteAccountsResponse {}
1021
+ if _ , err := c .post (ctx , "/accounts:batchDelete" , payload , & resp ); err != nil {
1022
+ return nil , err
1023
+ }
1024
+
1025
+ result := DeleteUsersResult {
1026
+ FailureCount : len (resp .Errors ),
1027
+ SuccessCount : len (uids ) - len (resp .Errors ),
1028
+ Errors : resp .Errors ,
1029
+ }
1030
+
1031
+ return & result , nil
1032
+ }
1033
+
731
1034
// SessionCookie creates a new Firebase session cookie from the given ID token and expiry
732
1035
// duration. The returned JWT can be set as a server-side session cookie with a custom cookie
733
1036
// policy. Expiry duration must be at least 5 minutes but may not exceed 14 days.
0 commit comments