From 5d6d025c9b8d2abca9ec2bfdc795d1f0c1c6592d Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Sat, 5 Oct 2024 02:45:06 +0900
Subject: [PATCH] Add support for searching users by email (#30908)

Fix #30898

we have an option `SearchByEmail`, so enable it, then we can search user
by email.
Also added a test for it.
---
 models/user/search.go                     | 14 ++++++++-
 routers/api/v1/user/user.go               | 11 +++----
 tests/integration/api_user_search_test.go | 36 +++++++++++++++++++++++
 3 files changed, 55 insertions(+), 6 deletions(-)

diff --git a/models/user/search.go b/models/user/search.go
index 45b051187ea0..382b6fac2b08 100644
--- a/models/user/search.go
+++ b/models/user/search.go
@@ -65,7 +65,19 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
 			builder.Like{"LOWER(full_name)", lowerKeyword},
 		)
 		if opts.SearchByEmail {
-			keywordCond = keywordCond.Or(builder.Like{"LOWER(email)", lowerKeyword})
+			var emailCond builder.Cond
+			emailCond = builder.Like{"LOWER(email)", lowerKeyword}
+			if opts.Actor == nil {
+				emailCond = emailCond.And(builder.Eq{"keep_email_private": false})
+			} else if !opts.Actor.IsAdmin {
+				emailCond = emailCond.And(
+					builder.Or(
+						builder.Eq{"keep_email_private": false},
+						builder.Eq{"id": opts.Actor.ID},
+					),
+				)
+			}
+			keywordCond = keywordCond.Or(emailCond)
 		}
 
 		cond = cond.And(keywordCond)
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index fedad87fc4fa..2c277a18c739 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -68,11 +68,12 @@ func Search(ctx *context.APIContext) {
 		users = []*user_model.User{user_model.NewActionsUser()}
 	default:
 		users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
-			Actor:       ctx.Doer,
-			Keyword:     ctx.FormTrim("q"),
-			UID:         uid,
-			Type:        user_model.UserTypeIndividual,
-			ListOptions: listOptions,
+			Actor:         ctx.Doer,
+			Keyword:       ctx.FormTrim("q"),
+			UID:           uid,
+			Type:          user_model.UserTypeIndividual,
+			SearchByEmail: true,
+			ListOptions:   listOptions,
 		})
 		if err != nil {
 			ctx.JSON(http.StatusInternalServerError, map[string]any{
diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go
index f776b3532576..ff4671c54e94 100644
--- a/tests/integration/api_user_search_test.go
+++ b/tests/integration/api_user_search_test.go
@@ -109,3 +109,39 @@ func TestAPIUserSearchNotLoggedInUserHidden(t *testing.T) {
 	DecodeJSON(t, resp, &results)
 	assert.Empty(t, results.Data)
 }
+
+func TestAPIUserSearchByEmail(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	// admin can search user with private email
+	adminUsername := "user1"
+	session := loginUser(t, adminUsername)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
+	query := "user2@example.com"
+	req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query).
+		AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+
+	var results SearchResults
+	DecodeJSON(t, resp, &results)
+	assert.Equal(t, 1, len(results.Data))
+	assert.Equal(t, query, results.Data[0].Email)
+
+	// no login user can not search user with private email
+	req = NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query)
+	resp = MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &results)
+	assert.Empty(t, results.Data)
+
+	// user can search self with private email
+	user2 := "user2"
+	session = loginUser(t, user2)
+	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
+	req = NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query).
+		AddTokenAuth(token)
+	resp = MakeRequest(t, req, http.StatusOK)
+
+	DecodeJSON(t, resp, &results)
+	assert.Equal(t, 1, len(results.Data))
+	assert.Equal(t, query, results.Data[0].Email)
+}