feat: add endpoint to fetch singular org member (#21732)

This commit is contained in:
Steven Masley
2026-02-03 12:48:25 -06:00
committed by GitHub
parent 1e2d2b92af
commit 6759b51cd6
7 changed files with 215 additions and 4 deletions
+39
View File
@@ -3482,6 +3482,45 @@ const docTemplate = `{
}
},
"/organizations/{organization}/members/{user}": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Members"
],
"summary": "Get organization member",
"operationId": "get-organization-member",
"parameters": [
{
"type": "string",
"description": "Organization ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User ID, name, or me",
"name": "user",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.OrganizationMemberWithUserData"
}
}
}
},
"post": {
"security": [
{
+35
View File
@@ -3059,6 +3059,41 @@
}
},
"/organizations/{organization}/members/{user}": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Members"],
"summary": "Get organization member",
"operationId": "get-organization-member",
"parameters": [
{
"type": "string",
"description": "Organization ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User ID, name, or me",
"name": "user",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.OrganizationMemberWithUserData"
}
}
}
},
"post": {
"security": [
{
+1
View File
@@ -1228,6 +1228,7 @@ func New(options *Options) *API {
r.Use(
httpmw.ExtractOrganizationMemberParam(options.Database),
)
r.Get("/", api.organizationMember)
r.Delete("/", api.deleteOrganizationMember)
r.Put("/roles", api.putMemberRoles)
r.Post("/workspaces", api.postWorkspacesByOrganization)
+48
View File
@@ -145,6 +145,54 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request
rw.WriteHeader(http.StatusNoContent)
}
// @Summary Get organization member
// @ID get-organization-member
// @Security CoderSessionToken
// @Tags Members
// @Param organization path string true "Organization ID"
// @Param user path string true "User ID, name, or me"
// @Success 200 {object} codersdk.OrganizationMemberWithUserData
// @Produce json
// @Router /organizations/{organization}/members/{user} [get]
func (api *API) organizationMember(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
organization = httpmw.OrganizationParam(r)
member = httpmw.OrganizationMemberParam(r)
)
// This is unfortunate to fetch like this, but we need the user table data.
// The listing route uses this data format, so it is just easier to reuse the
// list query.
rows, err := api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{
OrganizationID: organization.ID,
UserID: member.UserID,
IncludeSystem: false,
GithubUserID: 0,
})
if httpapi.Is404Error(err) || len(rows) == 0 {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
resp, err := convertOrganizationMembersWithUserData(ctx, api.Database, rows)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
if len(resp) != 1 {
httpapi.InternalServerError(rw, xerrors.Errorf("unexpected organization members, something went wrong"))
return
}
httpapi.Write(ctx, rw, http.StatusOK, resp[0])
}
// @Deprecated use /organizations/{organization}/paginated-members [get]
// @Summary List organization members
// @ID list-organization-members
+20 -4
View File
@@ -18,17 +18,33 @@ import (
func TestAddMember(t *testing.T) {
t.Parallel()
owner := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, owner)
_, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID)
t.Run("AlreadyMember", func(t *testing.T) {
t.Parallel()
owner := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, owner)
_, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID)
ctx := testutil.Context(t, testutil.WaitMedium)
// Add user to org, even though they already exist
// nolint:gocritic // must be an owner to see the user
_, err := owner.PostOrganizationMember(ctx, first.OrganizationID, user.Username)
require.ErrorContains(t, err, "already an organization member")
org, err := owner.Organization(ctx, first.OrganizationID)
require.NoError(t, err)
member, err := owner.OrganizationMember(ctx, org.Name, user.Username)
require.NoError(t, err)
require.Equal(t, member.UserID, user.ID)
})
t.Run("Me", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
member, err := owner.OrganizationMember(ctx, first.OrganizationID.String(), codersdk.Me)
require.NoError(t, err)
require.Equal(t, member.UserID, first.UserID)
})
}
+13
View File
@@ -644,6 +644,19 @@ func OrganizationMembersQueryOptionGithubUserID(githubUserID int64) Organization
}
}
func (c *Client) OrganizationMember(ctx context.Context, organizationIdent, userIdent string) (OrganizationMemberWithUserData, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationIdent, userIdent), nil)
if err != nil {
return OrganizationMemberWithUserData{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return OrganizationMemberWithUserData{}, ReadBodyAsError(res)
}
var member OrganizationMemberWithUserData
return member, json.NewDecoder(res.Body).Decode(&member)
}
// OrganizationMembers lists all members in an organization
func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UUID, opts ...OrganizationMembersQueryOption) ([]OrganizationMemberWithUserData, error) {
var query OrganizationMembersQuery
+59
View File
@@ -540,6 +540,65 @@ Status Code **200**
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get organization member
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members/{user} \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /organizations/{organization}/members/{user}`
### Parameters
| Name | In | Type | Required | Description |
|----------------|------|--------|----------|----------------------|
| `organization` | path | string | true | Organization ID |
| `user` | path | string | true | User ID, name, or me |
### Example responses
> 200 Response
```json
{
"avatar_url": "string",
"created_at": "2019-08-24T14:15:22Z",
"email": "string",
"global_roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string"
}
```
### Responses
| Status | Meaning | Description | Schema |
|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------------------|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationMemberWithUserData](schemas.md#codersdkorganizationmemberwithuserdata) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Add organization member
### Code samples