Skip to content
Open
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ The Firebase Admin Go SDK enables server-side (backend) applications to interact

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

Expand Down
29 changes: 29 additions & 0 deletions auth/tenant_mgt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,35 @@ func TestTenantGetUser(t *testing.T) {
}
}

func TestTenantQueryUsers(t *testing.T) {
resp := `{
"usersInfo": [],
"recordsCount": "0"
}`
s := echoServer([]byte(resp), t)
defer s.Close()

tenantClient, err := s.Client.TenantManager.AuthForTenant("test-tenant")
if err != nil {
t.Fatalf("Failed to create tenant client: %v", err)
}

returnUserInfo := true
query := &QueryUsersRequest{
ReturnUserInfo: &returnUserInfo,
}

_, err = tenantClient.QueryUsers(context.Background(), query)
if err != nil {
t.Fatalf("QueryUsers() with tenant client = %v", err)
}

wantPath := "/projects/mock-project-id/tenants/test-tenant/accounts:query"
if s.Req[0].RequestURI != wantPath {
t.Errorf("QueryUsers() URL = %q; want = %q", s.Req[0].RequestURI, wantPath)
}
}

func TestTenantGetUserByEmail(t *testing.T) {
s := echoServer(testGetUserResponse, t)
defer s.Close()
Expand Down
172 changes: 172 additions & 0 deletions auth/user_mgt.go
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,178 @@ func (c *baseClient) GetUsers(
return &GetUsersResult{userRecords, notFound}, nil
}

// QueryUserInfoResponse is the response structure for the QueryUsers function.
type QueryUserInfoResponse struct {
Users []*UserRecord
Count int64
}

type queryUsersResponse struct {
Users []*userQueryResponse `json:"userInfo"`
Count int64 `json:"recordsCount,string,omitempty"`
}

// Expression is a query condition used to filter results.
//
// Only one of Email, PhoneNumber, or UID should be specified.
// If more than one is specified, only the first (in the order of Email, PhoneNumber, UID) will be applied.
type Expression struct {
// Email is a case insensitive string that the account's email should match.
Email string `json:"email,omitempty"`
// PhoneNumber is a string that the account's phone number should match.
PhoneNumber string `json:"phoneNumber,omitempty"`
// UID is a string that the account's local ID should match.
UID string `json:"userId,omitempty"`
}

// QueryUsersRequest is the request structure for the QueryUsers function.
type QueryUsersRequest struct {
// ReturnUserInfo indicates whether to return the accounts matching the query.
// If false, only the count of accounts matching the query will be returned.
// Defaults to true.
ReturnUserInfo *bool `json:"returnUserInfo,omitempty"`
// Limit is the maximum number of accounts to return with an upper limit of 500.
// Defaults to 500. Only valid when ReturnUserInfo is set to true.
Limit int64 `json:"limit,string,omitempty"`
// Offset is the number of accounts to skip from the beginning of matching records.
// Only valid when ReturnUserInfo is set to true.
Offset int64 `json:"offset,string,omitempty"`
// SortBy is the field to use for sorting user accounts.
SortBy SortBy `json:"-"`
// Order is the order for sorting query results.
Order Order `json:"-"`
// TenantID is the ID of the tenant to which the result is scoped.
TenantID string `json:"tenantId,omitempty"`
// Expression is a list of query conditions used to filter results.
Expression []*Expression `json:"expression,omitempty"`
}

// build builds the query request (for internal use only).
func (q *QueryUsersRequest) build() interface{} {
var sortBy string
if q.SortBy != sortByUnspecified {
sortBys := map[SortBy]string{
UID: "USER_ID",
Name: "NAME",
CreatedAt: "CREATED_AT",
LastLoginAt: "LAST_LOGIN_AT",
UserEmail: "USER_EMAIL",
}
sortBy = sortBys[q.SortBy]
}

var order string
if q.Order != orderUnspecified {
orders := map[Order]string{
Asc: "ASC",
Desc: "DESC",
}
order = orders[q.Order]
}

type queryUsersRequestInternal QueryUsersRequest
internal := (*queryUsersRequestInternal)(q)
if internal.ReturnUserInfo == nil {
t := true
internal.ReturnUserInfo = &t
}

return &struct {
SortBy string `json:"sortBy,omitempty"`
Order string `json:"order,omitempty"`
*queryUsersRequestInternal
}{
SortBy: sortBy,
Order: order,
queryUsersRequestInternal: internal,
}
}

func (q *QueryUsersRequest) validate() error {
if q.Limit != 0 && (q.Limit < 1 || q.Limit > 500) {
return fmt.Errorf("limit must be between 1 and 500")
}
if q.Offset < 0 {
return fmt.Errorf("offset must be non-negative")
}
for _, exp := range q.Expression {
if exp.Email != "" {
if err := validateEmail(exp.Email); err != nil {
return err
}
}
if exp.PhoneNumber != "" {
if err := validatePhone(exp.PhoneNumber); err != nil {
return err
}
}
if exp.UID != "" {
if err := validateUID(exp.UID); err != nil {
return err
}
}
}
return nil
}

// SortBy is a field to use for sorting user accounts.
type SortBy int

const (
sortByUnspecified SortBy = iota
// UID sorts results by userId.
UID
// Name sorts results by name.
Name
// CreatedAt sorts results by createdAt.
CreatedAt
// LastLoginAt sorts results by lastLoginAt.
LastLoginAt
// UserEmail sorts results by userEmail.
UserEmail
)

// Order is an order for sorting query results.
type Order int

const (
orderUnspecified Order = iota
// Asc sorts in ascending order.
Asc
// Desc sorts in descending order.
Desc
)

// QueryUsers queries for user accounts based on the provided query configuration.
func (c *baseClient) QueryUsers(ctx context.Context, query *QueryUsersRequest) (*QueryUserInfoResponse, error) {
if query == nil {
return nil, fmt.Errorf("query request must not be nil")
}
if err := query.validate(); err != nil {
return nil, err
}

var parsed queryUsersResponse
_, err := c.post(ctx, "/accounts:query", query.build(), &parsed)
if err != nil {
return nil, err
}

var userRecords []*UserRecord
for _, user := range parsed.Users {
userRecord, err := user.makeUserRecord()
if err != nil {
return nil, fmt.Errorf("error while parsing response: %w", err)
}
userRecords = append(userRecords, userRecord)
}

return &QueryUserInfoResponse{
Users: userRecords,
Count: parsed.Count,
}, nil
}

type userQueryResponse struct {
UID string `json:"localId,omitempty"`
DisplayName string `json:"displayName,omitempty"`
Expand Down
Loading
Loading