mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat: add username and email user search filters (#27922)
## Summary User search can now resolve exact `email:` and `username:` terms through `GET /api/v2/users` instead of only supporting fuzzy free-text matches. The database query already had exact email and username filters; this wires the public search parser and API handler to those filters so clients can ask for a single user by email without fetching every user or depending on substring matching. This is the API half of coder/terraform-provider-coderd#403: that provider PR adds `data.coderd_user.email`, and this PR gives it an efficient exact lookup path. ## Testing - `go test ./coderd/searchquery -run '^TestSearchUsers$' -count=1` - `go test ./coderd -run '^TestGetUsersFilter$' -count=1` - Live API test: - Built local enterprise Coder from this branch. - Started Coder on `http://127.0.0.1:39991` against a clean Postgres database. - Created `lookup-target@example.com`. - Verified `GET /api/v2/users?q=email:LOOKUP-TARGET@EXAMPLE.COM&limit=2` returned exactly one user: ```json { "count": 1, "users": [ { "id": "efc6f909-ce0a-4731-bd2f-6e4df417aaa7", "username": "lookup-target", "email": "lookup-target@example.com" } ] } ``` ---   --------- Co-authored-by: Ethan Dickson <ethanndickson@gmail.com>
This commit is contained in:
co-authored by
Ethan Dickson
parent
af90d8e2be
commit
a005e5cd22
Generated
+84
-52
@@ -14657,74 +14657,86 @@ WHERE
|
||||
user_name ILIKE concat('%', $4, '%')
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by exact username
|
||||
AND CASE
|
||||
WHEN $5 :: text != '' THEN
|
||||
lower(user_username) = lower($5)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by exact email
|
||||
AND CASE
|
||||
WHEN $6 :: text != '' THEN
|
||||
lower(user_email) = lower($6)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by status
|
||||
AND CASE
|
||||
-- @status needs to be a text because it can be empty, If it was
|
||||
-- user_status enum, it would not.
|
||||
WHEN cardinality($5 :: user_status[]) > 0 THEN
|
||||
user_status = ANY($5 :: user_status[])
|
||||
WHEN cardinality($7 :: user_status[]) > 0 THEN
|
||||
user_status = ANY($7 :: user_status[])
|
||||
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($6 :: text[]) > 0 AND 'member' != ANY($6 :: text[]) THEN
|
||||
user_rbac_roles && $6 :: text[]
|
||||
WHEN cardinality($8 :: text[]) > 0 AND 'member' != ANY($8 :: text[]) THEN
|
||||
user_rbac_roles && $8 :: text[]
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by last_seen
|
||||
AND CASE
|
||||
WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_last_seen_at <= $7
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_last_seen_at >= $8
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by created_at
|
||||
AND CASE
|
||||
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_created_at <= $9
|
||||
user_last_seen_at <= $9
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_created_at >= $10
|
||||
user_last_seen_at >= $10
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by created_at
|
||||
AND CASE
|
||||
WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_created_at <= $11
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $12 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_created_at >= $12
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by system type
|
||||
AND CASE
|
||||
WHEN $11::bool THEN TRUE
|
||||
WHEN $13::bool THEN TRUE
|
||||
ELSE user_is_system = false
|
||||
END
|
||||
-- Filter by github.com user ID
|
||||
AND CASE
|
||||
WHEN $12 :: bigint != 0 THEN
|
||||
user_github_com_user_id = $12
|
||||
WHEN $14 :: bigint != 0 THEN
|
||||
user_github_com_user_id = $14
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by login_type
|
||||
AND CASE
|
||||
WHEN cardinality($13 :: login_type[]) > 0 THEN
|
||||
user_login_type = ANY($13 :: login_type[])
|
||||
WHEN cardinality($15 :: login_type[]) > 0 THEN
|
||||
user_login_type = ANY($15 :: login_type[])
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by service account.
|
||||
AND CASE
|
||||
WHEN $14 :: boolean IS NOT NULL THEN
|
||||
user_is_service_account = $14 :: boolean
|
||||
WHEN $16 :: boolean IS NOT NULL THEN
|
||||
user_is_service_account = $16 :: boolean
|
||||
ELSE true
|
||||
END
|
||||
-- End of filters
|
||||
ORDER BY
|
||||
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
|
||||
LOWER(user_username) ASC OFFSET $15
|
||||
LOWER(user_username) ASC OFFSET $17
|
||||
LIMIT
|
||||
-- A null limit means "no limit", so 0 means return all
|
||||
NULLIF($16 :: int, 0)
|
||||
NULLIF($18 :: int, 0)
|
||||
`
|
||||
|
||||
type GetGroupMembersByGroupIDPaginatedParams struct {
|
||||
@@ -14732,6 +14744,8 @@ type GetGroupMembersByGroupIDPaginatedParams struct {
|
||||
AfterID uuid.UUID `db:"after_id" json:"after_id"`
|
||||
Search string `db:"search" json:"search"`
|
||||
Name string `db:"name" json:"name"`
|
||||
ExactUsername string `db:"exact_username" json:"exact_username"`
|
||||
ExactEmail string `db:"exact_email" json:"exact_email"`
|
||||
Status []UserStatus `db:"status" json:"status"`
|
||||
RbacRole []string `db:"rbac_role" json:"rbac_role"`
|
||||
LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"`
|
||||
@@ -14776,6 +14790,8 @@ func (q *sqlQuerier) GetGroupMembersByGroupIDPaginated(ctx context.Context, arg
|
||||
arg.AfterID,
|
||||
arg.Search,
|
||||
arg.Name,
|
||||
arg.ExactUsername,
|
||||
arg.ExactEmail,
|
||||
pq.Array(arg.Status),
|
||||
pq.Array(arg.RbacRole),
|
||||
arg.LastSeenBefore,
|
||||
@@ -20176,74 +20192,86 @@ WHERE
|
||||
users.name ILIKE concat('%', $4, '%')
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by exact username
|
||||
AND CASE
|
||||
WHEN $5 :: text != '' THEN
|
||||
lower(users.username) = lower($5)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by exact email
|
||||
AND CASE
|
||||
WHEN $6 :: text != '' THEN
|
||||
lower(users.email) = lower($6)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by status
|
||||
AND CASE
|
||||
-- @status needs to be a text because it can be empty, If it was
|
||||
-- user_status enum, it would not.
|
||||
WHEN cardinality($5 :: user_status[]) > 0 THEN
|
||||
users.status = ANY($5 :: user_status[])
|
||||
WHEN cardinality($7 :: user_status[]) > 0 THEN
|
||||
users.status = ANY($7 :: user_status[])
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by global rbac_roles
|
||||
AND CASE
|
||||
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
|
||||
-- everyone is a member.
|
||||
WHEN cardinality($6 :: text[]) > 0 AND 'member' != ANY($6 :: text[]) THEN
|
||||
users.rbac_roles && $6 :: text[]
|
||||
WHEN cardinality($8 :: text[]) > 0 AND 'member' != ANY($8 :: text[]) THEN
|
||||
users.rbac_roles && $8 :: text[]
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by last_seen
|
||||
AND CASE
|
||||
WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
users.last_seen_at <= $7
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
users.last_seen_at >= $8
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by created_at (user creation date, not date added to org)
|
||||
AND CASE
|
||||
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
users.created_at <= $9
|
||||
users.last_seen_at <= $9
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
users.created_at >= $10
|
||||
users.last_seen_at >= $10
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by created_at (user creation date, not date added to org)
|
||||
AND CASE
|
||||
WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
users.created_at <= $11
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $12 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
users.created_at >= $12
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by system type
|
||||
AND CASE
|
||||
WHEN $11::bool THEN TRUE
|
||||
WHEN $13::bool THEN TRUE
|
||||
ELSE users.is_system = false
|
||||
END
|
||||
-- Filter by github.com user ID
|
||||
AND CASE
|
||||
WHEN $12 :: bigint != 0 THEN
|
||||
users.github_com_user_id = $12
|
||||
WHEN $14 :: bigint != 0 THEN
|
||||
users.github_com_user_id = $14
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by login_type
|
||||
AND CASE
|
||||
WHEN cardinality($13 :: login_type[]) > 0 THEN
|
||||
users.login_type = ANY($13 :: login_type[])
|
||||
WHEN cardinality($15 :: login_type[]) > 0 THEN
|
||||
users.login_type = ANY($15 :: login_type[])
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by service account.
|
||||
AND CASE
|
||||
WHEN $14 :: boolean IS NOT NULL THEN
|
||||
users.is_service_account = $14 :: boolean
|
||||
WHEN $16 :: boolean IS NOT NULL THEN
|
||||
users.is_service_account = $16 :: boolean
|
||||
ELSE true
|
||||
END
|
||||
-- End of filters
|
||||
ORDER BY
|
||||
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
|
||||
LOWER(users.username) ASC OFFSET $15
|
||||
LOWER(users.username) ASC OFFSET $17
|
||||
LIMIT
|
||||
-- A null limit means "no limit", so 0 means return all
|
||||
NULLIF($16 :: int, 0)
|
||||
NULLIF($18 :: int, 0)
|
||||
`
|
||||
|
||||
type PaginatedOrganizationMembersParams struct {
|
||||
@@ -20251,6 +20279,8 @@ type PaginatedOrganizationMembersParams struct {
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Search string `db:"search" json:"search"`
|
||||
Name string `db:"name" json:"name"`
|
||||
ExactUsername string `db:"exact_username" json:"exact_username"`
|
||||
ExactEmail string `db:"exact_email" json:"exact_email"`
|
||||
Status []UserStatus `db:"status" json:"status"`
|
||||
RbacRole []string `db:"rbac_role" json:"rbac_role"`
|
||||
LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"`
|
||||
@@ -20287,6 +20317,8 @@ func (q *sqlQuerier) PaginatedOrganizationMembers(ctx context.Context, arg Pagin
|
||||
arg.OrganizationID,
|
||||
arg.Search,
|
||||
arg.Name,
|
||||
arg.ExactUsername,
|
||||
arg.ExactEmail,
|
||||
pq.Array(arg.Status),
|
||||
pq.Array(arg.RbacRole),
|
||||
arg.LastSeenBefore,
|
||||
|
||||
Reference in New Issue
Block a user