mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Implement POST /users/search endpoint for APIv4 (#5822)
* Implement POST /users/search endpoint for APIv4 * PLT-2713 Added store functions for searching users that don't have a team * PLT-2713 Added 'without_team' option when searching users * PLT-2713 Added 'without_team' option when searching users (v4)
This commit is contained in:
committed by
George Goldberg
parent
7e2e823884
commit
2a753949f1
16
api/user.go
16
api/user.go
@@ -1535,22 +1535,12 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var profiles []*model.User
|
||||
var err *model.AppError
|
||||
if props.InChannelId != "" {
|
||||
profiles, err = app.SearchUsersInChannel(props.InChannelId, props.Term, searchOptions, c.IsSystemAdmin())
|
||||
} else if props.NotInChannelId != "" {
|
||||
profiles, err = app.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions, c.IsSystemAdmin())
|
||||
} else {
|
||||
profiles, err = app.SearchUsersInTeam(props.TeamId, props.Term, searchOptions, c.IsSystemAdmin())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if profiles, err := app.SearchUsers(props, searchOptions, c.IsSystemAdmin()); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
w.Write([]byte(model.UserListToJson(profiles)))
|
||||
}
|
||||
|
||||
w.Write([]byte(model.UserListToJson(profiles)))
|
||||
}
|
||||
|
||||
func getProfilesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -2440,6 +2440,59 @@ func TestSearchUsers(t *testing.T) {
|
||||
}
|
||||
|
||||
if _, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username, NotInChannelId: th.BasicChannel.Id}); err == nil {
|
||||
t.Fatal("should not have access")
|
||||
}
|
||||
|
||||
userWithoutTeam := th.CreateUser(Client)
|
||||
if result, err := Client.SearchUsers(model.UserSearch{Term: userWithoutTeam.Username}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
users := result.Data.([]*model.User)
|
||||
|
||||
found := false
|
||||
for _, user := range users {
|
||||
if user.Id == userWithoutTeam.Id {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatal("should have found user without team")
|
||||
}
|
||||
}
|
||||
|
||||
if result, err := Client.SearchUsers(model.UserSearch{Term: userWithoutTeam.Username, WithoutTeam: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
users := result.Data.([]*model.User)
|
||||
|
||||
found := false
|
||||
for _, user := range users {
|
||||
if user.Id == userWithoutTeam.Id {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatal("should have found user without team")
|
||||
}
|
||||
}
|
||||
|
||||
if result, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username, WithoutTeam: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
users := result.Data.([]*model.User)
|
||||
|
||||
found := false
|
||||
for _, user := range users {
|
||||
if user.Id == th.BasicUser.Id {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
t.Fatal("should not have found user with team")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
api4/user.go
57
api4/user.go
@@ -21,6 +21,7 @@ func InitUser() {
|
||||
BaseRoutes.Users.Handle("", ApiHandler(createUser)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("", ApiSessionRequired(getUsers)).Methods("GET")
|
||||
BaseRoutes.Users.Handle("/ids", ApiSessionRequired(getUsersByIds)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/search", ApiSessionRequired(searchUsers)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/autocomplete", ApiSessionRequired(autocompleteUsers)).Methods("GET")
|
||||
|
||||
BaseRoutes.User.Handle("", ApiSessionRequired(getUser)).Methods("GET")
|
||||
@@ -334,6 +335,62 @@ func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.UserSearchFromJson(r.Body)
|
||||
if props == nil {
|
||||
c.SetInvalidParam("")
|
||||
return
|
||||
}
|
||||
|
||||
if len(props.Term) == 0 {
|
||||
c.SetInvalidParam("term")
|
||||
return
|
||||
}
|
||||
|
||||
if props.TeamId == "" && props.NotInChannelId != "" {
|
||||
c.SetInvalidParam("team_id")
|
||||
return
|
||||
}
|
||||
|
||||
if props.InChannelId != "" && !app.SessionHasPermissionToChannel(c.Session, props.InChannelId, model.PERMISSION_READ_CHANNEL) {
|
||||
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
|
||||
return
|
||||
}
|
||||
|
||||
if props.NotInChannelId != "" && !app.SessionHasPermissionToChannel(c.Session, props.NotInChannelId, model.PERMISSION_READ_CHANNEL) {
|
||||
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
|
||||
return
|
||||
}
|
||||
|
||||
if props.TeamId != "" && !app.SessionHasPermissionToTeam(c.Session, props.TeamId, model.PERMISSION_VIEW_TEAM) {
|
||||
c.SetPermissionError(model.PERMISSION_VIEW_TEAM)
|
||||
return
|
||||
}
|
||||
|
||||
searchOptions := map[string]bool{}
|
||||
searchOptions[store.USER_SEARCH_OPTION_ALLOW_INACTIVE] = props.AllowInactive
|
||||
|
||||
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
|
||||
hideFullName := !utils.Cfg.PrivacySettings.ShowFullName
|
||||
hideEmail := !utils.Cfg.PrivacySettings.ShowEmailAddress
|
||||
|
||||
if hideFullName && hideEmail {
|
||||
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true
|
||||
} else if hideFullName {
|
||||
searchOptions[store.USER_SEARCH_OPTION_ALL_NO_FULL_NAME] = true
|
||||
} else if hideEmail {
|
||||
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true
|
||||
}
|
||||
}
|
||||
|
||||
if profiles, err := app.SearchUsers(props, searchOptions, c.IsSystemAdmin()); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
w.Write([]byte(model.UserListToJson(profiles)))
|
||||
}
|
||||
}
|
||||
|
||||
func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
channelId := r.URL.Query().Get("in_channel")
|
||||
teamId := r.URL.Query().Get("in_team")
|
||||
|
||||
@@ -284,6 +284,156 @@ func TestGetUserByEmail(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchUsers(t *testing.T) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
defer TearDown()
|
||||
Client := th.Client
|
||||
|
||||
search := &model.UserSearch{Term: th.BasicUser.Username}
|
||||
|
||||
users, resp := Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if !findUserInList(th.BasicUser.Id, users) {
|
||||
t.Fatal("should have found user")
|
||||
}
|
||||
|
||||
_, err := app.UpdateActiveNoLdap(th.BasicUser2.Id, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
search.Term = th.BasicUser2.Username
|
||||
search.AllowInactive = false
|
||||
|
||||
users, resp = Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if findUserInList(th.BasicUser2.Id, users) {
|
||||
t.Fatal("should not have found user")
|
||||
}
|
||||
|
||||
search.AllowInactive = true
|
||||
|
||||
users, resp = Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if !findUserInList(th.BasicUser2.Id, users) {
|
||||
t.Fatal("should have found user")
|
||||
}
|
||||
|
||||
search.Term = th.BasicUser.Username
|
||||
search.AllowInactive = false
|
||||
search.TeamId = th.BasicTeam.Id
|
||||
|
||||
users, resp = Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if !findUserInList(th.BasicUser.Id, users) {
|
||||
t.Fatal("should have found user")
|
||||
}
|
||||
|
||||
search.NotInChannelId = th.BasicChannel.Id
|
||||
|
||||
users, resp = Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if findUserInList(th.BasicUser.Id, users) {
|
||||
t.Fatal("should not have found user")
|
||||
}
|
||||
|
||||
search.TeamId = ""
|
||||
search.NotInChannelId = ""
|
||||
search.InChannelId = th.BasicChannel.Id
|
||||
|
||||
users, resp = Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if !findUserInList(th.BasicUser.Id, users) {
|
||||
t.Fatal("should have found user")
|
||||
}
|
||||
|
||||
search.InChannelId = ""
|
||||
search.NotInChannelId = th.BasicChannel.Id
|
||||
_, resp = Client.SearchUsers(search)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
|
||||
search.NotInChannelId = model.NewId()
|
||||
search.TeamId = model.NewId()
|
||||
_, resp = Client.SearchUsers(search)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
search.NotInChannelId = ""
|
||||
search.TeamId = model.NewId()
|
||||
_, resp = Client.SearchUsers(search)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
search.InChannelId = model.NewId()
|
||||
search.TeamId = ""
|
||||
_, resp = Client.SearchUsers(search)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
emailPrivacy := utils.Cfg.PrivacySettings.ShowEmailAddress
|
||||
namePrivacy := utils.Cfg.PrivacySettings.ShowFullName
|
||||
defer func() {
|
||||
utils.Cfg.PrivacySettings.ShowEmailAddress = emailPrivacy
|
||||
utils.Cfg.PrivacySettings.ShowFullName = namePrivacy
|
||||
}()
|
||||
utils.Cfg.PrivacySettings.ShowEmailAddress = false
|
||||
utils.Cfg.PrivacySettings.ShowFullName = false
|
||||
|
||||
_, err = app.UpdateActiveNoLdap(th.BasicUser2.Id, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
search.InChannelId = ""
|
||||
search.Term = th.BasicUser2.Email
|
||||
users, resp = Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if findUserInList(th.BasicUser2.Id, users) {
|
||||
t.Fatal("should not have found user")
|
||||
}
|
||||
|
||||
search.Term = th.BasicUser2.FirstName
|
||||
users, resp = Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if findUserInList(th.BasicUser2.Id, users) {
|
||||
t.Fatal("should not have found user")
|
||||
}
|
||||
|
||||
search.Term = th.BasicUser2.LastName
|
||||
users, resp = Client.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if findUserInList(th.BasicUser2.Id, users) {
|
||||
t.Fatal("should not have found user")
|
||||
}
|
||||
|
||||
search.Term = th.BasicUser.FirstName
|
||||
search.InChannelId = th.BasicChannel.Id
|
||||
search.NotInChannelId = th.BasicChannel.Id
|
||||
search.TeamId = th.BasicTeam.Id
|
||||
users, resp = th.SystemAdminClient.SearchUsers(search)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if !findUserInList(th.BasicUser.Id, users) {
|
||||
t.Fatal("should have found user")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findUserInList(id string, users []*model.User) bool {
|
||||
for _, user := range users {
|
||||
if user.Id == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestAutocompleteUsers(t *testing.T) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
defer TearDown()
|
||||
|
||||
26
app/user.go
26
app/user.go
@@ -1211,6 +1211,18 @@ func VerifyUserEmail(userId string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SearchUsers(props *model.UserSearch, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) {
|
||||
if props.WithoutTeam {
|
||||
return SearchUsersWithoutTeam(props.Term, searchOptions, asAdmin)
|
||||
} else if props.InChannelId != "" {
|
||||
return SearchUsersInChannel(props.InChannelId, props.Term, searchOptions, asAdmin)
|
||||
} else if props.NotInChannelId != "" {
|
||||
return SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions, asAdmin)
|
||||
} else {
|
||||
return SearchUsersInTeam(props.TeamId, props.Term, searchOptions, asAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
func SearchUsersInChannel(channelId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().SearchInChannel(channelId, term, searchOptions); result.Err != nil {
|
||||
return nil, result.Err
|
||||
@@ -1253,6 +1265,20 @@ func SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool
|
||||
}
|
||||
}
|
||||
|
||||
func SearchUsersWithoutTeam(term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().SearchWithoutTeam(term, searchOptions); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
users := result.Data.([]*model.User)
|
||||
|
||||
for _, user := range users {
|
||||
SanitizeProfile(user, asAdmin)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
}
|
||||
|
||||
func AutocompleteUsersInChannel(teamId string, channelId string, term string, searchOptions map[string]bool, asAdmin bool) (*model.UserAutocompleteInChannel, *model.AppError) {
|
||||
uchan := Srv.Store.User().SearchInChannel(channelId, term, searchOptions)
|
||||
nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions)
|
||||
|
||||
@@ -487,6 +487,16 @@ func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response) {
|
||||
}
|
||||
}
|
||||
|
||||
// SearchUsers returns a list of users based on some search criteria.
|
||||
func (c *Client4) SearchUsers(search *UserSearch) ([]*User, *Response) {
|
||||
if r, err := c.DoApiPost(c.GetUsersRoute()+"/search", search.ToJson()); err != nil {
|
||||
return nil, &Response{StatusCode: r.StatusCode, Error: err}
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return UserListFromJson(r.Body), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUser updates a user in the system based on the provided user struct.
|
||||
func (c *Client4) UpdateUser(user *User) (*User, *Response) {
|
||||
if r, err := c.DoApiPut(c.GetUserRoute(user.Id), user.ToJson()); err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ type UserSearch struct {
|
||||
InChannelId string `json:"in_channel_id"`
|
||||
NotInChannelId string `json:"not_in_channel_id"`
|
||||
AllowInactive bool `json:"allow_inactive"`
|
||||
WithoutTeam bool `json:"without_team"`
|
||||
}
|
||||
|
||||
// ToJson convert a User to a json string
|
||||
|
||||
@@ -1261,6 +1261,36 @@ func (us SqlUserStore) Search(teamId string, term string, options map[string]boo
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (us SqlUserStore) SearchWithoutTeam(term string, options map[string]bool) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
searchQuery := `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
Users
|
||||
WHERE
|
||||
(SELECT
|
||||
COUNT(0)
|
||||
FROM
|
||||
TeamMembers
|
||||
WHERE
|
||||
TeamMembers.UserId = Users.Id
|
||||
AND TeamMembers.DeleteAt = 0) = 0
|
||||
SEARCH_CLAUSE
|
||||
INACTIVE_CLAUSE
|
||||
ORDER BY Username ASC
|
||||
LIMIT 100`
|
||||
|
||||
storeChannel <- us.performSearch(searchQuery, term, options, map[string]interface{}{})
|
||||
close(storeChannel)
|
||||
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
|
||||
@@ -1505,6 +1505,67 @@ func TestUserStoreSearch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserStoreSearchWithoutTeam(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
u1 := &model.User{}
|
||||
u1.Username = "jimbo" + model.NewId()
|
||||
u1.FirstName = "Tim"
|
||||
u1.LastName = "Bill"
|
||||
u1.Nickname = "Rob"
|
||||
u1.Email = "harold" + model.NewId() + "@simulator.amazonses.com"
|
||||
Must(store.User().Save(u1))
|
||||
|
||||
u2 := &model.User{}
|
||||
u2.Username = "jim-bobby" + model.NewId()
|
||||
u2.Email = model.NewId()
|
||||
Must(store.User().Save(u2))
|
||||
|
||||
u3 := &model.User{}
|
||||
u3.Username = "jimbo" + model.NewId()
|
||||
u3.Email = model.NewId()
|
||||
u3.DeleteAt = 1
|
||||
Must(store.User().Save(u3))
|
||||
|
||||
tid := model.NewId()
|
||||
Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}))
|
||||
|
||||
searchOptions := map[string]bool{}
|
||||
searchOptions[USER_SEARCH_OPTION_NAMES_ONLY] = true
|
||||
|
||||
if r1 := <-store.User().SearchWithoutTeam("", searchOptions); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
}
|
||||
|
||||
if r1 := <-store.User().SearchWithoutTeam("jim", searchOptions); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
profiles := r1.Data.([]*model.User)
|
||||
|
||||
found1 := false
|
||||
found2 := false
|
||||
found3 := false
|
||||
|
||||
for _, profile := range profiles {
|
||||
if profile.Id == u1.Id {
|
||||
found1 = true
|
||||
} else if profile.Id == u2.Id {
|
||||
found2 = true
|
||||
} else if profile.Id == u3.Id {
|
||||
found3 = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found1 {
|
||||
t.Fatal("should have found user1")
|
||||
} else if !found2 {
|
||||
t.Fatal("should have found user2")
|
||||
} else if found3 {
|
||||
t.Fatal("should not have found user3")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserStoreAnalyticsGetInactiveUsersCount(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@ type UserStore interface {
|
||||
Search(teamId string, term string, options map[string]bool) StoreChannel
|
||||
SearchInChannel(channelId string, term string, options map[string]bool) StoreChannel
|
||||
SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) StoreChannel
|
||||
SearchWithoutTeam(term string, options map[string]bool) StoreChannel
|
||||
AnalyticsGetInactiveUsersCount() StoreChannel
|
||||
AnalyticsGetSystemAdminCount() StoreChannel
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user