From f3c76c79eeaeb7ca2895d991bad09aa61e3b194e Mon Sep 17 00:00:00 2001 From: Andrew Atimapre Date: Mon, 30 Oct 2023 18:23:23 +0100 Subject: [PATCH] Checkly group level sync support --- .../v1/integrationAuthController.ts | 36 ++++++- backend/src/integrations/groups.ts | 96 +++++++++++++++++++ backend/src/integrations/index.ts | 2 + backend/src/routes/v1/integrationAuth.ts | 8 ++ backend/src/validation/integrationAuth.ts | 6 ++ .../src/hooks/api/integrationAuth/index.tsx | 3 +- .../src/hooks/api/integrationAuth/queries.tsx | 19 +++- .../src/hooks/api/integrationAuth/types.ts | 5 + .../src/pages/integrations/checkly/create.tsx | 48 +++++++++- 9 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 backend/src/integrations/groups.ts diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 06b245659..5138e4391 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { standardRequest } from "../../config/request"; -import { getApps, getTeams, revokeAccess } from "../../integrations"; +import { getApps, getTeams, getGroups, revokeAccess } from "../../integrations"; import { Bot, IntegrationAuth, Workspace } from "../../models"; import { EventType } from "../../ee/models"; import { IntegrationService } from "../../services"; @@ -208,6 +208,40 @@ export const saveIntegrationToken = async (req: Request, res: Response) => { }); }; +/** + * Return list of groups allowed for integration with integration authorization id [integrationAuthId] + * @param req + * @param res + * @returns + */ +export const getIntegrationAuthGroups = async (req: Request, res: Response) => { + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.GetIntegrationAuthGroupsV1, req); + + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + + const groups = await getGroups({ + integrationAuth: integrationAuth, + accessToken: accessToken + }); + + return res.status(200).send({ + groups + }); +}; + /** * Return list of applications allowed for integration with integration authorization id [integrationAuthId] * @param req diff --git a/backend/src/integrations/groups.ts b/backend/src/integrations/groups.ts new file mode 100644 index 000000000..c69a21bbd --- /dev/null +++ b/backend/src/integrations/groups.ts @@ -0,0 +1,96 @@ +import { + IIntegrationAuth, +} from "../models"; +import { + INTEGRATION_CHECKLY, + INTEGRATION_CHECKLY_API_URL, +} from "../variables"; +import { standardRequest } from "../config/request"; + +interface Group { + name: string; + groupId: string; +} + +/** + * Return list of groups for checkly integration authorization [integrationAuth] + * @param {Object} obj + * @param {String} obj.integrationAuth - integration authorization to get groups + * @param {String} obj.accessToken - access token for integration authorization + * @returns {Object[]} groups - groups for integration authorization + * @returns {String} groups.name - name of group + * @returns {String} groups.groupId - id of group +*/ +const getGroups = async ({ + integrationAuth, + accessToken, +}: { + integrationAuth: IIntegrationAuth; + accessToken: string; +}) => { + + let groups: Group[] = []; + + switch (integrationAuth.integration) { + case INTEGRATION_CHECKLY: + groups = await getGroupsCheckly({ + accessToken, + }); + break; + } + + return groups; +} + +/** + * Return list of groups for Checkly integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Checkly API + * @returns {Object[]} groups - list of groups in Checkly + * @returns {String} groups.name - name of group + * @returns {String} groups.groupId - id of group +*/ +const getGroupsCheckly = async ({ + accessToken, +}: { + accessToken: string; +}) => { + + let groups: Group[] = []; + + // case: fetch account id + const { data } = await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/accounts`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); + + const accountId = data.map((a: any) => { + return { + id: a.id, + }; + }); + + // case: fetch list of groups in Checkly + const res = accountId.length > 0 && ( + await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/check-groups`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": accountId[0].id, + } + }) + ).data; + + groups = res.map((g: any) => ({ + name: g.name, + groupId: g.id, + })); + + return groups; +} + +export { + getGroups, +} \ No newline at end of file diff --git a/backend/src/integrations/index.ts b/backend/src/integrations/index.ts index e1bf23ba1..e5b6ad0ea 100644 --- a/backend/src/integrations/index.ts +++ b/backend/src/integrations/index.ts @@ -2,6 +2,7 @@ import { exchangeCode } from "./exchange"; import { exchangeRefresh } from "./refresh"; import { getApps } from "./apps"; import { getTeams } from "./teams"; +import { getGroups } from "./groups"; import { revokeAccess } from "./revoke"; export { @@ -9,5 +10,6 @@ export { exchangeRefresh, getApps, getTeams, + getGroups, revokeAccess, } \ No newline at end of file diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index e28874788..d5d81cf13 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -52,6 +52,14 @@ router.get( integrationAuthController.getIntegrationAuthTeams ); +router.get( + "/:integrationAuthId/groups", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthGroups +); + router.get( "/:integrationAuthId/vercel/branches", requireAuth({ diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index 65eee4477..9f3798ad6 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -117,6 +117,12 @@ export const GetIntegrationAuthVercelBranchesV1 = z.object({ }) }); +export const GetIntegrationAuthGroupsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + export const GetIntegrationAuthQoveryOrgsV1 = z.object({ params: z.object({ integrationAuthId: z.string().trim() diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index 7e7c355ca..2a9147ffe 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -10,5 +10,6 @@ export { useGetIntegrationAuthTeamCityBuildConfigs, useGetIntegrationAuthTeams, useGetIntegrationAuthVercelBranches, - useSaveIntegrationAccessToken + useSaveIntegrationAccessToken, + useGetIntegrationAuthGroups } from "./queries"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 9213919e0..71ee4f535 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -12,7 +12,8 @@ import { Org, Project, Service, - Team, + Team, + Group, TeamCityBuildConfig} from "./types"; const integrationAuthKeys = { @@ -22,6 +23,8 @@ const integrationAuthKeys = { [{ integrationAuthId, teamId, workspaceSlug }, "integrationAuthApps"] as const, getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthTeams"] as const, + getIntegrationAuthGroups: (integrationAuthId: string) => + [{ integrationAuthId }, "integrationAuthGroups"] as const, getIntegrationAuthVercelBranches: ({ integrationAuthId, appId @@ -125,6 +128,12 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => { return data.teams; }; +const fetchIntegrationAuthGroups = async (integrationAuthId: string) => { + const { data } = await apiRequest.get<{ groups: Group[] }>( + `/api/v1/integration-auth/${integrationAuthId}/groups` + ); + return data.groups; +}; const fetchIntegrationAuthVercelBranches = async ({ integrationAuthId, @@ -413,6 +422,14 @@ export const useGetIntegrationAuthVercelBranches = ({ }); }; +export const useGetIntegrationAuthGroups = (integrationAuthId: string) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthGroups(integrationAuthId), + queryFn: () => fetchIntegrationAuthGroups(integrationAuthId), + enabled: true + }); +}; + export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId), diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 2292e3222..e3f9af247 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -26,6 +26,11 @@ export type Environment = { environmentId: string; }; +export type Group = { + name: string; + groupId: string; +}; + export type Container = { name: string; containerId: string; diff --git a/frontend/src/pages/integrations/checkly/create.tsx b/frontend/src/pages/integrations/checkly/create.tsx index 4393dfc0e..9c203bb4c 100644 --- a/frontend/src/pages/integrations/checkly/create.tsx +++ b/frontend/src/pages/integrations/checkly/create.tsx @@ -9,6 +9,8 @@ import { motion } from "framer-motion"; import queryString from "query-string"; import { + Alert, + AlertDescription, Button, Card, CardTitle, @@ -27,7 +29,8 @@ import { import { useGetIntegrationAuthApps, - useGetIntegrationAuthById + useGetIntegrationAuthById, + useGetIntegrationAuthGroups } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; @@ -47,6 +50,9 @@ export default function ChecklyCreateIntegrationPage() { const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" }); + const { data: integrationAuthGroups, isLoading: isintegrationAuthGroupsLoading } = useGetIntegrationAuthGroups( + (integrationAuthId as string) ?? "" + ); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -55,6 +61,9 @@ export default function ChecklyCreateIntegrationPage() { const [targetApp, setTargetApp] = useState(""); const [targetAppId, setTargetAppId] = useState(""); + const [targetGroup, setTargetGroup] = useState(""); + const [targetGroupId, setTargetGroupId] = useState(""); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { @@ -104,9 +113,10 @@ export default function ChecklyCreateIntegrationPage() { return integrationAuth && workspace && selectedSourceEnvironment && - integrationAuthApps && + integrationAuthApps && + integrationAuthGroups && targetApp ? ( -
+
Set Up Checkly Integration @@ -175,6 +185,36 @@ export default function ChecklyCreateIntegrationPage() { placeholder="Provide a path, default is /" /> + + + + + + By default environment variables are synced to the global level, select a group above to sync at the Group level. + +