diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 60b7bbfd0f06d..48b9fe284844c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2037,6 +2037,32 @@ const docTemplate = `{ } }, "/organizations": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Organizations" + ], + "summary": "Get organizations", + "operationId": "get-organizations", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Organization" + } + } + } + } + }, "post": { "security": [ { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 43db2a118291a..f75fc96989955 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1779,6 +1779,28 @@ } }, "/organizations": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Organizations"], + "summary": "Get organizations", + "operationId": "get-organizations", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Organization" + } + } + } + } + }, "post": { "security": [ { diff --git a/coderd/coderd.go b/coderd/coderd.go index 0a3414fdb984c..26f2c66bb43d3 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -865,6 +865,7 @@ func New(options *Options) *API { apiKeyMiddleware, ) r.Post("/", api.postOrganizations) + r.Get("/", api.organizations) r.Route("/{organization}", func(r chi.Router) { r.Use( httpmw.ExtractOrganizationParam(options.Database), diff --git a/coderd/organizations.go b/coderd/organizations.go index 24d55fa950c65..83492b6cdb5bc 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -11,12 +11,38 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/codersdk" ) +// @Summary Get organizations +// @ID get-organizations +// @Security CoderSessionToken +// @Produce json +// @Tags Organizations +// @Success 200 {object} []codersdk.Organization +// @Router /organizations [get] +func (api *API) organizations(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + organizations, err := api.Database.GetOrganizations(ctx) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching organizations.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(organizations, convertOrganization)) +} + // @Summary Get organization by ID // @ID get-organization-by-id // @Security CoderSessionToken diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index 347048ed67a5c..47c8415feef8f 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -27,10 +27,15 @@ func TestMultiOrgFetch(t *testing.T) { require.NoError(t, err) } - orgs, err := client.OrganizationsByUser(ctx, codersdk.Me) + myOrgs, err := client.OrganizationsByUser(ctx, codersdk.Me) + require.NoError(t, err) + require.NotNil(t, myOrgs) + require.Len(t, myOrgs, len(makeOrgs)+1) + + orgs, err := client.Organizations(ctx) require.NoError(t, err) require.NotNil(t, orgs) - require.Len(t, orgs, len(makeOrgs)+1) + require.ElementsMatch(t, myOrgs, orgs) } func TestOrganizationsByUser(t *testing.T) { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 041087b26709a..758db099f95c9 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -215,6 +215,21 @@ func (c *Client) OrganizationByName(ctx context.Context, name string) (Organizat return organization, json.NewDecoder(res.Body).Decode(&organization) } +func (c *Client) Organizations(ctx context.Context) ([]Organization, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/organizations", nil) + if err != nil { + return []Organization{}, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return []Organization{}, ReadBodyAsError(res) + } + + var organizations []Organization + return organizations, json.NewDecoder(res.Body).Decode(&organizations) +} + func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) { // OrganizationByName uses the exact same endpoint. It accepts a name or uuid. // We just provide this function for type safety. diff --git a/docs/api/organizations.md b/docs/api/organizations.md index a1f8273549f80..4c4f49bb9d9d6 100644 --- a/docs/api/organizations.md +++ b/docs/api/organizations.md @@ -87,6 +87,62 @@ curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get organizations + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations` + +### Example responses + +> 200 Response + +```json +[ + { + "created_at": "2019-08-24T14:15:22Z", + "description": "string", + "display_name": "string", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_default": true, + "name": "string", + "updated_at": "2019-08-24T14:15:22Z" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Organization](schemas.md#codersdkorganization) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +| ---------------- | ----------------- | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | +| `» created_at` | string(date-time) | true | | | +| `» description` | string | false | | | +| `» display_name` | string | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | true | | | +| `» is_default` | boolean | true | | | +| `» name` | string | false | | | +| `» updated_at` | string(date-time) | true | | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Create organization ### Code samples diff --git a/site/src/api/api.ts b/site/src/api/api.ts index e60da675ccaa9..72d3ea2e48a57 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -565,7 +565,7 @@ class ApiMethods { getOrganizations = async (): Promise => { const response = await this.axios.get( - "/api/v2/users/me/organizations", + "/api/v2/organizations", ); return response.data; }; diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 3be956e5164ba..171efaec104f4 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -4,7 +4,7 @@ import type { CreateOrganizationRequest, UpdateOrganizationRequest, } from "api/typesGenerated"; -import { meKey, myOrganizationsKey } from "./users"; +import { meKey } from "./users"; export const createOrganization = (queryClient: QueryClient) => { return { @@ -13,7 +13,7 @@ export const createOrganization = (queryClient: QueryClient) => { onSuccess: async () => { await queryClient.invalidateQueries(meKey); - await queryClient.invalidateQueries(myOrganizationsKey); + await queryClient.invalidateQueries(organizationsKey); }, }; }; @@ -29,7 +29,7 @@ export const updateOrganization = (queryClient: QueryClient) => { API.updateOrganization(variables.orgId, variables.req), onSuccess: async () => { - await queryClient.invalidateQueries(myOrganizationsKey); + await queryClient.invalidateQueries(organizationsKey); }, }; }; @@ -40,7 +40,7 @@ export const deleteOrganization = (queryClient: QueryClient) => { onSuccess: async () => { await queryClient.invalidateQueries(meKey); - await queryClient.invalidateQueries(myOrganizationsKey); + await queryClient.invalidateQueries(organizationsKey); }, }; }; @@ -78,3 +78,12 @@ export const removeOrganizationMember = ( }, }; }; + +export const organizationsKey = ["organizations", "me"] as const; + +export const organizations = () => { + return { + queryKey: organizationsKey, + queryFn: () => API.getOrganizations(), + }; +}; diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index db43fa46620f5..8417dade576c8 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -249,12 +249,3 @@ export const updateAppearanceSettings = ( }, }; }; - -export const myOrganizationsKey = ["organizations", "me"] as const; - -export const myOrganizations = () => { - return { - queryKey: myOrganizationsKey, - queryFn: () => API.getOrganizations(), - }; -}; diff --git a/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx b/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx index 4ecd2dbe74ce0..35563f3eb13c3 100644 --- a/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx +++ b/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx @@ -2,7 +2,7 @@ import { createContext, type FC, Suspense, useContext } from "react"; import { useQuery } from "react-query"; import { Outlet, useLocation, useParams } from "react-router-dom"; import { deploymentConfig } from "api/queries/deployment"; -import { myOrganizations } from "api/queries/users"; +import { organizations } from "api/queries/organizations"; import type { Organization } from "api/typesGenerated"; import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; @@ -39,7 +39,7 @@ export const ManagementSettingsLayout: FC = () => { const { experiments } = useDashboard(); const { organization } = useParams() as { organization: string }; const deploymentConfigQuery = useQuery(deploymentConfig()); - const organizationsQuery = useQuery(myOrganizations()); + const organizationsQuery = useQuery(organizations()); const multiOrgExperimentEnabled = experiments.includes("multi-organization");