Skip to content

Commit d7795a6

Browse files
feat(auth): Add QueryUsers API (#727)
This change implements the accounts:query functionality, providing a new QueryUsers method that allows searching for users with filters and sorting options. RELEASE_NOTE: Added QueryUsers() API to support querying user accounts with filters, sorting, and pagination.
1 parent 1fe3c70 commit d7795a6

File tree

6 files changed

+496
-0
lines changed

6 files changed

+496
-0
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ The Firebase Admin Go SDK enables server-side (backend) applications to interact
6464

6565
- **DO:** Use the centralized HTTP client in `internal/http_client.go` for all network calls.
6666
- **DO:** Pass `context.Context` as the first argument to all functions that perform I/O or other blocking operations.
67+
- **DO:** Run `go fmt` after implementing a change and fix any linting errors.
6768
- **DON'T:** Expose types or functions from the `internal/` directory in the public API.
6869
- **DON'T:** Introduce new third-party dependencies without a strong, documented justification and team consensus.
6970

auth/tenant_mgt_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,35 @@ func TestTenantGetUser(t *testing.T) {
9090
}
9191
}
9292

93+
func TestTenantQueryUsers(t *testing.T) {
94+
resp := `{
95+
"usersInfo": [],
96+
"recordsCount": "0"
97+
}`
98+
s := echoServer([]byte(resp), t)
99+
defer s.Close()
100+
101+
tenantClient, err := s.Client.TenantManager.AuthForTenant("test-tenant")
102+
if err != nil {
103+
t.Fatalf("Failed to create tenant client: %v", err)
104+
}
105+
106+
returnUserInfo := true
107+
query := &QueryUsersRequest{
108+
ReturnUserInfo: &returnUserInfo,
109+
}
110+
111+
_, err = tenantClient.QueryUsers(context.Background(), query)
112+
if err != nil {
113+
t.Fatalf("QueryUsers() with tenant client = %v", err)
114+
}
115+
116+
wantPath := "/projects/mock-project-id/tenants/test-tenant/accounts:query"
117+
if s.Req[0].RequestURI != wantPath {
118+
t.Errorf("QueryUsers() URL = %q; want = %q", s.Req[0].RequestURI, wantPath)
119+
}
120+
}
121+
93122
func TestTenantGetUserByEmail(t *testing.T) {
94123
s := echoServer(testGetUserResponse, t)
95124
defer s.Close()

auth/user_mgt.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,178 @@ func (c *baseClient) GetUsers(
10481048
return &GetUsersResult{userRecords, notFound}, nil
10491049
}
10501050

1051+
// QueryUserInfoResponse is the response from the QueryUsers function.
1052+
type QueryUserInfoResponse struct {
1053+
Users []*UserRecord
1054+
Count int64
1055+
}
1056+
1057+
type queryUsersResponse struct {
1058+
Users []*userQueryResponse `json:"userInfo"`
1059+
Count int64 `json:"recordsCount,string,omitempty"`
1060+
}
1061+
1062+
// Expression represents a query condition used to filter results.
1063+
//
1064+
// Specify only one of Email, PhoneNumber, or UID. If you specify more than one,
1065+
// only the first (in order of Email, PhoneNumber, then UID) is applied.
1066+
type Expression struct {
1067+
// Email is a case-insensitive string that the account's email must match.
1068+
Email string `json:"email,omitempty"`
1069+
// PhoneNumber is a string that the account's phone number must match.
1070+
PhoneNumber string `json:"phoneNumber,omitempty"`
1071+
// UID is a string that the account's local ID must match.
1072+
UID string `json:"userId,omitempty"`
1073+
}
1074+
1075+
// QueryUsersRequest is the request structure for the QueryUsers function.
1076+
type QueryUsersRequest struct {
1077+
// ReturnUserInfo specifies whether to return user accounts that match the query.
1078+
// If set to false, only the count of matching accounts is returned.
1079+
// Defaults to true.
1080+
ReturnUserInfo *bool `json:"returnUserInfo,omitempty"`
1081+
// Limit is the maximum number of accounts to return with an upper limit of 500.
1082+
// Defaults to 500. This field is valid only when ReturnUserInfo is true.
1083+
Limit int64 `json:"limit,string,omitempty"`
1084+
// Offset is the number of accounts to skip from the beginning of matching records.
1085+
// This field is valid only when ReturnUserInfo is true.
1086+
Offset int64 `json:"offset,string,omitempty"`
1087+
// SortBy is the field to use for sorting user accounts.
1088+
SortBy SortBy `json:"-"`
1089+
// Order is the sort order for the query results.
1090+
Order Order `json:"-"`
1091+
// TenantID is the ID of the tenant to which the results are scoped.
1092+
TenantID string `json:"tenantId,omitempty"`
1093+
// Expression is a list of query conditions used to filter the results.
1094+
Expression []*Expression `json:"expression,omitempty"`
1095+
}
1096+
1097+
// build builds the query request (for internal use only).
1098+
func (q *QueryUsersRequest) build() interface{} {
1099+
var sortBy string
1100+
if q.SortBy != sortByUnspecified {
1101+
sortBys := map[SortBy]string{
1102+
UID: "USER_ID",
1103+
Name: "NAME",
1104+
CreatedAt: "CREATED_AT",
1105+
LastLoginAt: "LAST_LOGIN_AT",
1106+
UserEmail: "USER_EMAIL",
1107+
}
1108+
sortBy = sortBys[q.SortBy]
1109+
}
1110+
1111+
var order string
1112+
if q.Order != orderUnspecified {
1113+
orders := map[Order]string{
1114+
Asc: "ASC",
1115+
Desc: "DESC",
1116+
}
1117+
order = orders[q.Order]
1118+
}
1119+
1120+
type queryUsersRequestInternal QueryUsersRequest
1121+
internal := (*queryUsersRequestInternal)(q)
1122+
if internal.ReturnUserInfo == nil {
1123+
t := true
1124+
internal.ReturnUserInfo = &t
1125+
}
1126+
1127+
return &struct {
1128+
SortBy string `json:"sortBy,omitempty"`
1129+
Order string `json:"order,omitempty"`
1130+
*queryUsersRequestInternal
1131+
}{
1132+
SortBy: sortBy,
1133+
Order: order,
1134+
queryUsersRequestInternal: internal,
1135+
}
1136+
}
1137+
1138+
func (q *QueryUsersRequest) validate() error {
1139+
if q.Limit != 0 && (q.Limit < 1 || q.Limit > 500) {
1140+
return fmt.Errorf("limit must be between 1 and 500")
1141+
}
1142+
if q.Offset < 0 {
1143+
return fmt.Errorf("offset must be non-negative")
1144+
}
1145+
for _, exp := range q.Expression {
1146+
if exp.Email != "" {
1147+
if err := validateEmail(exp.Email); err != nil {
1148+
return err
1149+
}
1150+
}
1151+
if exp.PhoneNumber != "" {
1152+
if err := validatePhone(exp.PhoneNumber); err != nil {
1153+
return err
1154+
}
1155+
}
1156+
if exp.UID != "" {
1157+
if err := validateUID(exp.UID); err != nil {
1158+
return err
1159+
}
1160+
}
1161+
}
1162+
return nil
1163+
}
1164+
1165+
// SortBy defines the fields available for sorting user accounts.
1166+
type SortBy int
1167+
1168+
const (
1169+
sortByUnspecified SortBy = iota
1170+
// UID sorts results by user ID.
1171+
UID
1172+
// Name sorts results by name.
1173+
Name
1174+
// CreatedAt sorts results by creation time.
1175+
CreatedAt
1176+
// LastLoginAt sorts results by the last login time.
1177+
LastLoginAt
1178+
// UserEmail sorts results by user email.
1179+
UserEmail
1180+
)
1181+
1182+
// Order defines the sort order for query results.
1183+
type Order int
1184+
1185+
const (
1186+
orderUnspecified Order = iota
1187+
// Asc sorts results in ascending order.
1188+
Asc
1189+
// Desc sorts results in descending order.
1190+
Desc
1191+
)
1192+
1193+
// QueryUsers queries for user accounts based on the provided query configuration.
1194+
func (c *baseClient) QueryUsers(ctx context.Context, query *QueryUsersRequest) (*QueryUserInfoResponse, error) {
1195+
if query == nil {
1196+
return nil, fmt.Errorf("query request must not be nil")
1197+
}
1198+
if err := query.validate(); err != nil {
1199+
return nil, err
1200+
}
1201+
1202+
var parsed queryUsersResponse
1203+
_, err := c.post(ctx, "/accounts:query", query.build(), &parsed)
1204+
if err != nil {
1205+
return nil, err
1206+
}
1207+
1208+
var userRecords []*UserRecord
1209+
for _, user := range parsed.Users {
1210+
userRecord, err := user.makeUserRecord()
1211+
if err != nil {
1212+
return nil, fmt.Errorf("error while parsing response: %w", err)
1213+
}
1214+
userRecords = append(userRecords, userRecord)
1215+
}
1216+
1217+
return &QueryUserInfoResponse{
1218+
Users: userRecords,
1219+
Count: parsed.Count,
1220+
}, nil
1221+
}
1222+
10511223
type userQueryResponse struct {
10521224
UID string `json:"localId,omitempty"`
10531225
DisplayName string `json:"displayName,omitempty"`

0 commit comments

Comments
 (0)