From 2a6ffb524b550030a69761b4fa978866165cd118 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Mon, 3 Aug 2026 00:12:12 +0530 Subject: [PATCH] feat(admin): add exact email user lookup support --- internal/api/admin.go | 18 +++++++++++++ internal/api/admin_test.go | 54 ++++++++++++++++++++++++++++++++++++++ internal/models/user.go | 5 ++++ 3 files changed, 77 insertions(+) diff --git a/internal/api/admin.go b/internal/api/admin.go index 7077b757a..9799966b5 100644 --- a/internal/api/admin.go +++ b/internal/api/admin.go @@ -120,6 +120,24 @@ func (a *API) adminUsers(w http.ResponseWriter, r *http.Request) error { db := a.db.WithContext(ctx) aud := a.requestAud(ctx, r) + email := r.URL.Query().Get("email") + if email != "" { + user, err := models.FindUserByEmailAndAudienceIncludingSSO(db, email, aud) + if err != nil { + if models.IsNotFoundError(err) { + return sendJSON(w, http.StatusOK, AdminListUsersResponse{ + Users: []*models.User{}, + Aud: aud, + }) + } + return apierrors.NewInternalServerError("Database error finding user by email").WithInternalError(err) + } + return sendJSON(w, http.StatusOK, AdminListUsersResponse{ + Users: []*models.User{user}, + Aud: aud, + }) + } + sortParams, err := sort(r, map[string]bool{models.CreatedAt: true}, []models.SortField{{Name: models.CreatedAt, Dir: models.Descending}}) if err != nil { return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Bad Sort Parameters: %v", err) diff --git a/internal/api/admin_test.go b/internal/api/admin_test.go index d3404d012..d49d753ec 100644 --- a/internal/api/admin_test.go +++ b/internal/api/admin_test.go @@ -173,6 +173,60 @@ func (ts *AdminTestSuite) TestAdminUsers_SortDesc() { assert.Equal(ts.T(), "test1@example.com", data.Users[1].GetEmail()) } +func (ts *AdminTestSuite) TestAdminUsers_FindByEmail() { + u1, err := models.NewUser("12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err) + require.NoError(ts.T(), ts.API.db.Create(u1)) + + u2, err := models.NewUser("87654321", "test2@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err) + require.NoError(ts.T(), ts.API.db.Create(u2)) + + // 1. Exact match test + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/admin/users?email=test1@example.com", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + + var data struct { + Users []*models.User `json:"users"` + Aud string `json:"aud"` + } + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + require.Len(ts.T(), data.Users, 1) + assert.Equal(ts.T(), "test1@example.com", data.Users[0].GetEmail()) + + // 2. Case-insensitivity test + w = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/admin/users?email=TeSt2@ExAmPlE.cOm", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + + var data2 struct { + Users []*models.User `json:"users"` + Aud string `json:"aud"` + } + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data2)) + require.Len(ts.T(), data2.Users, 1) + assert.Equal(ts.T(), "test2@example.com", data2.Users[0].GetEmail()) + + // 3. Not found test + w = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/admin/users?email=notfound@example.com", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + + var data3 struct { + Users []*models.User `json:"users"` + Aud string `json:"aud"` + } + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data3)) + assert.Empty(ts.T(), data3.Users) +} + type adminUsersCursorResponse struct { Users []*models.User `json:"users"` Aud string `json:"aud"` diff --git a/internal/models/user.go b/internal/models/user.go index 791b9acc8..f20614ce9 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -621,6 +621,11 @@ func FindUserByEmailAndAudience(tx *storage.Connection, email, aud string) (*Use return findUser(tx, "instance_id = ? and LOWER(email) = ? and aud = ? and is_sso_user = false", uuid.Nil, strings.ToLower(email), aud) } +// FindUserByEmailAndAudienceIncludingSSO finds a user with the matching email and audience, including SSO users. +func FindUserByEmailAndAudienceIncludingSSO(tx *storage.Connection, email, aud string) (*User, error) { + return findUser(tx, "instance_id = ? and LOWER(email) = ? and aud = ?", uuid.Nil, strings.ToLower(email), aud) +} + // FindUserByPhoneAndAudience finds a user with the matching email and audience. func FindUserByPhoneAndAudience(tx *storage.Connection, phone, aud string) (*User, error) { return findUser(tx, "instance_id = ? and phone = ? and aud = ? and is_sso_user = false", uuid.Nil, phone, aud)