coder/site/src/api/api.ts

1546 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios from "axios";
import dayjs from "dayjs";
import * as TypesGen from "./typesGenerated";
// This needs to include the `../`, otherwise it breaks when importing into
// vscode-coder.
import { delay } from "../utils/delay";
import userAgentParser from "ua-parser-js";
// Adds 304 for the default axios validateStatus function
// https://github.com/axios/axios#handling-errors Check status here
// https://httpstatusdogs.com/
axios.defaults.validateStatus = (status) => {
return (status >= 200 && status < 300) || status === 304;
};
export const hardCodedCSRFCookie = (): string => {
// This is a hard coded CSRF token/cookie pair for local development. In prod,
// the GoLang webserver generates a random cookie with a new token for each
// document request. For local development, we don't use the Go webserver for
// static files, so this is the 'hack' to make local development work with
// remote apis. The CSRF cookie for this token is
// "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4="
const csrfToken =
"KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==";
axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
return csrfToken;
};
// withDefaultFeatures sets all unspecified features to not_entitled and
// disabled.
export const withDefaultFeatures = (
fs: Partial<TypesGen.Entitlements["features"]>,
): TypesGen.Entitlements["features"] => {
for (const feature of TypesGen.FeatureNames) {
// Skip fields that are already filled.
if (fs[feature] !== undefined) {
continue;
}
fs[feature] = {
enabled: false,
entitlement: "not_entitled",
};
}
return fs as TypesGen.Entitlements["features"];
};
// Always attach CSRF token to all requests. In puppeteer the document is
// undefined. In those cases, just do nothing.
const token =
typeof document !== "undefined"
? document.head.querySelector('meta[property="csrf-token"]')
: null;
if (token !== null && token.getAttribute("content") !== null) {
if (process.env.NODE_ENV === "development") {
// Development mode uses a hard-coded CSRF token
axios.defaults.headers.common["X-CSRF-TOKEN"] = hardCodedCSRFCookie();
token.setAttribute("content", hardCodedCSRFCookie());
} else {
axios.defaults.headers.common["X-CSRF-TOKEN"] =
token.getAttribute("content") ?? "";
}
} else {
// Do not write error logs if we are in a FE unit test.
if (process.env.JEST_WORKER_ID === undefined) {
console.error("CSRF token not found");
}
}
const CONTENT_TYPE_JSON = {
"Content-Type": "application/json",
};
export const provisioners: TypesGen.ProvisionerDaemon[] = [
{
id: "terraform",
name: "Terraform",
created_at: "",
provisioners: [],
tags: {},
},
{
id: "cdr-basic",
name: "Basic",
created_at: "",
provisioners: [],
tags: {},
},
];
export const login = async (
email: string,
password: string,
): Promise<TypesGen.LoginWithPasswordResponse> => {
const payload = JSON.stringify({
email,
password,
});
const response = await axios.post<TypesGen.LoginWithPasswordResponse>(
"/api/v2/users/login",
payload,
{
headers: { ...CONTENT_TYPE_JSON },
},
);
return response.data;
};
export const convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => {
const response = await axios.post<TypesGen.OAuthConversionResponse>(
"/api/v2/users/me/convert-login",
request,
);
return response.data;
};
export const logout = async (): Promise<void> => {
await axios.post("/api/v2/users/logout");
};
export const getAuthenticatedUser = async () => {
const response = await axios.get<TypesGen.User>("/api/v2/users/me");
return response.data;
};
export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
const response = await axios.get<TypesGen.AuthMethods>(
"/api/v2/users/authmethods",
);
return response.data;
};
export const getUserLoginType = async (): Promise<TypesGen.UserLoginType> => {
const response = await axios.get<TypesGen.UserLoginType>(
"/api/v2/users/me/login-type",
);
return response.data;
};
export const checkAuthorization = async (
params: TypesGen.AuthorizationRequest,
): Promise<TypesGen.AuthorizationResponse> => {
const response = await axios.post<TypesGen.AuthorizationResponse>(
`/api/v2/authcheck`,
params,
);
return response.data;
};
export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
const response = await axios.post<TypesGen.GenerateAPIKeyResponse>(
"/api/v2/users/me/keys",
);
return response.data;
};
export const getTokens = async (
params: TypesGen.TokensFilter,
): Promise<TypesGen.APIKeyWithOwner[]> => {
const response = await axios.get<TypesGen.APIKeyWithOwner[]>(
`/api/v2/users/me/keys/tokens`,
{
params,
},
);
return response.data;
};
export const deleteToken = async (keyId: string): Promise<void> => {
await axios.delete("/api/v2/users/me/keys/" + keyId);
};
export const createToken = async (
params: TypesGen.CreateTokenRequest,
): Promise<TypesGen.GenerateAPIKeyResponse> => {
const response = await axios.post(`/api/v2/users/me/keys/tokens`, params);
return response.data;
};
export const getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {
const response = await axios.get("/api/v2/users/me/keys/tokens/tokenconfig");
return response.data;
};
export const getUsers = async (
options: TypesGen.UsersRequest,
signal?: AbortSignal,
): Promise<TypesGen.GetUsersResponse> => {
const url = getURLWithSearchParams("/api/v2/users", options);
const response = await axios.get<TypesGen.GetUsersResponse>(url.toString(), {
signal,
});
return response.data;
};
export const getOrganization = async (
organizationId: string,
): Promise<TypesGen.Organization> => {
const response = await axios.get<TypesGen.Organization>(
`/api/v2/organizations/${organizationId}`,
);
return response.data;
};
export const getOrganizations = async (): Promise<TypesGen.Organization[]> => {
const response = await axios.get<TypesGen.Organization[]>(
"/api/v2/users/me/organizations",
);
return response.data;
};
export const getTemplate = async (
templateId: string,
): Promise<TypesGen.Template> => {
const response = await axios.get<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
);
return response.data;
};
export const getTemplates = async (
organizationId: string,
): Promise<TypesGen.Template[]> => {
const response = await axios.get<TypesGen.Template[]>(
`/api/v2/organizations/${organizationId}/templates`,
);
return response.data;
};
export const getTemplateByName = async (
organizationId: string,
name: string,
): Promise<TypesGen.Template> => {
const response = await axios.get<TypesGen.Template>(
`/api/v2/organizations/${organizationId}/templates/${name}`,
);
return response.data;
};
export const getTemplateVersion = async (
versionId: string,
): Promise<TypesGen.TemplateVersion> => {
const response = await axios.get<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${versionId}`,
);
return response.data;
};
export const getTemplateVersionResources = async (
versionId: string,
): Promise<TypesGen.WorkspaceResource[]> => {
const response = await axios.get<TypesGen.WorkspaceResource[]>(
`/api/v2/templateversions/${versionId}/resources`,
);
return response.data;
};
export const getTemplateVersionVariables = async (
versionId: string,
): Promise<TypesGen.TemplateVersionVariable[]> => {
const response = await axios.get<TypesGen.TemplateVersionVariable[]>(
`/api/v2/templateversions/${versionId}/variables`,
);
return response.data;
};
export const getTemplateVersions = async (
templateId: string,
): Promise<TypesGen.TemplateVersion[]> => {
const response = await axios.get<TypesGen.TemplateVersion[]>(
`/api/v2/templates/${templateId}/versions`,
);
return response.data;
};
export const getTemplateVersionByName = async (
organizationId: string,
templateName: string,
versionName: string,
): Promise<TypesGen.TemplateVersion> => {
const response = await axios.get<TypesGen.TemplateVersion>(
`/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`,
);
return response.data;
};
export type GetPreviousTemplateVersionByNameResponse =
| TypesGen.TemplateVersion
| undefined;
export const getPreviousTemplateVersionByName = async (
organizationId: string,
templateName: string,
versionName: string,
): Promise<GetPreviousTemplateVersionByNameResponse> => {
try {
const response = await axios.get<TypesGen.TemplateVersion>(
`/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`,
);
return response.data;
} catch (error) {
// When there is no previous version, like the first version of a template,
// the API returns 404 so in this case we can safely return undefined
if (
axios.isAxiosError(error) &&
error.response &&
error.response.status === 404
) {
return undefined;
}
throw error;
}
};
export const createTemplateVersion = async (
organizationId: string,
data: TypesGen.CreateTemplateVersionRequest,
): Promise<TypesGen.TemplateVersion> => {
const response = await axios.post<TypesGen.TemplateVersion>(
`/api/v2/organizations/${organizationId}/templateversions`,
data,
);
return response.data;
};
export const getTemplateVersionExternalAuth = async (
versionId: string,
): Promise<TypesGen.TemplateVersionExternalAuth[]> => {
const response = await axios.get(
`/api/v2/templateversions/${versionId}/external-auth`,
);
return response.data;
};
export const getTemplateVersionRichParameters = async (
versionId: string,
): Promise<TypesGen.TemplateVersionParameter[]> => {
const response = await axios.get(
`/api/v2/templateversions/${versionId}/rich-parameters`,
);
return response.data;
};
export const createTemplate = async (
organizationId: string,
data: TypesGen.CreateTemplateRequest,
): Promise<TypesGen.Template> => {
const response = await axios.post(
`/api/v2/organizations/${organizationId}/templates`,
data,
);
return response.data;
};
export const updateActiveTemplateVersion = async (
templateId: string,
data: TypesGen.UpdateActiveTemplateVersion,
) => {
const response = await axios.patch<TypesGen.Response>(
`/api/v2/templates/${templateId}/versions`,
data,
);
return response.data;
};
export const patchTemplateVersion = async (
templateVersionId: string,
data: TypesGen.PatchTemplateVersionRequest,
) => {
const response = await axios.patch<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${templateVersionId}`,
data,
);
return response.data;
};
export const archiveTemplateVersion = async (templateVersionId: string) => {
const response = await axios.post<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${templateVersionId}/archive`,
);
return response.data;
};
export const unarchiveTemplateVersion = async (templateVersionId: string) => {
const response = await axios.post<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${templateVersionId}/unarchive`,
);
return response.data;
};
export const updateTemplateMeta = async (
templateId: string,
data: TypesGen.UpdateTemplateMeta,
): Promise<TypesGen.Template> => {
const response = await axios.patch<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
data,
);
return response.data;
};
export const deleteTemplate = async (
templateId: string,
): Promise<TypesGen.Template> => {
const response = await axios.delete<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
);
return response.data;
};
export const getWorkspace = async (
workspaceId: string,
params?: TypesGen.WorkspaceOptions,
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(
`/api/v2/workspaces/${workspaceId}`,
{
params,
},
);
return response.data;
};
/**
*
* @param workspaceId
* @returns An EventSource that emits workspace event objects (ServerSentEvent)
*/
export const watchWorkspace = (workspaceId: string): EventSource => {
return new EventSource(
`${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`,
{ withCredentials: true },
);
};
interface SearchParamOptions extends TypesGen.Pagination {
q?: string;
}
export const getURLWithSearchParams = (
basePath: string,
options?: SearchParamOptions,
): string => {
if (options) {
const searchParams = new URLSearchParams();
const keys = Object.keys(options) as (keyof SearchParamOptions)[];
keys.forEach((key) => {
const value = options[key];
if (value !== undefined && value !== "") {
searchParams.append(key, value.toString());
}
});
const searchString = searchParams.toString();
return searchString ? `${basePath}?${searchString}` : basePath;
} else {
return basePath;
}
};
export const getWorkspaces = async (
options: TypesGen.WorkspacesRequest,
): Promise<TypesGen.WorkspacesResponse> => {
const url = getURLWithSearchParams("/api/v2/workspaces", options);
const response = await axios.get<TypesGen.WorkspacesResponse>(url);
return response.data;
};
export const getWorkspaceByOwnerAndName = async (
username = "me",
workspaceName: string,
params?: TypesGen.WorkspaceOptions,
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(
`/api/v2/users/${username}/workspace/${workspaceName}`,
{
params,
},
);
return response.data;
};
export function waitForBuild(build: TypesGen.WorkspaceBuild) {
return new Promise<TypesGen.ProvisionerJob | undefined>((res, reject) => {
void (async () => {
let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined;
while (
!["succeeded", "canceled"].some(
(status) => latestJobInfo?.status.includes(status),
)
) {
const { job } = await getWorkspaceBuildByNumber(
build.workspace_owner_name,
build.workspace_name,
build.build_number,
);
latestJobInfo = job;
if (latestJobInfo.status === "failed") {
return reject(latestJobInfo);
}
await delay(1000);
}
return res(latestJobInfo);
})();
});
}
export const postWorkspaceBuild = async (
workspaceId: string,
data: TypesGen.CreateWorkspaceBuildRequest,
): Promise<TypesGen.WorkspaceBuild> => {
const response = await axios.post(
`/api/v2/workspaces/${workspaceId}/builds`,
data,
);
return response.data;
};
export const startWorkspace = (
workspaceId: string,
templateVersionId: string,
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
buildParameters?: TypesGen.WorkspaceBuildParameter[],
) =>
postWorkspaceBuild(workspaceId, {
transition: "start",
template_version_id: templateVersionId,
log_level: logLevel,
rich_parameter_values: buildParameters,
});
export const stopWorkspace = (
workspaceId: string,
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
) =>
postWorkspaceBuild(workspaceId, {
transition: "stop",
log_level: logLevel,
});
export const deleteWorkspace = (
workspaceId: string,
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
) =>
postWorkspaceBuild(workspaceId, {
transition: "delete",
log_level: logLevel,
});
export const cancelWorkspaceBuild = async (
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
): Promise<TypesGen.Response> => {
const response = await axios.patch(
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
);
return response.data;
};
export const updateWorkspaceDormancy = async (
workspaceId: string,
dormant: boolean,
): Promise<TypesGen.Workspace> => {
const data: TypesGen.UpdateWorkspaceDormancy = {
dormant: dormant,
};
const response = await axios.put(
`/api/v2/workspaces/${workspaceId}/dormant`,
data,
);
return response.data;
};
export const restartWorkspace = async ({
workspace,
buildParameters,
}: {
workspace: TypesGen.Workspace;
buildParameters?: TypesGen.WorkspaceBuildParameter[];
}) => {
const stopBuild = await stopWorkspace(workspace.id);
const awaitedStopBuild = await waitForBuild(stopBuild);
// If the restart is canceled halfway through, make sure we bail
if (awaitedStopBuild?.status === "canceled") {
return;
}
const startBuild = await startWorkspace(
workspace.id,
workspace.latest_build.template_version_id,
undefined,
buildParameters,
);
await waitForBuild(startBuild);
};
export const cancelTemplateVersionBuild = async (
templateVersionId: TypesGen.TemplateVersion["id"],
): Promise<TypesGen.Response> => {
const response = await axios.patch(
`/api/v2/templateversions/${templateVersionId}/cancel`,
);
return response.data;
};
export const createUser = async (
user: TypesGen.CreateUserRequest,
): Promise<TypesGen.User> => {
const response = await axios.post<TypesGen.User>("/api/v2/users", user);
return response.data;
};
export const createWorkspace = async (
organizationId: string,
userId = "me",
workspace: TypesGen.CreateWorkspaceRequest,
): Promise<TypesGen.Workspace> => {
const response = await axios.post<TypesGen.Workspace>(
`/api/v2/organizations/${organizationId}/members/${userId}/workspaces`,
workspace,
);
return response.data;
};
export const patchWorkspace = async (
workspaceId: string,
data: TypesGen.UpdateWorkspaceRequest,
) => {
await axios.patch(`/api/v2/workspaces/${workspaceId}`, data);
};
export const getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
const response = await axios.get("/api/v2/buildinfo");
return response.data;
};
export const getUpdateCheck =
async (): Promise<TypesGen.UpdateCheckResponse> => {
const response = await axios.get("/api/v2/updatecheck");
return response.data;
};
export const putWorkspaceAutostart = async (
workspaceID: string,
autostart: TypesGen.UpdateWorkspaceAutostartRequest,
): Promise<void> => {
const payload = JSON.stringify(autostart);
await axios.put(`/api/v2/workspaces/${workspaceID}/autostart`, payload, {
headers: { ...CONTENT_TYPE_JSON },
});
};
export const putWorkspaceAutostop = async (
workspaceID: string,
ttl: TypesGen.UpdateWorkspaceTTLRequest,
): Promise<void> => {
const payload = JSON.stringify(ttl);
await axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, {
headers: { ...CONTENT_TYPE_JSON },
});
};
export const updateProfile = async (
userId: string,
data: TypesGen.UpdateUserProfileRequest,
): Promise<TypesGen.User> => {
const response = await axios.put(`/api/v2/users/${userId}/profile`, data);
return response.data;
};
export const getUserQuietHoursSchedule = async (
userId: TypesGen.User["id"],
): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
const response = await axios.get(`/api/v2/users/${userId}/quiet-hours`);
return response.data;
};
export const updateUserQuietHoursSchedule = async (
userId: TypesGen.User["id"],
data: TypesGen.UpdateUserQuietHoursScheduleRequest,
): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
const response = await axios.put(`/api/v2/users/${userId}/quiet-hours`, data);
return response.data;
};
export const activateUser = async (
userId: TypesGen.User["id"],
): Promise<TypesGen.User> => {
const response = await axios.put<TypesGen.User>(
`/api/v2/users/${userId}/status/activate`,
);
return response.data;
};
export const suspendUser = async (
userId: TypesGen.User["id"],
): Promise<TypesGen.User> => {
const response = await axios.put<TypesGen.User>(
`/api/v2/users/${userId}/status/suspend`,
);
return response.data;
};
export const deleteUser = async (
userId: TypesGen.User["id"],
): Promise<undefined> => {
return await axios.delete(`/api/v2/users/${userId}`);
};
// API definition:
// https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53
export const hasFirstUser = async (): Promise<boolean> => {
try {
// If it is success, it is true
await axios.get("/api/v2/users/first");
return true;
} catch (error) {
// If it returns a 404, it is false
if (axios.isAxiosError(error) && error.response?.status === 404) {
return false;
}
throw error;
}
};
export const createFirstUser = async (
req: TypesGen.CreateFirstUserRequest,
): Promise<TypesGen.CreateFirstUserResponse> => {
const response = await axios.post(`/api/v2/users/first`, req);
return response.data;
};
export const updateUserPassword = async (
userId: TypesGen.User["id"],
updatePassword: TypesGen.UpdateUserPasswordRequest,
): Promise<undefined> =>
axios.put(`/api/v2/users/${userId}/password`, updatePassword);
export const getRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => {
const response = await axios.get<Array<TypesGen.AssignableRoles>>(
`/api/v2/users/roles`,
);
return response.data;
};
export const updateUserRoles = async (
roles: TypesGen.Role["name"][],
userId: TypesGen.User["id"],
): Promise<TypesGen.User> => {
const response = await axios.put<TypesGen.User>(
`/api/v2/users/${userId}/roles`,
{ roles },
);
return response.data;
};
export const getUserSSHKey = async (
userId = "me",
): Promise<TypesGen.GitSSHKey> => {
const response = await axios.get<TypesGen.GitSSHKey>(
`/api/v2/users/${userId}/gitsshkey`,
);
return response.data;
};
export const regenerateUserSSHKey = async (
userId = "me",
): Promise<TypesGen.GitSSHKey> => {
const response = await axios.put<TypesGen.GitSSHKey>(
`/api/v2/users/${userId}/gitsshkey`,
);
return response.data;
};
export const getWorkspaceBuilds = async (
workspaceId: string,
req?: TypesGen.WorkspaceBuildsRequest,
) => {
const response = await axios.get<TypesGen.WorkspaceBuild[]>(
getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req),
);
return response.data;
};
export const getWorkspaceBuildByNumber = async (
username = "me",
workspaceName: string,
buildNumber: number,
): Promise<TypesGen.WorkspaceBuild> => {
const response = await axios.get<TypesGen.WorkspaceBuild>(
`/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`,
);
return response.data;
};
export const getWorkspaceBuildLogs = async (
buildId: string,
before: Date,
): Promise<TypesGen.ProvisionerJobLog[]> => {
const response = await axios.get<TypesGen.ProvisionerJobLog[]>(
`/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`,
);
return response.data;
};
export const getWorkspaceAgentLogs = async (
agentID: string,
): Promise<TypesGen.WorkspaceAgentLog[]> => {
const response = await axios.get<TypesGen.WorkspaceAgentLog[]>(
`/api/v2/workspaceagents/${agentID}/logs`,
);
return response.data;
};
export const putWorkspaceExtension = async (
workspaceId: string,
newDeadline: dayjs.Dayjs,
): Promise<void> => {
await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, {
deadline: newDeadline,
});
};
export const refreshEntitlements = async (): Promise<void> => {
await axios.post("/api/v2/licenses/refresh-entitlements");
};
export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
try {
const response = await axios.get("/api/v2/entitlements");
return response.data;
} catch (ex) {
if (axios.isAxiosError(ex) && ex.response?.status === 404) {
return {
errors: [],
features: withDefaultFeatures({}),
has_license: false,
require_telemetry: false,
trial: false,
warnings: [],
refreshed_at: "",
};
}
throw ex;
}
};
export const getExperiments = async (): Promise<TypesGen.Experiment[]> => {
try {
const response = await axios.get("/api/v2/experiments");
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return [];
}
throw error;
}
};
export const getAvailableExperiments =
async (): Promise<TypesGen.AvailableExperiments> => {
try {
const response = await axios.get("/api/v2/experiments/available");
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return { safe: [] };
}
throw error;
}
};
export const getExternalAuthProvider = async (
provider: string,
): Promise<TypesGen.ExternalAuth> => {
const resp = await axios.get(`/api/v2/external-auth/${provider}`);
return resp.data;
};
export const getExternalAuthDevice = async (
provider: string,
): Promise<TypesGen.ExternalAuthDevice> => {
const resp = await axios.get(`/api/v2/external-auth/${provider}/device`);
return resp.data;
};
export const exchangeExternalAuthDevice = async (
provider: string,
req: TypesGen.ExternalAuthDeviceExchange,
): Promise<void> => {
const resp = await axios.post(
`/api/v2/external-auth/${provider}/device`,
req,
);
return resp.data;
};
export const getAuditLogs = async (
options: TypesGen.AuditLogsRequest,
): Promise<TypesGen.AuditLogResponse> => {
const url = getURLWithSearchParams("/api/v2/audit", options);
const response = await axios.get(url);
return response.data;
};
export const getTemplateDAUs = async (
templateId: string,
): Promise<TypesGen.DAUsResponse> => {
const response = await axios.get(`/api/v2/templates/${templateId}/daus`);
return response.data;
};
export const getDeploymentDAUs = async (
// Default to user's local timezone
offset = new Date().getTimezoneOffset() / 60,
): Promise<TypesGen.DAUsResponse> => {
const response = await axios.get(`/api/v2/insights/daus?tz_offset=${offset}`);
return response.data;
};
export const getTemplateACLAvailable = async (
templateId: string,
options: TypesGen.UsersRequest,
): Promise<TypesGen.ACLAvailable> => {
const url = getURLWithSearchParams(
`/api/v2/templates/${templateId}/acl/available`,
options,
);
const response = await axios.get(url.toString());
return response.data;
};
export const getTemplateACL = async (
templateId: string,
): Promise<TypesGen.TemplateACL> => {
const response = await axios.get(`/api/v2/templates/${templateId}/acl`);
return response.data;
};
export const updateTemplateACL = async (
templateId: string,
data: TypesGen.UpdateTemplateACL,
): Promise<TypesGen.TemplateACL> => {
const response = await axios.patch(
`/api/v2/templates/${templateId}/acl`,
data,
);
return response.data;
};
export const getApplicationsHost =
async (): Promise<TypesGen.AppHostResponse> => {
const response = await axios.get(`/api/v2/applications/host`);
return response.data;
};
export const getGroups = async (
organizationId: string,
): Promise<TypesGen.Group[]> => {
const response = await axios.get(
`/api/v2/organizations/${organizationId}/groups`,
);
return response.data;
};
export const createGroup = async (
organizationId: string,
data: TypesGen.CreateGroupRequest,
): Promise<TypesGen.Group> => {
const response = await axios.post(
`/api/v2/organizations/${organizationId}/groups`,
data,
);
return response.data;
};
export const getGroup = async (groupId: string): Promise<TypesGen.Group> => {
const response = await axios.get(`/api/v2/groups/${groupId}`);
return response.data;
};
export const patchGroup = async (
groupId: string,
data: TypesGen.PatchGroupRequest,
): Promise<TypesGen.Group> => {
const response = await axios.patch(`/api/v2/groups/${groupId}`, data);
return response.data;
};
export const addMember = async (groupId: string, userId: string) => {
return patchGroup(groupId, {
name: "",
display_name: "",
add_users: [userId],
remove_users: [],
});
};
export const removeMember = async (groupId: string, userId: string) => {
return patchGroup(groupId, {
name: "",
display_name: "",
add_users: [],
remove_users: [userId],
});
};
export const deleteGroup = async (groupId: string): Promise<void> => {
await axios.delete(`/api/v2/groups/${groupId}`);
};
export const getWorkspaceQuota = async (
username: string,
): Promise<TypesGen.WorkspaceQuota> => {
const response = await axios.get(
`/api/v2/workspace-quota/${encodeURIComponent(username)}`,
);
return response.data;
};
export const getAgentListeningPorts = async (
agentID: string,
): Promise<TypesGen.WorkspaceAgentListeningPortsResponse> => {
const response = await axios.get(
`/api/v2/workspaceagents/${agentID}/listening-ports`,
);
return response.data;
};
// getDeploymentSSHConfig is used by the VSCode-Extension.
export const getDeploymentSSHConfig =
async (): Promise<TypesGen.SSHConfigResponse> => {
const response = await axios.get(`/api/v2/deployment/ssh`);
return response.data;
};
export type DeploymentConfig = {
readonly config: TypesGen.DeploymentValues;
readonly options: TypesGen.ClibaseOption[];
};
export const getDeploymentConfig = async (): Promise<DeploymentConfig> => {
const response = await axios.get(`/api/v2/deployment/config`);
return response.data;
};
export const getDeploymentStats =
async (): Promise<TypesGen.DeploymentStats> => {
const response = await axios.get(`/api/v2/deployment/stats`);
return response.data;
};
export const getReplicas = async (): Promise<TypesGen.Replica[]> => {
const response = await axios.get(`/api/v2/replicas`);
return response.data;
};
export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
const response = await axios.get<ArrayBuffer>(`/api/v2/files/${fileId}`, {
responseType: "arraybuffer",
});
return response.data;
};
export const getWorkspaceProxyRegions = async (): Promise<
TypesGen.RegionsResponse<TypesGen.Region>
> => {
const response = await axios.get<TypesGen.RegionsResponse<TypesGen.Region>>(
`/api/v2/regions`,
);
return response.data;
};
export const getWorkspaceProxies = async (): Promise<
TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
> => {
const response = await axios.get<
TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
>(`/api/v2/workspaceproxies`);
return response.data;
};
export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
try {
const response = await axios.get(`/api/v2/appearance`);
return response.data || {};
} catch (ex) {
if (axios.isAxiosError(ex) && ex.response?.status === 404) {
return {
application_name: "",
logo_url: "",
service_banner: {
enabled: false,
},
};
}
throw ex;
}
};
export const updateAppearance = async (
b: TypesGen.AppearanceConfig,
): Promise<TypesGen.AppearanceConfig> => {
const response = await axios.put(`/api/v2/appearance`, b);
return response.data;
};
export const getTemplateExamples = async (
organizationId: string,
): Promise<TypesGen.TemplateExample[]> => {
const response = await axios.get(
`/api/v2/organizations/${organizationId}/templates/examples`,
);
return response.data;
};
export const uploadFile = async (
file: File,
): Promise<TypesGen.UploadResponse> => {
const response = await axios.post("/api/v2/files", file, {
headers: {
"Content-Type": "application/x-tar",
},
});
return response.data;
};
export const getTemplateVersionLogs = async (
versionId: string,
): Promise<TypesGen.ProvisionerJobLog[]> => {
const response = await axios.get<TypesGen.ProvisionerJobLog[]>(
`/api/v2/templateversions/${versionId}/logs`,
);
return response.data;
};
export const updateWorkspaceVersion = async (
workspace: TypesGen.Workspace,
): Promise<TypesGen.WorkspaceBuild> => {
const template = await getTemplate(workspace.template_id);
return startWorkspace(workspace.id, template.active_version_id);
};
export const getWorkspaceBuildParameters = async (
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
): Promise<TypesGen.WorkspaceBuildParameter[]> => {
const response = await axios.get<TypesGen.WorkspaceBuildParameter[]>(
`/api/v2/workspacebuilds/${workspaceBuildId}/parameters`,
);
return response.data;
};
type Claims = {
license_expires: number;
account_type?: string;
account_id?: string;
trial: boolean;
all_features: boolean;
version: number;
features: Record<string, number>;
require_telemetry?: boolean;
};
export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & {
claims: Claims;
expires_at: string;
};
export const getLicenses = async (): Promise<GetLicensesResponse[]> => {
const response = await axios.get(`/api/v2/licenses`);
return response.data;
};
export const createLicense = async (
data: TypesGen.AddLicenseRequest,
): Promise<TypesGen.AddLicenseRequest> => {
const response = await axios.post(`/api/v2/licenses`, data);
return response.data;
};
export const removeLicense = async (licenseId: number): Promise<void> => {
await axios.delete(`/api/v2/licenses/${licenseId}`);
};
export class MissingBuildParameters extends Error {
parameters: TypesGen.TemplateVersionParameter[] = [];
constructor(parameters: TypesGen.TemplateVersionParameter[]) {
super("Missing build parameters.");
this.parameters = parameters;
}
}
/** Steps to change the workspace version
* - Get the latest template to access the latest active version
* - Get the current build parameters
* - Get the template parameters
* - Update the build parameters and check if there are missed parameters for the new version
* - If there are missing parameters raise an error
* - Create a build with the version and updated build parameters
*/
export const changeWorkspaceVersion = async (
workspace: TypesGen.Workspace,
templateVersionId: string,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
): Promise<TypesGen.WorkspaceBuild> => {
const [currentBuildParameters, templateParameters] = await Promise.all([
getWorkspaceBuildParameters(workspace.latest_build.id),
getTemplateVersionRichParameters(templateVersionId),
]);
const missingParameters = getMissingParameters(
currentBuildParameters,
newBuildParameters,
templateParameters,
);
if (missingParameters.length > 0) {
throw new MissingBuildParameters(missingParameters);
}
return postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: templateVersionId,
rich_parameter_values: newBuildParameters,
});
};
/** Steps to update the workspace
* - Get the latest template to access the latest active version
* - Get the current build parameters
* - Get the template parameters
* - Update the build parameters and check if there are missed parameters for
* the newest version
* - If there are missing parameters raise an error
* - Create a build with the latest version and updated build parameters
*/
export const updateWorkspace = async (
workspace: TypesGen.Workspace,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
): Promise<TypesGen.WorkspaceBuild> => {
const [template, oldBuildParameters] = await Promise.all([
getTemplate(workspace.template_id),
getWorkspaceBuildParameters(workspace.latest_build.id),
]);
const activeVersionId = template.active_version_id;
const templateParameters = await getTemplateVersionRichParameters(
activeVersionId,
);
const missingParameters = getMissingParameters(
oldBuildParameters,
newBuildParameters,
templateParameters,
);
if (missingParameters.length > 0) {
throw new MissingBuildParameters(missingParameters);
}
return postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: activeVersionId,
rich_parameter_values: newBuildParameters,
});
};
const getMissingParameters = (
oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
newBuildParameters: TypesGen.WorkspaceBuildParameter[],
templateParameters: TypesGen.TemplateVersionParameter[],
) => {
const missingParameters: TypesGen.TemplateVersionParameter[] = [];
const requiredParameters: TypesGen.TemplateVersionParameter[] = [];
templateParameters.forEach((p) => {
// It is mutable and required. Mutable values can be changed after so we
// don't need to ask them if they are not required.
const isMutableAndRequired = p.mutable && p.required;
// Is immutable, so we can check if it is its first time on the build
const isImmutable = !p.mutable;
if (isMutableAndRequired || isImmutable) {
requiredParameters.push(p);
}
});
for (const parameter of requiredParameters) {
// Check if there is a new value
let buildParameter = newBuildParameters.find(
(p) => p.name === parameter.name,
);
// If not, get the old one
if (!buildParameter) {
buildParameter = oldBuildParameters.find(
(p) => p.name === parameter.name,
);
}
// If there is a value from the new or old one, it is not missed
if (buildParameter) {
continue;
}
missingParameters.push(parameter);
}
// Check if parameter "options" changed and we can't use old build parameters.
templateParameters.forEach((templateParameter) => {
if (templateParameter.options.length === 0) {
return;
}
// Check if there is a new value
let buildParameter = newBuildParameters.find(
(p) => p.name === templateParameter.name,
);
// If not, get the old one
if (!buildParameter) {
buildParameter = oldBuildParameters.find(
(p) => p.name === templateParameter.name,
);
}
if (!buildParameter) {
return;
}
const matchingOption = templateParameter.options.find(
(option) => option.value === buildParameter?.value,
);
if (!matchingOption) {
missingParameters.push(templateParameter);
}
});
return missingParameters;
};
/**
*
* @param agentId
* @returns An EventSource that emits agent metadata event objects
* (ServerSentEvent)
*/
export const watchAgentMetadata = (agentId: string): EventSource => {
return new EventSource(
`${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`,
{ withCredentials: true },
);
};
type WatchBuildLogsByTemplateVersionIdOptions = {
after?: number;
onMessage: (log: TypesGen.ProvisionerJobLog) => void;
onDone: () => void;
onError: (error: Error) => void;
};
export const watchBuildLogsByTemplateVersionId = (
versionId: string,
{
onMessage,
onDone,
onError,
after,
}: WatchBuildLogsByTemplateVersionIdOptions,
) => {
const searchParams = new URLSearchParams({ follow: "true" });
if (after !== undefined) {
searchParams.append("after", after.toString());
}
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const socket = new WebSocket(
`${proto}//${
location.host
}/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`,
);
socket.binaryType = "blob";
socket.addEventListener("message", (event) =>
onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog),
);
socket.addEventListener("error", () => {
onError(new Error("Connection for logs failed."));
socket.close();
});
socket.addEventListener("close", () => {
// When the socket closes, logs have finished streaming!
onDone();
});
return socket;
};
type WatchWorkspaceAgentLogsOptions = {
after: number;
onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void;
onDone?: () => void;
onError: (error: Error) => void;
};
export const watchWorkspaceAgentLogs = (
agentId: string,
{ after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions,
) => {
// WebSocket compression in Safari (confirmed in 16.5) is broken when
// the server sends large messages. The following error is seen:
//
// WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldnt be completed. Protocol error
//
const noCompression =
userAgentParser(navigator.userAgent).browser.name === "Safari"
? "&no_compression"
: "";
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const socket = new WebSocket(
`${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`,
);
socket.binaryType = "blob";
socket.addEventListener("message", (event) => {
const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[];
onMessage(logs);
});
socket.addEventListener("error", () => {
onError(new Error("socket errored"));
});
socket.addEventListener("close", () => {
onDone && onDone();
});
return socket;
};
type WatchBuildLogsByBuildIdOptions = {
after?: number;
onMessage: (log: TypesGen.ProvisionerJobLog) => void;
onDone?: () => void;
onError?: (error: Error) => void;
};
export const watchBuildLogsByBuildId = (
buildId: string,
{ onMessage, onDone, onError, after }: WatchBuildLogsByBuildIdOptions,
) => {
const searchParams = new URLSearchParams({ follow: "true" });
if (after !== undefined) {
searchParams.append("after", after.toString());
}
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const socket = new WebSocket(
`${proto}//${
location.host
}/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`,
);
socket.binaryType = "blob";
socket.addEventListener("message", (event) =>
onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog),
);
socket.addEventListener("error", () => {
onError && onError(new Error("Connection for logs failed."));
socket.close();
});
socket.addEventListener("close", () => {
// When the socket closes, logs have finished streaming!
onDone && onDone();
});
return socket;
};
export const issueReconnectingPTYSignedToken = async (
params: TypesGen.IssueReconnectingPTYSignedTokenRequest,
): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => {
const response = await axios.post(
"/api/v2/applications/reconnecting-pty-signed-token",
params,
);
return response.data;
};
export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => {
const latestBuild = workspace.latest_build;
const [templateVersionRichParameters, buildParameters] = await Promise.all([
getTemplateVersionRichParameters(latestBuild.template_version_id),
getWorkspaceBuildParameters(latestBuild.id),
]);
return {
templateVersionRichParameters,
buildParameters,
};
};
export type InsightsParams = {
start_time: string;
end_time: string;
template_ids: string;
};
export const getInsightsUserLatency = async (
filters: InsightsParams,
): Promise<TypesGen.UserLatencyInsightsResponse> => {
const params = new URLSearchParams(filters);
const response = await axios.get(`/api/v2/insights/user-latency?${params}`);
return response.data;
};
export const getInsightsUserActivity = async (
filters: InsightsParams,
): Promise<TypesGen.UserActivityInsightsResponse> => {
const params = new URLSearchParams(filters);
const response = await axios.get(`/api/v2/insights/user-activity?${params}`);
return response.data;
};
export type InsightsTemplateParams = InsightsParams & {
interval: "day" | "week";
};
export const getInsightsTemplate = async (
params: InsightsTemplateParams,
): Promise<TypesGen.TemplateInsightsResponse> => {
const searchParams = new URLSearchParams(params);
const response = await axios.get(
`/api/v2/insights/templates?${searchParams}`,
);
return response.data;
};
export interface Health {
healthy: boolean;
time: string;
coder_version: string;
access_url: { healthy: boolean };
database: { healthy: boolean };
derp: { healthy: boolean };
websocket: { healthy: boolean };
}
export const getHealth = async () => {
const response = await axios.get<Health>("/api/v2/debug/health");
return response.data;
};