Skip to content

Commit bfbc38f

Browse files
Add sorting/filtering to admin user search API endpoint (#36112)
1 parent d2a372f commit bfbc38f

File tree

3 files changed

+176
-8
lines changed

3 files changed

+176
-8
lines changed

models/user/search.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@ import (
1818
"xorm.io/xorm"
1919
)
2020

21+
// AdminUserOrderByMap represents all possible admin user search orders
22+
// This should only be used for admin API endpoints as we should not expose "updated" ordering which could expose recent user activity including logins.
23+
var AdminUserOrderByMap = map[string]map[string]db.SearchOrderBy{
24+
"asc": {
25+
"name": db.SearchOrderByAlphabetically,
26+
"created": db.SearchOrderByOldest,
27+
"updated": db.SearchOrderByLeastUpdated,
28+
"id": db.SearchOrderByID,
29+
},
30+
"desc": {
31+
"name": db.SearchOrderByAlphabeticallyReverse,
32+
"created": db.SearchOrderByNewest,
33+
"updated": db.SearchOrderByRecentUpdated,
34+
"id": db.SearchOrderByIDReverse,
35+
},
36+
}
37+
2138
// SearchUserOptions contains the options for searching
2239
type SearchUserOptions struct {
2340
db.ListOptions

routers/api/v1/admin/user.go

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -414,22 +414,116 @@ func SearchUsers(ctx *context.APIContext) {
414414
// in: query
415415
// description: page size of results
416416
// type: integer
417+
// - name: sort
418+
// in: query
419+
// description: sort users by attribute. Supported values are
420+
// "name", "created", "updated" and "id".
421+
// Default is "name"
422+
// type: string
423+
// - name: order
424+
// in: query
425+
// description: sort order, either "asc" (ascending) or "desc" (descending).
426+
// Default is "asc", ignored if "sort" is not specified.
427+
// type: string
428+
// - name: q
429+
// in: query
430+
// description: search term (username, full name, email)
431+
// type: string
432+
// - name: visibility
433+
// in: query
434+
// description: visibility filter. Supported values are
435+
// "public", "limited" and "private".
436+
// type: string
437+
// - name: is_active
438+
// in: query
439+
// description: filter active users
440+
// type: boolean
441+
// - name: is_admin
442+
// in: query
443+
// description: filter admin users
444+
// type: boolean
445+
// - name: is_restricted
446+
// in: query
447+
// description: filter restricted users
448+
// type: boolean
449+
// - name: is_2fa_enabled
450+
// in: query
451+
// description: filter 2FA enabled users
452+
// type: boolean
453+
// - name: is_prohibit_login
454+
// in: query
455+
// description: filter login prohibited users
456+
// type: boolean
417457
// responses:
418458
// "200":
419459
// "$ref": "#/responses/UserList"
420460
// "403":
421461
// "$ref": "#/responses/forbidden"
462+
// "422":
463+
// "$ref": "#/responses/validationError"
422464

423465
listOptions := utils.GetListOptions(ctx)
424466

425-
users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
426-
Actor: ctx.Doer,
427-
Types: []user_model.UserType{user_model.UserTypeIndividual},
428-
LoginName: ctx.FormTrim("login_name"),
429-
SourceID: ctx.FormInt64("source_id"),
430-
OrderBy: db.SearchOrderByAlphabetically,
431-
ListOptions: listOptions,
432-
})
467+
orderBy := db.SearchOrderByAlphabetically
468+
sortMode := ctx.FormString("sort")
469+
if len(sortMode) > 0 {
470+
sortOrder := ctx.FormString("order")
471+
if len(sortOrder) == 0 {
472+
sortOrder = "asc"
473+
}
474+
if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok {
475+
if order, ok := searchModeMap[sortMode]; ok {
476+
orderBy = order
477+
} else {
478+
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
479+
return
480+
}
481+
} else {
482+
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
483+
return
484+
}
485+
}
486+
487+
var visible []api.VisibleType
488+
visibilityParam := ctx.FormString("visibility")
489+
if len(visibilityParam) > 0 {
490+
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
491+
visible = []api.VisibleType{visibility}
492+
} else {
493+
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam))
494+
return
495+
}
496+
}
497+
498+
searchOpts := user_model.SearchUserOptions{
499+
Actor: ctx.Doer,
500+
Types: []user_model.UserType{user_model.UserTypeIndividual},
501+
LoginName: ctx.FormTrim("login_name"),
502+
SourceID: ctx.FormInt64("source_id"),
503+
Keyword: ctx.FormTrim("q"),
504+
Visible: visible,
505+
OrderBy: orderBy,
506+
ListOptions: listOptions,
507+
SearchByEmail: true,
508+
}
509+
510+
if ctx.FormString("is_active") != "" {
511+
searchOpts.IsActive = optional.Some(ctx.FormBool("is_active"))
512+
}
513+
if ctx.FormString("is_admin") != "" {
514+
searchOpts.IsAdmin = optional.Some(ctx.FormBool("is_admin"))
515+
}
516+
if ctx.FormString("is_restricted") != "" {
517+
searchOpts.IsRestricted = optional.Some(ctx.FormBool("is_restricted"))
518+
}
519+
if ctx.FormString("is_2fa_enabled") != "" {
520+
searchOpts.IsTwoFactorEnabled = optional.Some(ctx.FormBool("is_2fa_enabled"))
521+
}
522+
if ctx.FormString("is_prohibit_login") != "" {
523+
searchOpts.IsProhibitLogin = optional.Some(ctx.FormBool("is_prohibit_login"))
524+
}
525+
526+
users, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
433527
if err != nil {
434528
ctx.APIErrorInternal(err)
435529
return

templates/swagger/v1_json.tmpl

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)