mirror of https://github.com/coder/coder.git
feat: Backend api for filtering users using filter query string (#2553)
* User search query string
This commit is contained in:
parent
981fb2764f
commit
d21ab2115d
|
@ -338,10 +338,11 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"POST:/api/v2/users/{user}/organizations/": {
|
||||
"POST:/api/v2/users/{user}/organizations": {
|
||||
AssertAction: rbac.ActionCreate,
|
||||
AssertObject: rbac.ResourceOrganization,
|
||||
},
|
||||
"GET:/api/v2/users": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceUser},
|
||||
|
||||
// These endpoints need payloads to get to the auth part. Payloads will be required
|
||||
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
|
|
|
@ -285,19 +285,25 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
|
|||
users = tmp
|
||||
}
|
||||
|
||||
if len(params.Status) == 0 {
|
||||
params.Status = []database.UserStatus{database.UserStatusActive}
|
||||
}
|
||||
|
||||
usersFilteredByStatus := make([]database.User, 0, len(users))
|
||||
for i, user := range users {
|
||||
for _, status := range params.Status {
|
||||
if user.Status == status {
|
||||
if len(params.Status) > 0 {
|
||||
usersFilteredByStatus := make([]database.User, 0, len(users))
|
||||
for i, user := range users {
|
||||
if slice.Contains(params.Status, user.Status) {
|
||||
usersFilteredByStatus = append(usersFilteredByStatus, users[i])
|
||||
}
|
||||
}
|
||||
users = usersFilteredByStatus
|
||||
}
|
||||
|
||||
if len(params.RbacRole) > 0 {
|
||||
usersFilteredByRole := make([]database.User, 0, len(users))
|
||||
for i, user := range users {
|
||||
if slice.Overlap(params.RbacRole, user.RBACRoles) {
|
||||
usersFilteredByRole = append(usersFilteredByRole, users[i])
|
||||
}
|
||||
}
|
||||
users = usersFilteredByRole
|
||||
}
|
||||
users = usersFilteredByStatus
|
||||
|
||||
if params.OffsetOpt > 0 {
|
||||
if int(params.OffsetOpt) > len(users)-1 {
|
||||
|
|
|
@ -30,3 +30,10 @@ func (d ProvisionerDaemon) RBACObject() rbac.Object {
|
|||
func (f File) RBACObject() rbac.Object {
|
||||
return rbac.ResourceFile.WithID(f.Hash).WithOwner(f.CreatedBy.String())
|
||||
}
|
||||
|
||||
// RBACObject returns the RBAC object for the site wide user resource.
|
||||
// If you are trying to get the RBAC object for the UserData, use
|
||||
// rbac.ResourceUserData
|
||||
func (u User) RBACObject() rbac.Object {
|
||||
return rbac.ResourceUser.WithID(u.ID.String())
|
||||
}
|
||||
|
|
|
@ -2571,27 +2571,33 @@ WHERE
|
|||
AND CASE
|
||||
-- @status needs to be a text because it can be empty, If it was
|
||||
-- user_status enum, it would not.
|
||||
WHEN cardinality($3 :: user_status[]) > 0 THEN (
|
||||
WHEN cardinality($3 :: user_status[]) > 0 THEN
|
||||
status = ANY($3 :: user_status[])
|
||||
)
|
||||
ELSE
|
||||
-- Only show active by default
|
||||
status = 'active'
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by rbac_roles
|
||||
AND CASE
|
||||
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
|
||||
-- everyone is a member.
|
||||
WHEN cardinality($4 :: text[]) > 0 AND 'member' != ANY($4 :: text[]) THEN
|
||||
rbac_roles && $4 :: text[]
|
||||
ELSE true
|
||||
END
|
||||
-- End of filters
|
||||
ORDER BY
|
||||
-- Deterministic and consistent ordering of all users, even if they share
|
||||
-- a timestamp. This is to ensure consistent pagination.
|
||||
(created_at, id) ASC OFFSET $4
|
||||
(created_at, id) ASC OFFSET $5
|
||||
LIMIT
|
||||
-- A null limit means "no limit", so -1 means return all
|
||||
NULLIF($5 :: int, -1)
|
||||
NULLIF($6 :: int, -1)
|
||||
`
|
||||
|
||||
type GetUsersParams struct {
|
||||
AfterID uuid.UUID `db:"after_id" json:"after_id"`
|
||||
Search string `db:"search" json:"search"`
|
||||
Status []UserStatus `db:"status" json:"status"`
|
||||
RbacRole []string `db:"rbac_role" json:"rbac_role"`
|
||||
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
}
|
||||
|
@ -2601,6 +2607,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User,
|
|||
arg.AfterID,
|
||||
arg.Search,
|
||||
pq.Array(arg.Status),
|
||||
pq.Array(arg.RbacRole),
|
||||
arg.OffsetOpt,
|
||||
arg.LimitOpt,
|
||||
)
|
||||
|
|
|
@ -108,12 +108,17 @@ WHERE
|
|||
AND CASE
|
||||
-- @status needs to be a text because it can be empty, If it was
|
||||
-- user_status enum, it would not.
|
||||
WHEN cardinality(@status :: user_status[]) > 0 THEN (
|
||||
WHEN cardinality(@status :: user_status[]) > 0 THEN
|
||||
status = ANY(@status :: user_status[])
|
||||
)
|
||||
ELSE
|
||||
-- Only show active by default
|
||||
status = 'active'
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by rbac_roles
|
||||
AND CASE
|
||||
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
|
||||
-- everyone is a member.
|
||||
WHEN cardinality(@rbac_role :: text[]) > 0 AND 'member' != ANY(@rbac_role :: text[]) THEN
|
||||
rbac_roles && @rbac_role :: text[]
|
||||
ELSE true
|
||||
END
|
||||
-- End of filters
|
||||
ORDER BY
|
||||
|
|
|
@ -83,14 +83,31 @@ func (p *QueryParamParser) UUIDs(vals url.Values, def []uuid.UUID, queryParam st
|
|||
return v
|
||||
}
|
||||
|
||||
func (p *QueryParamParser) String(vals url.Values, def string, queryParam string) string {
|
||||
v, err := parseQueryParam(vals, func(v string) (string, error) {
|
||||
func (*QueryParamParser) String(vals url.Values, def string, queryParam string) string {
|
||||
v, _ := parseQueryParam(vals, func(v string) (string, error) {
|
||||
return v, nil
|
||||
}, def, queryParam)
|
||||
return v
|
||||
}
|
||||
|
||||
func (*QueryParamParser) Strings(vals url.Values, def []string, queryParam string) []string {
|
||||
v, _ := parseQueryParam(vals, func(v string) ([]string, error) {
|
||||
if v == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
return strings.Split(v, ","), nil
|
||||
}, def, queryParam)
|
||||
return v
|
||||
}
|
||||
|
||||
// ParseCustom has to be a function, not a method on QueryParamParser because generics
|
||||
// cannot be used on struct methods.
|
||||
func ParseCustom[T any](parser *QueryParamParser, vals url.Values, def T, queryParam string, parseFunc func(v string) (T, error)) T {
|
||||
v, err := parseQueryParam(vals, parseFunc, def, queryParam)
|
||||
if err != nil {
|
||||
p.Errors = append(p.Errors, Error{
|
||||
parser.Errors = append(parser.Errors, Error{
|
||||
Field: queryParam,
|
||||
Detail: fmt.Sprintf("Query param %q must be a valid string", queryParam),
|
||||
Detail: fmt.Sprintf("Query param %q has invalid uuids: %q", queryParam, err.Error()),
|
||||
})
|
||||
}
|
||||
return v
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -119,35 +120,13 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
searchName = r.URL.Query().Get("search")
|
||||
statusFilters = r.URL.Query().Get("status")
|
||||
)
|
||||
|
||||
statuses := make([]database.UserStatus, 0)
|
||||
|
||||
if statusFilters != "" {
|
||||
// Split on commas if present to account for it being a list
|
||||
for _, filter := range strings.Split(statusFilters, ",") {
|
||||
switch database.UserStatus(filter) {
|
||||
case database.UserStatusSuspended, database.UserStatusActive:
|
||||
statuses = append(statuses, database.UserStatus(filter))
|
||||
default:
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("%q is not a valid user status.", filter),
|
||||
Validations: []httpapi.Error{
|
||||
{Field: "status", Detail: "invalid status"},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reading all users across the site.
|
||||
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceUser) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
query := r.URL.Query().Get("q")
|
||||
params, errs := userSearchQuery(query)
|
||||
if len(errs) > 0 {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "Invalid user search query.",
|
||||
Validations: errs,
|
||||
})
|
||||
}
|
||||
|
||||
paginationParams, ok := parsePagination(rw, r)
|
||||
|
@ -159,8 +138,9 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
|||
AfterID: paginationParams.AfterID,
|
||||
OffsetOpt: int32(paginationParams.Offset),
|
||||
LimitOpt: int32(paginationParams.Limit),
|
||||
Search: searchName,
|
||||
Status: statuses,
|
||||
Search: params.Search,
|
||||
Status: params.Status,
|
||||
RbacRole: params.RbacRole,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusOK, []codersdk.User{})
|
||||
|
@ -174,6 +154,7 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
users = AuthorizeFilter(api, r, rbac.ActionRead, users)
|
||||
userIDs := make([]uuid.UUID, 0, len(users))
|
||||
for _, user := range users {
|
||||
userIDs = append(userIDs, user.ID)
|
||||
|
@ -971,3 +952,56 @@ func findUser(id uuid.UUID, users []database.User) *database.User {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func userSearchQuery(query string) (database.GetUsersParams, []httpapi.Error) {
|
||||
searchParams := make(url.Values)
|
||||
if query == "" {
|
||||
// No filter
|
||||
return database.GetUsersParams{}, nil
|
||||
}
|
||||
// Because we do this in 2 passes, we want to maintain quotes on the first
|
||||
// pass.Further splitting occurs on the second pass and quotes will be
|
||||
// dropped.
|
||||
elements := splitQueryParameterByDelimiter(query, ' ', true)
|
||||
for _, element := range elements {
|
||||
parts := splitQueryParameterByDelimiter(element, ':', false)
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
// No key:value pair.
|
||||
searchParams.Set("search", parts[0])
|
||||
case 2:
|
||||
searchParams.Set(parts[0], parts[1])
|
||||
default:
|
||||
return database.GetUsersParams{}, []httpapi.Error{
|
||||
{Field: "q", Detail: fmt.Sprintf("Query element %q can only contain 1 ':'", element)},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parser := httpapi.NewQueryParamParser()
|
||||
filter := database.GetUsersParams{
|
||||
Search: parser.String(searchParams, "", "search"),
|
||||
Status: httpapi.ParseCustom(parser, searchParams, []database.UserStatus{}, "status", parseUserStatus),
|
||||
RbacRole: parser.Strings(searchParams, []string{}, "role"),
|
||||
}
|
||||
|
||||
return filter, parser.Errors
|
||||
}
|
||||
|
||||
// parseUserStatus ensures proper enums are used for user statuses
|
||||
func parseUserStatus(v string) ([]database.UserStatus, error) {
|
||||
var statuses []database.UserStatus
|
||||
if v == "" {
|
||||
return statuses, nil
|
||||
}
|
||||
parts := strings.Split(v, ",")
|
||||
for _, part := range parts {
|
||||
switch database.UserStatus(part) {
|
||||
case database.UserStatusActive, database.UserStatusSuspended:
|
||||
statuses = append(statuses, database.UserStatus(part))
|
||||
default:
|
||||
return []database.UserStatus{}, xerrors.Errorf("%q is not a valid user status", part)
|
||||
}
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
|
|
@ -704,6 +704,131 @@ func TestGetUser(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestUsersFilter creates a set of users to run various filters against for testing.
|
||||
func TestUsersFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
firstUser, err := client.User(context.Background(), codersdk.Me)
|
||||
require.NoError(t, err, "fetch me")
|
||||
|
||||
users := make([]codersdk.User, 0)
|
||||
users = append(users, firstUser)
|
||||
for i := 0; i < 15; i++ {
|
||||
roles := []string{}
|
||||
if i%2 == 0 {
|
||||
roles = append(roles, rbac.RoleAdmin())
|
||||
}
|
||||
if i%3 == 0 {
|
||||
roles = append(roles, "auditor")
|
||||
}
|
||||
userClient := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, roles...)
|
||||
user, err := userClient.User(context.Background(), codersdk.Me)
|
||||
require.NoError(t, err, "fetch me")
|
||||
|
||||
if i%4 == 0 {
|
||||
user, err = client.UpdateUserStatus(context.Background(), user.ID.String(), codersdk.UserStatusSuspended)
|
||||
require.NoError(t, err, "suspend user")
|
||||
}
|
||||
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
// --- Setup done ---
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Filter codersdk.UsersRequest
|
||||
// If FilterF is true, we include it in the expected results
|
||||
FilterF func(f codersdk.UsersRequest, user codersdk.User) bool
|
||||
}{
|
||||
{
|
||||
Name: "All",
|
||||
Filter: codersdk.UsersRequest{
|
||||
Status: codersdk.UserStatusSuspended + "," + codersdk.UserStatusActive,
|
||||
},
|
||||
FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Active",
|
||||
Filter: codersdk.UsersRequest{
|
||||
Status: codersdk.UserStatusActive,
|
||||
},
|
||||
FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
|
||||
return u.Status == codersdk.UserStatusActive
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Suspended",
|
||||
Filter: codersdk.UsersRequest{
|
||||
Status: codersdk.UserStatusSuspended,
|
||||
},
|
||||
FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
|
||||
return u.Status == codersdk.UserStatusSuspended
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "NameContains",
|
||||
Filter: codersdk.UsersRequest{
|
||||
Search: "a",
|
||||
},
|
||||
FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
|
||||
return (strings.Contains(u.Username, "a") || strings.Contains(u.Email, "a"))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Admins",
|
||||
Filter: codersdk.UsersRequest{
|
||||
Role: rbac.RoleAdmin(),
|
||||
Status: codersdk.UserStatusSuspended + "," + codersdk.UserStatusActive,
|
||||
},
|
||||
FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
|
||||
for _, r := range u.Roles {
|
||||
if r.Name == rbac.RoleAdmin() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SearchQuery",
|
||||
Filter: codersdk.UsersRequest{
|
||||
SearchQuery: "i role:admin status:active",
|
||||
},
|
||||
FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
|
||||
for _, r := range u.Roles {
|
||||
if r.Name == rbac.RoleAdmin() {
|
||||
return (strings.Contains(u.Username, "i") || strings.Contains(u.Email, "i")) &&
|
||||
u.Status == codersdk.UserStatusActive
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
matched, err := client.Users(context.Background(), c.Filter)
|
||||
require.NoError(t, err, "fetch workspaces")
|
||||
|
||||
exp := make([]codersdk.User, 0)
|
||||
for _, made := range users {
|
||||
match := c.FilterF(c.Filter, made)
|
||||
if match {
|
||||
exp = append(exp, made)
|
||||
}
|
||||
}
|
||||
require.ElementsMatch(t, exp, matched, "expected workspaces returned")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUsers(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("AllUsers", func(t *testing.T) {
|
||||
|
@ -754,7 +879,7 @@ func TestGetUsers(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
users, err := client.Users(context.Background(), codersdk.UsersRequest{
|
||||
Status: string(codersdk.UserStatusActive),
|
||||
Status: codersdk.UserStatusActive,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, active, users)
|
||||
|
|
|
@ -8,3 +8,15 @@ func Contains[T comparable](haystack []T, needle T) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Overlap returns if the 2 sets have any overlap (element(s) in common)
|
||||
func Overlap[T comparable](a []T, b []T) bool {
|
||||
// For each element in b, if at least 1 is contained in 'a',
|
||||
// return true.
|
||||
for _, element := range b {
|
||||
if Contains(a, element) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -21,6 +21,35 @@ func TestContains(t *testing.T) {
|
|||
)
|
||||
}
|
||||
|
||||
func TestOverlap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assertSetOverlaps(t, true, []int{1, 2, 3, 4, 5}, []int{1, 2, 3, 4, 5})
|
||||
assertSetOverlaps(t, true, []int{10}, []int{10})
|
||||
|
||||
assertSetOverlaps(t, false, []int{1, 2, 3, 4, 5}, []int{6, 7, 8, 9})
|
||||
assertSetOverlaps(t, false, []int{1, 2, 3, 4, 5}, []int{})
|
||||
assertSetOverlaps(t, false, []int{}, []int{})
|
||||
|
||||
assertSetOverlaps(t, true, []string{"hello", "world", "foo", "bar", "baz"}, []string{"hello", "world", "baz"})
|
||||
assertSetOverlaps(t, true,
|
||||
[]uuid.UUID{uuid.New(), uuid.MustParse("c7c6686d-a93c-4df2-bef9-5f837e9a33d5"), uuid.MustParse("8f3b3e0b-2c3f-46a5-a365-fd5b62bd8818")},
|
||||
[]uuid.UUID{uuid.MustParse("c7c6686d-a93c-4df2-bef9-5f837e9a33d5")},
|
||||
)
|
||||
}
|
||||
|
||||
func assertSetOverlaps[T comparable](t *testing.T, overlap bool, a []T, b []T) {
|
||||
t.Helper()
|
||||
for _, e := range a {
|
||||
require.True(t, slice.Overlap(a, []T{e}), "elements in set should overlap with itself")
|
||||
}
|
||||
for _, e := range b {
|
||||
require.True(t, slice.Overlap(b, []T{e}), "elements in set should overlap with itself")
|
||||
}
|
||||
|
||||
require.Equal(t, overlap, slice.Overlap(a, b))
|
||||
}
|
||||
|
||||
func assertSetContains[T comparable](t *testing.T, set []T, in []T, out []T) {
|
||||
t.Helper()
|
||||
for _, e := range set {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
@ -22,9 +23,13 @@ const (
|
|||
)
|
||||
|
||||
type UsersRequest struct {
|
||||
Search string `json:"search,omitempty"`
|
||||
// Filter users by status
|
||||
Status string `json:"status,omitempty"`
|
||||
Search string `json:"search,omitempty" typescript:"-"`
|
||||
// Filter users by status.
|
||||
Status UserStatus `json:"status,omitempty" typescript:"-"`
|
||||
// Filter users that have the given role.
|
||||
Role string `json:"role,omitempty" typescript:"-"`
|
||||
|
||||
SearchQuery string `json:"q,omitempty"`
|
||||
Pagination
|
||||
}
|
||||
|
||||
|
@ -362,8 +367,20 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) {
|
|||
req.Pagination.asRequestOption(),
|
||||
func(r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
q.Set("search", req.Search)
|
||||
q.Set("status", req.Status)
|
||||
var params []string
|
||||
if req.Search != "" {
|
||||
params = append(params, req.Search)
|
||||
}
|
||||
if req.Status != "" {
|
||||
params = append(params, "status:"+string(req.Status))
|
||||
}
|
||||
if req.Role != "" {
|
||||
params = append(params, "role:"+req.Role)
|
||||
}
|
||||
if req.SearchQuery != "" {
|
||||
params = append(params, req.SearchQuery)
|
||||
}
|
||||
q.Set("q", strings.Join(params, " "))
|
||||
r.URL.RawQuery = q.Encode()
|
||||
},
|
||||
)
|
||||
|
|
|
@ -63,7 +63,7 @@ export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
|
|||
}
|
||||
|
||||
export const getUsers = async (): Promise<TypesGen.User[]> => {
|
||||
const response = await axios.get<TypesGen.User[]>("/api/v2/users?status=active,suspended")
|
||||
const response = await axios.get<TypesGen.User[]>("/api/v2/users?q=status:active,suspended")
|
||||
return response.data
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ export interface AgentGitSSHKey {
|
|||
readonly private_key: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:151:6
|
||||
// From codersdk/users.go:156:6
|
||||
export interface AuthMethods {
|
||||
readonly password: boolean
|
||||
readonly github: boolean
|
||||
|
@ -37,7 +37,7 @@ export interface ComputedParameter extends Parameter {
|
|||
readonly default_source_value: boolean
|
||||
}
|
||||
|
||||
// From codersdk/users.go:42:6
|
||||
// From codersdk/users.go:47:6
|
||||
export interface CreateFirstUserRequest {
|
||||
readonly email: string
|
||||
readonly username: string
|
||||
|
@ -45,13 +45,13 @@ export interface CreateFirstUserRequest {
|
|||
readonly organization: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:50:6
|
||||
// From codersdk/users.go:55:6
|
||||
export interface CreateFirstUserResponse {
|
||||
readonly user_id: string
|
||||
readonly organization_id: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:146:6
|
||||
// From codersdk/users.go:151:6
|
||||
export interface CreateOrganizationRequest {
|
||||
readonly name: string
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ export interface CreateTemplateVersionRequest {
|
|||
readonly parameter_values?: CreateParameterRequest[]
|
||||
}
|
||||
|
||||
// From codersdk/users.go:55:6
|
||||
// From codersdk/users.go:60:6
|
||||
export interface CreateUserRequest {
|
||||
readonly email: string
|
||||
readonly username: string
|
||||
|
@ -115,7 +115,7 @@ export interface CreateWorkspaceRequest {
|
|||
readonly parameter_values?: CreateParameterRequest[]
|
||||
}
|
||||
|
||||
// From codersdk/users.go:142:6
|
||||
// From codersdk/users.go:147:6
|
||||
export interface GenerateAPIKeyResponse {
|
||||
readonly key: string
|
||||
}
|
||||
|
@ -133,13 +133,13 @@ export interface GoogleInstanceIdentityToken {
|
|||
readonly json_web_token: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:131:6
|
||||
// From codersdk/users.go:136:6
|
||||
export interface LoginWithPasswordRequest {
|
||||
readonly email: string
|
||||
readonly password: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:137:6
|
||||
// From codersdk/users.go:142:6
|
||||
export interface LoginWithPasswordResponse {
|
||||
readonly session_token: string
|
||||
}
|
||||
|
@ -282,7 +282,7 @@ export interface UpdateActiveTemplateVersion {
|
|||
readonly id: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:71:6
|
||||
// From codersdk/users.go:76:6
|
||||
export interface UpdateRoles {
|
||||
readonly roles: string[]
|
||||
}
|
||||
|
@ -294,13 +294,13 @@ export interface UpdateTemplateMeta {
|
|||
readonly min_autostart_interval_ms?: number
|
||||
}
|
||||
|
||||
// From codersdk/users.go:66:6
|
||||
// From codersdk/users.go:71:6
|
||||
export interface UpdateUserPasswordRequest {
|
||||
readonly old_password: string
|
||||
readonly password: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:62:6
|
||||
// From codersdk/users.go:67:6
|
||||
export interface UpdateUserProfileRequest {
|
||||
readonly username: string
|
||||
}
|
||||
|
@ -320,7 +320,7 @@ export interface UploadResponse {
|
|||
readonly hash: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:32:6
|
||||
// From codersdk/users.go:37:6
|
||||
export interface User {
|
||||
readonly id: string
|
||||
readonly email: string
|
||||
|
@ -331,13 +331,13 @@ export interface User {
|
|||
readonly roles: Role[]
|
||||
}
|
||||
|
||||
// From codersdk/users.go:96:6
|
||||
// From codersdk/users.go:101:6
|
||||
export interface UserAuthorization {
|
||||
readonly object: UserAuthorizationObject
|
||||
readonly action: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:112:6
|
||||
// From codersdk/users.go:117:6
|
||||
export interface UserAuthorizationObject {
|
||||
readonly resource_type: string
|
||||
readonly owner_id?: string
|
||||
|
@ -345,24 +345,23 @@ export interface UserAuthorizationObject {
|
|||
readonly resource_id?: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:85:6
|
||||
// From codersdk/users.go:90:6
|
||||
export interface UserAuthorizationRequest {
|
||||
readonly checks: Record<string, UserAuthorization>
|
||||
}
|
||||
|
||||
// From codersdk/users.go:80:6
|
||||
// From codersdk/users.go:85:6
|
||||
export type UserAuthorizationResponse = Record<string, boolean>
|
||||
|
||||
// From codersdk/users.go:75:6
|
||||
// From codersdk/users.go:80:6
|
||||
export interface UserRoles {
|
||||
readonly roles: string[]
|
||||
readonly organization_roles: Record<string, string[]>
|
||||
}
|
||||
|
||||
// From codersdk/users.go:24:6
|
||||
// From codersdk/users.go:25:6
|
||||
export interface UsersRequest extends Pagination {
|
||||
readonly search?: string
|
||||
readonly status?: string
|
||||
readonly q?: string
|
||||
}
|
||||
|
||||
// From codersdk/workspaces.go:19:6
|
||||
|
@ -512,7 +511,7 @@ export type ProvisionerStorageMethod = "file"
|
|||
// From codersdk/organizations.go:20:6
|
||||
export type ProvisionerType = "echo" | "terraform"
|
||||
|
||||
// From codersdk/users.go:17:6
|
||||
// From codersdk/users.go:18:6
|
||||
export type UserStatus = "active" | "suspended"
|
||||
|
||||
// From codersdk/workspaceresources.go:13:6
|
||||
|
|
Loading…
Reference in New Issue