From ab715c030eb89d9cbc9f5709ae5051f8c785c0ef Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Tue, 19 Dec 2023 11:54:11 +1100 Subject: [PATCH 01/14] chore: make spa wrapper width variable --- spa/src/common/Wrapper.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spa/src/common/Wrapper.tsx b/spa/src/common/Wrapper.tsx index 3e0b41903..cf882a204 100644 --- a/spa/src/common/Wrapper.tsx +++ b/spa/src/common/Wrapper.tsx @@ -9,16 +9,16 @@ const navHeight = 56; const wrapperStyle = css` padding: 20px 40px 0px 40px; `; -const wrapperCenterStyle = css` +const getWrapperCenterStyle = (width: string | undefined) => css` margin: 0 auto; - max-width: 580px; + max-width: ${width ? width: "580px"}; height: calc(100vh - ${navHeight * 2}px); display: flex; flex-direction: column; justify-content: center; `; -export const Wrapper = (attr: { hideClosedBtn?: boolean, children?: ReactNode | undefined }) => { +export const Wrapper = (attr: { hideClosedBtn?: boolean, children?: ReactNode | undefined, width?:string }) => { const navigateToHomePage = () => { analyticsClient.sendUIEvent({ actionSubject: "dropExperienceViaBackButton", action: "clicked" }); AP.getLocation((location: string) => { @@ -26,6 +26,7 @@ export const Wrapper = (attr: { hideClosedBtn?: boolean, children?: ReactNode | AP.navigator.go( "site", { absoluteUrl: `${locationUrl.origin}/jira/marketplace/discover/app/com.github.integration.production` }); }); }; + const wrapperCenterStyle = getWrapperCenterStyle(attr.width || undefined); return (
@@ -37,7 +38,6 @@ export const Wrapper = (attr: { hideClosedBtn?: boolean, children?: ReactNode | onClick={navigateToHomePage} /> } -
{attr.children}
); From 988694eda7fc000e418f79cb8c3dc1bc27528053 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Tue, 19 Dec 2023 11:55:16 +1100 Subject: [PATCH 02/14] chore: rectify colum names --- spa/src/utils/dynamicTableHelper.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spa/src/utils/dynamicTableHelper.tsx b/spa/src/utils/dynamicTableHelper.tsx index f42aa3f87..8a2a605c1 100644 --- a/spa/src/utils/dynamicTableHelper.tsx +++ b/spa/src/utils/dynamicTableHelper.tsx @@ -63,17 +63,17 @@ const createHead = (withWidth: boolean) => { cells: [ { key: "name", - content: "Name", + content: "Connected organization", width: withWidth ? 30 : undefined, }, { key: "repos", - content: "Repos", + content: "Repository access", width: withWidth ? 30 : undefined, }, { key: "status", - content: "Status", + content: "Backfill status", width: withWidth ? 30 : undefined, }, { @@ -180,7 +180,8 @@ export const getGHSubscriptionsRows = ( ), - },{ + }, + { key: cloudConnection.id, content: (
From b3e4d207f7a0bb7f4ed7ec518fae9acad586a272 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Thu, 21 Dec 2023 10:13:09 +1100 Subject: [PATCH 03/14] chore: work to delete ghe server --- spa/src/api/subscriptions/index.ts | 2 + .../Connections/GHCloudConnections/index.tsx | 54 ++------ .../GHEnterpriseApplication.tsx | 104 +++++++++++++-- .../GHEnterpriseConnections/index.tsx | 84 +++++++++--- .../Modals/DisconnectGHEServerModal.tsx | 112 ++++++++++++++++ .../Modals/DisconnectSubscriptionModal.tsx | 85 +++++++------ spa/src/pages/Connections/index.tsx | 120 +++++++++++++++--- .../services/subscription-manager/index.ts | 19 +++ spa/src/utils/dynamicTableHelper.tsx | 6 +- src/rest-interfaces/index.ts | 2 +- src/rest/rest-router.ts | 5 + .../routes/enterprise/delete-ghe-server.ts | 50 ++++++++ src/rest/routes/enterprise/index.ts | 1 + src/rest/routes/subscriptions/sync.ts | 11 +- 14 files changed, 521 insertions(+), 134 deletions(-) create mode 100644 spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx create mode 100644 src/rest/routes/enterprise/delete-ghe-server.ts create mode 100644 src/rest/routes/enterprise/index.ts diff --git a/spa/src/api/subscriptions/index.ts b/spa/src/api/subscriptions/index.ts index 85c894bc3..b11c076b3 100644 --- a/spa/src/api/subscriptions/index.ts +++ b/spa/src/api/subscriptions/index.ts @@ -3,6 +3,8 @@ import { RestSyncReqBody } from "~/src/rest-interfaces"; export default { getSubscriptions: () => axiosRest.get("/rest/subscriptions"), + deleteGHEServer: (uuid: string) => + axiosRest.delete(`/rest/app/${uuid}`), deleteSubscription: (subscriptionId: number) => axiosRest.delete(`/rest/app/cloud/subscriptions/${subscriptionId}`), syncSubscriptions: (subscriptionId: number, reqBody: RestSyncReqBody) => diff --git a/spa/src/pages/Connections/GHCloudConnections/index.tsx b/spa/src/pages/Connections/GHCloudConnections/index.tsx index ceef92608..91a4e955b 100644 --- a/spa/src/pages/Connections/GHCloudConnections/index.tsx +++ b/spa/src/pages/Connections/GHCloudConnections/index.tsx @@ -1,16 +1,14 @@ /** @jsxImportSource @emotion/react */ -import { useState } from "react"; +import { Box, xcss } from "@atlaskit/primitives"; import { DynamicTableStateless } from "@atlaskit/dynamic-table"; import { head, getGHSubscriptionsRows, } from "../../../utils/dynamicTableHelper"; -import { BackfillPageModalTypes, GhCloudSubscriptions } from "../../../rest-interfaces"; -import { Box, xcss } from "@atlaskit/primitives"; -import { SuccessfulConnection } from "rest-interfaces"; -import DisconnectSubscriptionModal from "../Modals/DisconnectSubscriptionModal"; -import RestartBackfillModal from "../Modals/RestartBackfillModal"; -import { ModalTransition } from "@atlaskit/modal-dialog"; +import { + BackfillPageModalTypes, + GhCloudSubscriptions, SuccessfulConnection, +} from "../../../rest-interfaces"; const containerStyles = xcss({ display: "flex", @@ -19,39 +17,17 @@ const containerStyles = xcss({ type GitHubCloudConnectionsProps = { ghCloudSubscriptions: GhCloudSubscriptions; - refetch: () => void; + setDataForModal: (dataForModal: SuccessfulConnection | undefined) => void, + setSelectedModal: (selectedModal:BackfillPageModalTypes) => void, + setIsModalOpened: (isModalOpen: boolean) => void, }; const GitHubCloudConnections = ({ ghCloudSubscriptions, - refetch, + setIsModalOpened, + setDataForModal, + setSelectedModal, }: GitHubCloudConnectionsProps) => { - const [isModalOpened, setIsModalOpened] = useState(false); - const [subscriptionForModal, setSubscriptionForModal] = useState(undefined); - const [selectedModal, setSelectedModal] = useState("BACKFILL"); - - const openedModal = (refetch: () => void) => { - switch (selectedModal) { - case "BACKFILL": - return (); - case "DISCONNECT_SUBSCRIPTION": - return ; - // TODO: Create modals for GHE later - case "DISCONNECT_SERVER": - case "DISCONNECT_SERVER_APP": - default: - return <>; - } - }; - return ( <> @@ -59,18 +35,12 @@ const GitHubCloudConnections = ({ head={head} rows={getGHSubscriptionsRows( ghCloudSubscriptions.successfulCloudConnections, - { setIsModalOpened, setSubscriptionForModal, setSelectedModal } + { setIsModalOpened, setDataForModal, setSelectedModal } )} rowsPerPage={5} page={1} /> - - - { - isModalOpened && subscriptionForModal && openedModal(refetch) - } - ); }; diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx index c5a261229..e3edb9db5 100644 --- a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx +++ b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx @@ -4,16 +4,39 @@ import { useState } from "react"; import Heading from "@atlaskit/heading"; import ChevronRightIcon from "@atlaskit/icon/glyph/chevron-right"; import ChevronDownIcon from "@atlaskit/icon/glyph/chevron-down"; -import { head, getGHSubscriptionsRows } from "../../../utils/dynamicTableHelper"; -import { GitHubEnterpriseApplication } from "../../../rest-interfaces"; +import { + head, + getGHSubscriptionsRows, +} from "../../../utils/dynamicTableHelper"; +import { + BackfillPageModalTypes, + GitHubEnterpriseApplication, + SuccessfulConnection, +} from "../../../rest-interfaces"; import { css } from "@emotion/react"; +const connectNewAppLinkStyle = css` + text-decoration: none; +`; + const wrapperStyle = css` display: flex; - align-items: center; + align-items: baseline; flex-direction: column; `; +const noConnectionsHeaderStyle = css` + padding-left: 25px; + padding-bottom: 10px; + padding-top: 10px; +`; + +const noConnectionsBodyStyle = css` + padding-left: 25px; + padding-bottom: 10px; + padding-top: 10px; +`; + const applicationHeaderStyle = css` cursor: pointer; display: flex; @@ -29,12 +52,42 @@ const applicationContentStyle = css` type GitHubEnterpriseApplicationProps = { application: GitHubEnterpriseApplication; + setDataForModal: ( + dataForModal: SuccessfulConnection | undefined + ) => void; + setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; + setIsModalOpened: (isModalOpen: boolean) => void; }; +function openChildWindow(url: string) { + const child: Window | null = window.open(url); + const interval = setInterval(function () { + if (child?.closed) { + clearInterval(interval); + AP.navigator.reload(); + } + }, 100); + return child; +} + const GitHubEnterpriseApp = ({ application, + setIsModalOpened, + setDataForModal, + setSelectedModal, }: GitHubEnterpriseApplicationProps) => { const [showAppContent, setShowAppContent] = useState(true); + const onConnectNewApp = () => { + return AP.context.getToken((token: string) => { + const child: Window | null = openChildWindow( + `/session/github/${application.uuid}/configuration?ghRedirect=to` + ); + if (child) { + /* eslint-disable @typescript-eslint/no-explicit-any*/ + (child as any).window.jwt = token; + } + }); + }; return (
)} + {application.gitHubAppName}
{showAppContent && ( -
- -
+ <> + {application.successfulConnections.length > 0 ? ( +
+ +
+ ) : ( + <> +
+ Connected organizations +
+
+ No connected organizations. + + {" "} + Connect a GitHub organization. + +
+ + )} + )}
); diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx index 424acd913..0a43a8bd8 100644 --- a/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx +++ b/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx @@ -1,10 +1,17 @@ /** @jsxImportSource @emotion/react */ import { token } from "@atlaskit/tokens"; -import { Box, xcss } from "@atlaskit/primitives"; +import { first } from "lodash"; +import { Box, xcss, Flex } from "@atlaskit/primitives"; import Heading from "@atlaskit/heading"; -import { GhEnterpriseServer } from "../../../rest-interfaces"; -import GitHubEnterpriseApplication from "./GHEnterpriseApplication"; +import Button from "@atlaskit/button"; import { css } from "@emotion/react"; +import { + BackfillPageModalTypes, + GhEnterpriseServer, + SuccessfulConnection, + GitHubEnterpriseApplication +} from "../../../rest-interfaces"; +import GHEApplication from "./GHEnterpriseApplication"; const enterpriserServerHeaderStyle = css` display: flex; @@ -14,6 +21,11 @@ const enterpriserServerHeaderStyle = css` justify-content: space-between; `; +const enterpriserAppsHeaderStyle = css` + padding-left: 25px; + padding-bottom: 30px; +`; + const containerStyles = xcss({ display: "flex", flexDirection: "column", @@ -26,6 +38,11 @@ const containerStyles = xcss({ boxShadow: "elevation.shadow.raised", }); +const containerHeaderStyle = xcss({ + width: "100%", + justifyContent: "space-between", +}); + const whiteBoxStyle = xcss({ display: "flex", flexDirection: "column", @@ -36,30 +53,63 @@ const whiteBoxStyle = xcss({ marginBottom: `${token("space.200")}`, boxShadow: "elevation.shadow.raised", backgroundColor: "elevation.surface.raised", - width: "100%" + width: "100%", }); type GitHubEnterpriseConnectionsProps = { ghEnterpriseServers: GhEnterpriseServer[]; + setDataForModal: ( + dataForModal: SuccessfulConnection | GitHubEnterpriseApplication | undefined + ) => void; + setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; + setIsModalOpened: (isModalOpen: boolean) => void; }; const GitHubEnterpriseConnections = ({ ghEnterpriseServers, + setIsModalOpened, + setDataForModal, + setSelectedModal, }: GitHubEnterpriseConnectionsProps) => { - return <> - { - ghEnterpriseServers.map((connection) => { + console.log(":::::::::",JSON.stringify(ghEnterpriseServers)); + return ( + <> + {ghEnterpriseServers.map((connection) => { return ( - -
- {connection.gitHubBaseUrl} -
- - {connection.applications.map((application) => ())} + <> + + +
+ {connection.gitHubBaseUrl} +
+ +
+ + +
+ APPLICATIONS +
+ {connection.applications.map((application) => ( + + ))} +
-
+ ); - }) - } - ; + })} + + ); }; export default GitHubEnterpriseConnections; diff --git a/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx b/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx new file mode 100644 index 000000000..9be9dace1 --- /dev/null +++ b/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { AxiosError } from "axios"; +import Modal, { + ModalBody, + ModalFooter, + ModalHeader, + ModalTitle, +} from "@atlaskit/modal-dialog"; +import Button, { LoadingButton } from "@atlaskit/button"; +import { BackfillPageModalTypes, GitHubEnterpriseApplication } from "../../../../../src/rest-interfaces"; +import SubscriptionManager from "../../../services/subscription-manager"; + +/** + * NOTE: While testing in dev mode, please disable the React.StrictMode first, + * otherwise this modal won't show up. + */ +const DisconnectGHEServerModal = ({ + gheServer, + setIsModalOpened, + setSelectedModal, +}: { + gheServer: GitHubEnterpriseApplication; + setIsModalOpened: (x: boolean) => void; + setSelectedModal: (selectedModal:BackfillPageModalTypes) => void; +}) => { + const [isLoading, setIsLoading] = useState(false); + + const disconnect = async () => { + setIsLoading(true); + const response: boolean | AxiosError = + await SubscriptionManager.deleteGHEServer(gheServer.uuid); + if (response instanceof AxiosError) { + // TODO: Handle the error once we have the designs + console.error("Error", response); + } else { + setSelectedModal("DELETE_GHE_APP"); + } + }; + + return ( + setIsModalOpened(false)}> + + + Are you sure you want to disconnect this server? + + + +

+ To reconnect this server, you'll need to create new GitHub apps and + import data about its organizations and repositories again. +

+
+ + + {isLoading ? ( + + Loading button + + ) : ( + + )} + +
+ ); +}; + +const DeleteAppsInGitHubModal = ({ + gheServer, + setIsModalOpened, + refetch, +}: { + gheServer: GitHubEnterpriseApplication; + setIsModalOpened: (x: boolean) => void; + refetch: () => void; +}) => { + const { gitHubAppName, gitHubBaseUrl } = gheServer; + return ( + setIsModalOpened(false)}> + Server disconnected + +

+ You can now delete these unused apps from your GitHub server. Select + the app, then in GitHub select Delete GitHub app. +

+ +
+ + + +
+ ); +}; + +export { DisconnectGHEServerModal, DeleteAppsInGitHubModal }; diff --git a/spa/src/pages/Connections/Modals/DisconnectSubscriptionModal.tsx b/spa/src/pages/Connections/Modals/DisconnectSubscriptionModal.tsx index 05c12cbc6..78c0143ac 100644 --- a/spa/src/pages/Connections/Modals/DisconnectSubscriptionModal.tsx +++ b/spa/src/pages/Connections/Modals/DisconnectSubscriptionModal.tsx @@ -1,6 +1,11 @@ import { useState } from "react"; import { AxiosError } from "axios"; -import Modal, { ModalBody, ModalFooter, ModalHeader, ModalTitle } from "@atlaskit/modal-dialog"; +import Modal, { + ModalBody, + ModalFooter, + ModalHeader, + ModalTitle, +} from "@atlaskit/modal-dialog"; import Button, { LoadingButton } from "@atlaskit/button"; import { SuccessfulConnection } from "../../../../../src/rest-interfaces"; import SubscriptionManager from "../../../services/subscription-manager"; @@ -9,16 +14,21 @@ import SubscriptionManager from "../../../services/subscription-manager"; * NOTE: While testing in dev mode, please disable the React.StrictMode first, * otherwise this modal won't show up. */ -const DisconnectSubscriptionModal = ({ subscription, setIsModalOpened, refetch }: { - subscription: SuccessfulConnection, - setIsModalOpened: (x: boolean) => void, - refetch: () => void +const DisconnectSubscriptionModal = ({ + subscription, + setIsModalOpened, + refetch, +}: { + subscription: SuccessfulConnection; + setIsModalOpened: (x: boolean) => void; + refetch: () => void; }) => { const [isLoading, setIsLoading] = useState(false); const disconnect = async () => { setIsLoading(true); - const response: boolean | AxiosError = await SubscriptionManager.deleteSubscription(subscription.subscriptionId); + const response: boolean | AxiosError = + await SubscriptionManager.deleteSubscription(subscription.subscriptionId); if (response instanceof AxiosError) { // TODO: Handle the error once we have the designs console.error("Error", response); @@ -29,38 +39,39 @@ const DisconnectSubscriptionModal = ({ subscription, setIsModalOpened, refetch } }; return ( - <> - setIsModalOpened(false)}> - - - <>Disconnect {subscription.account.login}? - - - -

- Are you sure you want to disconnect your organization {subscription.account.login}? - This means that you will have to redo the backfill of historical data if you ever want to reconnect -

-
- - + {isLoading ? ( + + Loading button + + ) : ( + - { - isLoading ? - Loading button - : - } - -
- + )} + + ); }; -export default DisconnectSubscriptionModal; +export default DisconnectSubscriptionModal; diff --git a/spa/src/pages/Connections/index.tsx b/spa/src/pages/Connections/index.tsx index 6b02c82bd..a42c5d66f 100644 --- a/spa/src/pages/Connections/index.tsx +++ b/spa/src/pages/Connections/index.tsx @@ -1,19 +1,83 @@ import { useEffect, useState } from "react"; +import { ModalTransition } from "@atlaskit/modal-dialog"; +import { AxiosError } from "axios"; +import { useNavigate } from "react-router-dom"; import SyncHeader from "../../components/SyncHeader"; import Step from "../../components/Step"; import { Wrapper } from "../../common/Wrapper"; import GitHubCloudConnections from "./GHCloudConnections"; import GitHubEnterpriseConnections from "./GHEnterpriseConnections"; -import { GHSubscriptions } from "../../rest-interfaces"; +import { + GHSubscriptions, + BackfillPageModalTypes, + SuccessfulConnection, + GitHubEnterpriseApplication, +} from "../../rest-interfaces"; import SkeletonForLoading from "./SkeletonForLoading"; -import { useNavigate } from "react-router-dom"; import SubscriptionManager from "../../services/subscription-manager"; -import { AxiosError } from "axios"; +import RestartBackfillModal from "./Modals/RestartBackfillModal"; +import DisconnectSubscriptionModal from "./Modals/DisconnectSubscriptionModal"; +import { DisconnectGHEServerModal, DeleteAppsInGitHubModal } from "./Modals/DisconnectGHEServerModal"; + +const hasGHCloudConnections = (subscriptions: GHSubscriptions): boolean => + subscriptions?.ghCloudSubscriptions && + subscriptions?.ghCloudSubscriptions?.successfulCloudConnections && + subscriptions?.ghCloudSubscriptions?.successfulCloudConnections.length > 0; const Connections = () => { const navigate = useNavigate(); + ////////// + const [isModalOpened, setIsModalOpened] = useState(false); + const [selectedModal, setSelectedModal] = + useState("BACKFILL"); + const [dataForModal, setDataForModal] = useState< + SuccessfulConnection | GitHubEnterpriseApplication | undefined + >(undefined); + const openedModal = () => { + switch (selectedModal) { + case "BACKFILL": + return ( + + ); + case "DISCONNECT_SUBSCRIPTION": + return ( + + ); + // TODO: Create modals for GHE later + case "DISCONNECT_SERVER": + return ( + + ); + case "DELETE_GHE_APP": + return ( + + ); + case "DISCONNECT_SERVER_APP": + default: + return <>; + } + }; + ////////// const [isLoading, setIsLoading] = useState(false); - const [subscriptions, setSubscriptions] = useState(null); + const [subscriptions, setSubscriptions] = useState( + null + ); const fetchGHSubscriptions = async () => { setIsLoading(true); const response = await SubscriptionManager.getSubscriptions(); @@ -30,29 +94,49 @@ const Connections = () => { // If there are no connections then go back to the start page useEffect(() => { - if (!subscriptions?.ghCloudSubscriptions && subscriptions?.ghEnterpriseServers && subscriptions.ghEnterpriseServers?.length === 0) { + if ( + !subscriptions?.ghCloudSubscriptions && + subscriptions?.ghEnterpriseServers && + subscriptions.ghEnterpriseServers?.length === 0 + ) { navigate("/spa"); } }, [subscriptions, navigate]); return ( - + - { - isLoading ? : <> - { - subscriptions?.ghCloudSubscriptions && - + {isLoading ? ( + + ) : ( + <> + {subscriptions && hasGHCloudConnections(subscriptions) && ( + + - } + )} - { - subscriptions?.ghEnterpriseServers && subscriptions.ghEnterpriseServers?.length > 0 && - - - } + {subscriptions?.ghEnterpriseServers && + subscriptions.ghEnterpriseServers?.length > 0 && ( + + + + )} + + {isModalOpened && dataForModal && openedModal()} + - } + )} ); }; diff --git a/spa/src/services/subscription-manager/index.ts b/spa/src/services/subscription-manager/index.ts index c32b006c1..895fb44ed 100644 --- a/spa/src/services/subscription-manager/index.ts +++ b/spa/src/services/subscription-manager/index.ts @@ -56,9 +56,28 @@ async function deleteSubscription(subscriptionId: number): Promise { + try { + const response= await Api.subscriptions.deleteGHEServer(uuid); + const isSuccessful = response.status === 204; + if(!isSuccessful) { + reportError( + { message: "Response status for deleting GHE server is not 204", status: response.status }, + { path: "deleteGHEServer" } + ); + } + + return isSuccessful; + } catch (e: unknown) { + reportError(new Error("Unable to delete GHE server", { cause: e }), { path: "deleteGHEServer" }); + return e as AxiosError; + } +} + export default { getSubscriptions, deleteSubscription, + deleteGHEServer, syncSubscription }; diff --git a/spa/src/utils/dynamicTableHelper.tsx b/spa/src/utils/dynamicTableHelper.tsx index 8a2a605c1..ec15c5a4f 100644 --- a/spa/src/utils/dynamicTableHelper.tsx +++ b/spa/src/utils/dynamicTableHelper.tsx @@ -19,7 +19,7 @@ type Row = { type ConnectionsActionsCallback = { setIsModalOpened: (x: boolean) => void; - setSubscriptionForModal: (sub: SuccessfulConnection) => void; + setDataForModal: (sub: SuccessfulConnection) => void; setSelectedModal: (x: BackfillPageModalTypes) => void; }; @@ -205,7 +205,7 @@ export const getGHSubscriptionsRows = ( { callbacks?.setIsModalOpened(true); - callbacks?.setSubscriptionForModal(cloudConnection); + callbacks?.setDataForModal(cloudConnection); callbacks?.setSelectedModal("BACKFILL"); }} > @@ -214,7 +214,7 @@ export const getGHSubscriptionsRows = ( { callbacks?.setIsModalOpened(true); - callbacks?.setSubscriptionForModal(cloudConnection); + callbacks?.setDataForModal(cloudConnection); callbacks?.setSelectedModal("DISCONNECT_SUBSCRIPTION"); }} > diff --git a/src/rest-interfaces/index.ts b/src/rest-interfaces/index.ts index 6e25588ac..03aedd0dc 100644 --- a/src/rest-interfaces/index.ts +++ b/src/rest-interfaces/index.ts @@ -186,4 +186,4 @@ export type GHSubscriptions = { ghEnterpriseServers: GhEnterpriseServer[]; }; -export type BackfillPageModalTypes = "BACKFILL" | "DISCONNECT_SUBSCRIPTION" | "DISCONNECT_SERVER_APP" | "DISCONNECT_SERVER"; +export type BackfillPageModalTypes = "BACKFILL" | "DISCONNECT_SUBSCRIPTION" | "DISCONNECT_SERVER_APP" | "DISCONNECT_SERVER" | "DELETE_GHE_APP"; diff --git a/src/rest/rest-router.ts b/src/rest/rest-router.ts index a0df75f6a..fac4c802b 100644 --- a/src/rest/rest-router.ts +++ b/src/rest/rest-router.ts @@ -11,6 +11,8 @@ import { JiraAdminEnforceMiddleware } from "./middleware/jira-admin/jira-admin-c import { AnalyticsProxyHandler } from "./routes/analytics-proxy"; import { SubscriptionsRouter } from "./routes/subscriptions"; import { DeferredRouter } from "./routes/deferred"; +import { deleteEnterpriseServerHandler } from "./routes/enterprise"; + export const RestRouter = Router({ mergeParams: true }); @@ -39,6 +41,9 @@ subRouter.use("/deferred", DeferredRouter); // have done authentication only)? subRouter.use(JwtHandler); subRouter.use(JiraAdminEnforceMiddleware); +// This is to delete GHE server with specific UUID +subRouter.delete("/", deleteEnterpriseServerHandler); + subRouter.post("/analytics-proxy", AnalyticsProxyHandler); diff --git a/src/rest/routes/enterprise/delete-ghe-server.ts b/src/rest/routes/enterprise/delete-ghe-server.ts new file mode 100644 index 000000000..0a633eaa2 --- /dev/null +++ b/src/rest/routes/enterprise/delete-ghe-server.ts @@ -0,0 +1,50 @@ +import { Request, Response } from "express"; +import { ParamsDictionary } from "express-serve-static-core"; +import { errorWrapper } from "../../helper"; +import { BaseLocals } from ".."; +import { EnterpriseServerDeleteReqBody } from "~/src/rest-interfaces"; +import { GitHubServerApp } from "~/src/models/github-server-app"; +import { isConnected } from "~/src/util/is-connected"; +import { saveConfiguredAppProperties } from "~/src/util/app-properties-utils"; +import { InvalidArgumentError } from "~/src/config/errors"; + +const deleteEnterpriseServer = async ( + req: Request, + res: Response +): Promise => { + const { installation } = res.locals; + + const cloudOrUUID = req.params.cloudOrUUID; + if (!cloudOrUUID) { + throw new InvalidArgumentError( + "Invalid route, couldn't determine UUID of enterprise server!" + ); + } + + // TODO: Check and add test cases for GHE later + const gitHubApp = await GitHubServerApp.getForUuidAndInstallationId( + cloudOrUUID, + installation.id + ); + const gitHubBaseUrl = gitHubApp?.gitHubBaseUrl; + if (!gitHubBaseUrl) { + throw new InvalidArgumentError( + "Invalid route, couldn't determine gitHubBaseUrl for enterprise server!" + ); + } + + await GitHubServerApp.uninstallServer( + gitHubBaseUrl, + installation.id + ); + + if (!(await isConnected(installation.jiraHost))) { + await saveConfiguredAppProperties(jiraHost, req.log, false); + } + res.status(200).json("Success"); +}; + +export const deleteEnterpriseServerHandler = errorWrapper( + "deleteEnterpriseServerHandler", + deleteEnterpriseServer +); diff --git a/src/rest/routes/enterprise/index.ts b/src/rest/routes/enterprise/index.ts new file mode 100644 index 000000000..37a10c97b --- /dev/null +++ b/src/rest/routes/enterprise/index.ts @@ -0,0 +1 @@ +export { deleteEnterpriseServerHandler } from "./delete-ghe-server"; \ No newline at end of file diff --git a/src/rest/routes/subscriptions/sync.ts b/src/rest/routes/subscriptions/sync.ts index 4838a7926..c9787ccf3 100644 --- a/src/rest/routes/subscriptions/sync.ts +++ b/src/rest/routes/subscriptions/sync.ts @@ -7,7 +7,6 @@ import { determineSyncTypeAndTargetTasks } from "~/src/util/github-sync-helper"; import { BaseLocals } from ".."; import { InsufficientPermissionError, RestApiError } from "~/src/config/errors"; import { RestSyncReqBody } from "~/src/rest-interfaces"; -// import { GitHubServerApp } from "~/src/models/github-server-app"; const restSyncPost = async ( req: Request, @@ -18,6 +17,9 @@ const restSyncPost = async ( source, commitsFromDate: commitsFrmDate } = req.body; + const { + installation: { jiraHost } + } = res.locals; // A date to start fetching commit history(main and branch) from. const commitsFromDate = commitsFrmDate ? new Date(commitsFrmDate) : undefined; @@ -33,7 +35,7 @@ const restSyncPost = async ( if (!subscriptionId) { req.log.info( { - jiraHost: res.locals.installation.jiraHost, + jiraHost, subscriptionId }, "Subscription ID not found when retrying sync." @@ -59,7 +61,7 @@ const restSyncPost = async ( if (!subscription) { req.log.info( { - jiraHost: res.locals.installation.jiraHost, + jiraHost, subscriptionId }, "Subscription not found when retrying sync." @@ -71,13 +73,12 @@ const restSyncPost = async ( ); } - const localJiraHost = res.locals.installation.jiraHost; + const localJiraHost = jiraHost; if (subscription.jiraHost !== localJiraHost) { throw new InsufficientPermissionError("Forbidden - mismatched Jira Host"); } - const { syncType, targetTasks } = determineSyncTypeAndTargetTasks( syncTypeFromReq, subscription From fa14171c58e3ce2a18ab71e4582027a736da5fdd Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Thu, 21 Dec 2023 13:16:38 +1100 Subject: [PATCH 04/14] chore: update test cases --- src/rest/routes/enterprise/delete-ghe-server.ts | 3 +-- test/snapshots/app.test.ts.snap | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/rest/routes/enterprise/delete-ghe-server.ts b/src/rest/routes/enterprise/delete-ghe-server.ts index 0a633eaa2..d03983119 100644 --- a/src/rest/routes/enterprise/delete-ghe-server.ts +++ b/src/rest/routes/enterprise/delete-ghe-server.ts @@ -2,14 +2,13 @@ import { Request, Response } from "express"; import { ParamsDictionary } from "express-serve-static-core"; import { errorWrapper } from "../../helper"; import { BaseLocals } from ".."; -import { EnterpriseServerDeleteReqBody } from "~/src/rest-interfaces"; import { GitHubServerApp } from "~/src/models/github-server-app"; import { isConnected } from "~/src/util/is-connected"; import { saveConfiguredAppProperties } from "~/src/util/app-properties-utils"; import { InvalidArgumentError } from "~/src/config/errors"; const deleteEnterpriseServer = async ( - req: Request, + req: Request, res: Response ): Promise => { const { installation } = res.locals; diff --git a/test/snapshots/app.test.ts.snap b/test/snapshots/app.test.ts.snap index b055ba427..170605bcd 100644 --- a/test/snapshots/app.test.ts.snap +++ b/test/snapshots/app.test.ts.snap @@ -37,6 +37,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,GitHubTokenHandler,CheckOwnershipAndConnectRoute :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/deferred/?(?=/|$)^/parse/(?:([^/]+?))/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,GitHubTokenHandler,ParseRequestId +:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/?$ + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,deleteEnterpriseServerHandler :POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/analytics-proxy/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,AnalyticsProxyHandler :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/installation/?(?=/|$)^/new/?$ From fc3dad18669eb3866bad3a6a7c491288ff4d9991 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Thu, 21 Dec 2023 14:23:37 +1100 Subject: [PATCH 05/14] chore: add test cases --- .../Modals/DisconnectGHEServerModa.test.tsx | 86 +++++++++++++++++++ .../Modals/DisconnectGHEServerModal.tsx | 28 ++++-- 2 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 spa/src/pages/Connections/Modals/DisconnectGHEServerModa.test.tsx diff --git a/spa/src/pages/Connections/Modals/DisconnectGHEServerModa.test.tsx b/spa/src/pages/Connections/Modals/DisconnectGHEServerModa.test.tsx new file mode 100644 index 000000000..1d3b4f18d --- /dev/null +++ b/spa/src/pages/Connections/Modals/DisconnectGHEServerModa.test.tsx @@ -0,0 +1,86 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; +import userEvent from "@testing-library/user-event"; +import { DisconnectGHEServerModal } from "./DisconnectGHEServerModal"; +import SubscriptionManager from "../../../services/subscription-manager"; + +jest.mock("../../../services/subscription-manager"); + +const sampleGHEServer = { + id: 12344, + uuid: "1hsjhg-2hjgej2-ushjsjs-97b2n", + appId: 1, + gitHubBaseUrl: "string", + gitHubClientId: "string", + gitHubAppName: "string", + installationId: 2, + createdAt: "string", + updatedAt: "string", + successfulConnections: [], + failedConnections: [], + installations: { + fulfilled: [], + rejected: [], + total: 1, + }, +}; +const isModalOpened = jest.fn(); +const setSelectedModal = jest.fn(); +const refetch = jest.fn(); + +test("Clicking cancel in disconnect subscription Modal", async () => { + render( + + + + ); + + expect( + screen.getByText("Are you sure you want to disconnect this server?") + ).toBeInTheDocument(); + const text = screen.getByTestId("disconnect-content"); + expect(text.textContent).toBe( + "To reconnect this server, you'll need to create new GitHub apps and import data about its organizations and repositories again." + ); + + await userEvent.click(screen.getByText("Cancel")); + expect(isModalOpened).toBeCalled(); + expect(refetch).not.toBeCalled(); +}); + +test("Clicking Disconnect in disconnect subscription Modal", async () => { + jest.mocked(SubscriptionManager).deleteSubscription = jest + .fn() + .mockReturnValue(Promise.resolve(true)); + + render( + + + + ); + + expect( + screen.getByText("Are you sure you want to disconnect this server?") + ).toBeInTheDocument(); + const text = screen.getByTestId("disconnect-content"); + expect(text.textContent).toBe( + "To reconnect this server, you'll need to create new GitHub apps and import data about its organizations and repositories again." + ); + + await userEvent.click(screen.getByText("Disconnect")); + /** + * Called twice, once when the loading is set to true, + * and later after getting the response from the API request + */ + expect(isModalOpened).toBeCalledTimes(1); + expect(refetch).toBeCalled(); +}); diff --git a/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx b/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx index 9be9dace1..0a15a08a2 100644 --- a/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx +++ b/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx @@ -7,7 +7,10 @@ import Modal, { ModalTitle, } from "@atlaskit/modal-dialog"; import Button, { LoadingButton } from "@atlaskit/button"; -import { BackfillPageModalTypes, GitHubEnterpriseApplication } from "../../../../../src/rest-interfaces"; +import { + BackfillPageModalTypes, + GitHubEnterpriseApplication, +} from "../../../../../src/rest-interfaces"; import SubscriptionManager from "../../../services/subscription-manager"; /** @@ -17,11 +20,11 @@ import SubscriptionManager from "../../../services/subscription-manager"; const DisconnectGHEServerModal = ({ gheServer, setIsModalOpened, - setSelectedModal, + setSelectedModal, }: { gheServer: GitHubEnterpriseApplication; setIsModalOpened: (x: boolean) => void; - setSelectedModal: (selectedModal:BackfillPageModalTypes) => void; + setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; }) => { const [isLoading, setIsLoading] = useState(false); @@ -81,18 +84,27 @@ const DeleteAppsInGitHubModal = ({ setIsModalOpened: (x: boolean) => void; refetch: () => void; }) => { - const { gitHubAppName, gitHubBaseUrl } = gheServer; + const { gitHubAppName, gitHubBaseUrl } = gheServer; return ( setIsModalOpened(false)}> - Server disconnected + + Server disconnected +

You can now delete these unused apps from your GitHub server. Select the app, then in GitHub select Delete GitHub app.

- +
- - -
- + setIsModalOpened(false)}> + + Backfill your data + + +

+ Backfilling data can take a long time, so we’ll only backfill your + data from the last 6 months. If you want to backfill more data, choose + a date below. Branches will be backfilled regardless of their age. +

+ + + setRestartFromDateCheck(!restartFromDateCheck)} + label={`Restart the backfill from today to this date`} + name="restart-from-selected-date" + /> +
+ + + + +
); }; -export default RestartBackfillModal; +export default RestartBackfillModal; From 124b23649687fe8d2230c61bfa31b48630d708a3 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Fri, 22 Dec 2023 17:27:25 +1100 Subject: [PATCH 08/14] chore: add test cases --- .../enterprise/delete-ghe-server.test.ts | 120 ++++++++++++++++++ .../routes/enterprise/delete-ghe-server.ts | 5 +- src/rest/routes/subscriptions/sync.test.ts | 2 +- 3 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 src/rest/routes/enterprise/delete-ghe-server.test.ts diff --git a/src/rest/routes/enterprise/delete-ghe-server.test.ts b/src/rest/routes/enterprise/delete-ghe-server.test.ts new file mode 100644 index 000000000..3c5df7eeb --- /dev/null +++ b/src/rest/routes/enterprise/delete-ghe-server.test.ts @@ -0,0 +1,120 @@ +import { getFrontendApp } from "~/src/app"; +import { Installation } from "models/installation"; +import { Subscription } from "models/subscription"; +import express, { Express } from "express"; +import { RootRouter } from "routes/router"; +import supertest from "supertest"; +import { encodeSymmetric } from "atlassian-jwt"; +import { GitHubServerApp } from "models/github-server-app"; +import { v4 as newUUID } from "uuid"; + +jest.mock("~/src/sqs/queues"); +jest.mock("config/feature-flags"); + +describe("Checking the sync request parsing route", () => { + let app: Express; + let installation: Installation; + // let installation1: Installation; + const installationIdForCloud = 1; + const installationIdForServer = 2; + const uuid = newUUID(); + let gitHubServerApp: GitHubServerApp; + // let gitHubServerApp1: GitHubServerApp; + const testSharedSecret = "test-secret"; + const clientKey = "jira-client-key"; + const getToken = ({ + secret = testSharedSecret, + iss = clientKey, + exp = Date.now() / 1000 + 10000, + qsh = "context-qsh", + sub = "myAccount" + } = {}): string => { + return encodeSymmetric( + { + qsh, + iss, + exp, + sub + }, + secret + ); + }; + beforeEach(async () => { + app = getFrontendApp(); + installation = await Installation.install({ + host: jiraHost, + sharedSecret: testSharedSecret, + clientKey: clientKey + }); + // installation1 = await Installation.install({ + // host: jiraHost, + // sharedSecret: testSharedSecret+"1", + // clientKey: clientKey+"1" + // }); + await Subscription.install({ + installationId: installationIdForCloud, + host: jiraHost, + hashedClientKey: installation.clientKey, + gitHubAppId: undefined + }); + gitHubServerApp = await GitHubServerApp.install( + { + uuid: uuid, + appId: 123, + gitHubAppName: "My GitHub Server App", + gitHubBaseUrl: gheUrl, + gitHubClientId: "lvl.1234", + gitHubClientSecret: "myghsecret", + webhookSecret: "mywebhooksecret", + privateKey: "myprivatekey", + installationId: installation.id + }, + jiraHost + ); + // gitHubServerApp1 = await GitHubServerApp.install( + // { + // uuid: uuid, + // appId: 123, + // gitHubAppName: "My GitHub Server App", + // gitHubBaseUrl: gheUrl, + // gitHubClientId: "lvl.1234", + // gitHubClientSecret: "myghsecret", + // webhookSecret: "mywebhooksecret", + // privateKey: "myprivatekey", + // installationId: installation1.id + // }, + // jiraHost + // ); + await Subscription.install({ + installationId: installationIdForServer, + host: jiraHost, + hashedClientKey: installation.clientKey, + gitHubAppId: gitHubServerApp.id + }); + app = express(); + app.use(RootRouter); + }); + + describe("cloud", () => { + it("should throw 401 error when no github token is passed", async () => { + const resp = await supertest(app).delete( + `/rest/app/${gitHubServerApp.uuid}` + ); + expect(resp.status).toEqual(401); + }); + + it("should return 400 on no uuid", async () => { + const resp = await supertest(app) + .delete(`/rest/app/cloud`) + .set("authorization", `${getToken()}`); + expect(resp.status).toEqual(400); + }); + + it("should return 200 on correct uuid", async () => { + const resp = await supertest(app) + .delete(`/rest/app/${gitHubServerApp.uuid}`) + .set("authorization", `${getToken()}`); + expect(resp.status).toEqual(200); + }); + }); +}); diff --git a/src/rest/routes/enterprise/delete-ghe-server.ts b/src/rest/routes/enterprise/delete-ghe-server.ts index efe0950d4..da0cec463 100644 --- a/src/rest/routes/enterprise/delete-ghe-server.ts +++ b/src/rest/routes/enterprise/delete-ghe-server.ts @@ -14,13 +14,14 @@ const deleteEnterpriseServer = async ( const { installation } = res.locals; const cloudOrUUID = req.params.cloudOrUUID; - if (!cloudOrUUID) { + const gheUUID = cloudOrUUID === "cloud" ? undefined : cloudOrUUID; //TODO: validate the uuid regex + + if (!gheUUID) { throw new InvalidArgumentError( "Invalid route, couldn't determine UUID of enterprise server!" ); } - // TODO: Check and add test cases for GHE later const gitHubApp = await GitHubServerApp.getForUuidAndInstallationId( cloudOrUUID, installation.id diff --git a/src/rest/routes/subscriptions/sync.test.ts b/src/rest/routes/subscriptions/sync.test.ts index 1aa22b812..28f745101 100644 --- a/src/rest/routes/subscriptions/sync.test.ts +++ b/src/rest/routes/subscriptions/sync.test.ts @@ -13,7 +13,7 @@ import { DatabaseStateCreator } from "~/test/utils/database-state-creator"; jest.mock("~/src/sqs/queues"); jest.mock("config/feature-flags"); -describe("Checking the deferred request parsing route", () => { +describe("Checking the sync request parsing route", () => { let app: Express; let installation: Installation; const installationIdForCloud = 1; From c01a83f5190eef77257d75fb703cbe9f2ca783bb Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Thu, 28 Dec 2023 23:14:30 +1100 Subject: [PATCH 09/14] chore: add config option for GHR server app --- spa/src/api/subscriptions/index.ts | 2 + .../GHEnterpriseApplication.tsx | 78 +++++++++++---- .../GHEnterpriseConnections/index.tsx | 1 - .../Modals/DisconnectGHEServerModal.test.tsx | 61 +++++++++++- .../Modals/DisconnectGHEServerModal.tsx | 61 +++++++++++- spa/src/pages/Connections/index.tsx | 16 ++- .../services/subscription-manager/index.ts | 19 +++- src/rest/rest-router.ts | 5 +- .../routes/enterprise/delete-ghe-app.test.ts | 99 +++++++++++++++++++ src/rest/routes/enterprise/delete-ghe-app.ts | 38 +++++++ .../enterprise/delete-ghe-server.test.ts | 21 ---- src/rest/routes/enterprise/index.ts | 3 +- test/snapshots/app.test.ts.snap | 2 + 13 files changed, 353 insertions(+), 53 deletions(-) create mode 100644 src/rest/routes/enterprise/delete-ghe-app.test.ts create mode 100644 src/rest/routes/enterprise/delete-ghe-app.ts diff --git a/spa/src/api/subscriptions/index.ts b/spa/src/api/subscriptions/index.ts index b11c076b3..1e6f23652 100644 --- a/spa/src/api/subscriptions/index.ts +++ b/spa/src/api/subscriptions/index.ts @@ -5,6 +5,8 @@ export default { getSubscriptions: () => axiosRest.get("/rest/subscriptions"), deleteGHEServer: (uuid: string) => axiosRest.delete(`/rest/app/${uuid}`), + deleteGHEApp: (uuid: string) => + axiosRest.delete(`/rest/app/${uuid}/ghe-app`), deleteSubscription: (subscriptionId: number) => axiosRest.delete(`/rest/app/cloud/subscriptions/${subscriptionId}`), syncSubscriptions: (subscriptionId: number, reqBody: RestSyncReqBody) => diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx index e3edb9db5..c6e6cf78d 100644 --- a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx +++ b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx @@ -1,6 +1,13 @@ /** @jsxImportSource @emotion/react */ import { DynamicTableStateless } from "@atlaskit/dynamic-table"; +import DropdownMenu, { + DropdownItem, + DropdownItemGroup, +} from "@atlaskit/dropdown-menu"; import { useState } from "react"; +import Button from "@atlaskit/button"; +import { Flex, xcss } from "@atlaskit/primitives"; +import MoreIcon from "@atlaskit/icon/glyph/more"; import Heading from "@atlaskit/heading"; import ChevronRightIcon from "@atlaskit/icon/glyph/chevron-right"; import ChevronDownIcon from "@atlaskit/icon/glyph/chevron-down"; @@ -43,18 +50,21 @@ const applicationHeaderStyle = css` align-items: center; justify-content: flex-start; width: 100%; - margin-bottom: 20px; `; const applicationContentStyle = css` width: 100%; `; +const appHeaderContainerStyle = xcss({ + width: "100%", + justifyContent: "space-between", + marginBottom: "20px", +}); + type GitHubEnterpriseApplicationProps = { application: GitHubEnterpriseApplication; - setDataForModal: ( - dataForModal: SuccessfulConnection | undefined - ) => void; + setDataForModal: (dataForModal: SuccessfulConnection | GitHubEnterpriseApplication) => void; setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; setIsModalOpened: (isModalOpen: boolean) => void; }; @@ -76,6 +86,7 @@ const GitHubEnterpriseApp = ({ setDataForModal, setSelectedModal, }: GitHubEnterpriseApplicationProps) => { + const [showAppContent, setShowAppContent] = useState(true); const onConnectNewApp = () => { return AP.context.getToken((token: string) => { @@ -88,22 +99,55 @@ const GitHubEnterpriseApp = ({ } }); }; + const onEditGitHubApp = () =>{ + const uuid = application.uuid; + AP.navigator.go( + "addonmodule", + { + moduleKey: "github-edit-app-page", + customData: { uuid } + } + ); + }; return (
-
{ - setShowAppContent((prevState) => !prevState); - }} - > - {showAppContent ? ( - - ) : ( - - )} + +
{ + setShowAppContent((prevState) => !prevState); + }} + > + {showAppContent ? ( + + ) : ( + + )} + {application.gitHubAppName} +
+
+ ( +
+
- {application.gitHubAppName} -
{showAppContent && ( <> {application.successfulConnections.length > 0 ? ( diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx index 0a43a8bd8..dcc1a30ca 100644 --- a/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx +++ b/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx @@ -69,7 +69,6 @@ const GitHubEnterpriseConnections = ({ setDataForModal, setSelectedModal, }: GitHubEnterpriseConnectionsProps) => { - console.log(":::::::::",JSON.stringify(ghEnterpriseServers)); return ( <> {ghEnterpriseServers.map((connection) => { diff --git a/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.test.tsx b/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.test.tsx index a0495dc1a..143f3dfc1 100644 --- a/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.test.tsx +++ b/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.test.tsx @@ -4,7 +4,8 @@ import { BrowserRouter } from "react-router-dom"; import userEvent from "@testing-library/user-event"; import { DisconnectGHEServerModal, - DeleteAppsInGitHubModal, + DeleteAppInGitHubModal, + DisconnectGHEServerAppModal } from "./DisconnectGHEServerModal"; import SubscriptionManager from "../../../services/subscription-manager"; @@ -32,6 +33,60 @@ const isModalOpened = jest.fn(); const setSelectedModal = jest.fn(); const refetch = jest.fn(); +test("Clicking cancel in disconnect GHE App Modal", async () => { + render( + + + + ); + + expect( + screen.getByText("Are you sure you want to disconnect this app?") + ).toBeInTheDocument(); + const text = screen.getByTestId("disconnect-content"); + expect(text.textContent).toBe( + "To reconnect this app, you'll need to recreate it and import data about its organizations and repositories again." + ); + + await userEvent.click(screen.getByText("Cancel")); + expect(isModalOpened).toBeCalled(); +}); + +test("Clicking Disconnect in disconnect GHE App Modal", async () => { + jest.mocked(SubscriptionManager).deleteSubscription = jest + .fn() + .mockReturnValue(Promise.resolve(true)); + + render( + + + + ); + + expect( + screen.getByText("Are you sure you want to disconnect this app?") + ).toBeInTheDocument(); + const text = screen.getByTestId("disconnect-content"); + expect(text.textContent).toBe( + "To reconnect this app, you'll need to recreate it and import data about its organizations and repositories again." + ); + + await userEvent.click(screen.getByText("Disconnect")); + /** + * Called twice, once when the loading is set to true, + * and later after getting the response from the API request + */ + expect(setSelectedModal).toBeCalledTimes(1); +}); + test("Clicking cancel in disconnect GHE server Modal", async () => { render( @@ -83,13 +138,13 @@ test("Clicking Disconnect in disconnect GHE server Modal", async () => { * Called twice, once when the loading is set to true, * and later after getting the response from the API request */ - expect(setSelectedModal).toBeCalledTimes(1); + expect(setSelectedModal).toBeCalledTimes(2); }); test("Clicking cancel in disconnect GHE server Modal", async () => { render( - void; + setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; +}) => { + const [isLoading, setIsLoading] = useState(false); + + const disconnect = async () => { + setIsLoading(true); + const response: boolean | AxiosError = + await SubscriptionManager.deleteGHEApp(gheServer.uuid); + if (response instanceof AxiosError) { + // TODO: Handle the error once we have the designs + console.error("Error", response); + } else { + setSelectedModal("DELETE_GHE_APP"); + } + }; + + return ( + setIsModalOpened(false)}> + + + Are you sure you want to disconnect this app? + + + +

+ To reconnect this app, you'll need to recreate it and import data about its organizations and repositories again. +

+
+ + + {isLoading ? ( + + Loading button + + ) : ( + + )} + +
+ ); +}; const DisconnectGHEServerModal = ({ gheServer, setIsModalOpened, @@ -75,7 +132,7 @@ const DisconnectGHEServerModal = ({ ); }; -const DeleteAppsInGitHubModal = ({ +const DeleteAppInGitHubModal = ({ gheServer, setIsModalOpened, refetch, @@ -121,4 +178,4 @@ const DeleteAppsInGitHubModal = ({ ); }; -export { DisconnectGHEServerModal, DeleteAppsInGitHubModal }; +export { DisconnectGHEServerModal, DeleteAppInGitHubModal, DisconnectGHEServerAppModal }; diff --git a/spa/src/pages/Connections/index.tsx b/spa/src/pages/Connections/index.tsx index a42c5d66f..0e6e34728 100644 --- a/spa/src/pages/Connections/index.tsx +++ b/spa/src/pages/Connections/index.tsx @@ -17,7 +17,7 @@ import SkeletonForLoading from "./SkeletonForLoading"; import SubscriptionManager from "../../services/subscription-manager"; import RestartBackfillModal from "./Modals/RestartBackfillModal"; import DisconnectSubscriptionModal from "./Modals/DisconnectSubscriptionModal"; -import { DisconnectGHEServerModal, DeleteAppsInGitHubModal } from "./Modals/DisconnectGHEServerModal"; +import { DisconnectGHEServerModal, DeleteAppInGitHubModal, DisconnectGHEServerAppModal } from "./Modals/DisconnectGHEServerModal"; const hasGHCloudConnections = (subscriptions: GHSubscriptions): boolean => subscriptions?.ghCloudSubscriptions && @@ -26,7 +26,7 @@ const hasGHCloudConnections = (subscriptions: GHSubscriptions): boolean => const Connections = () => { const navigate = useNavigate(); - ////////// + const [isModalOpened, setIsModalOpened] = useState(false); const [selectedModal, setSelectedModal] = useState("BACKFILL"); @@ -51,7 +51,6 @@ const Connections = () => { refetch={fetchGHSubscriptions} /> ); - // TODO: Create modals for GHE later case "DISCONNECT_SERVER": return ( { ); case "DELETE_GHE_APP": return ( - ); case "DISCONNECT_SERVER_APP": + return ( + + ); default: return <>; } }; - ////////// + const [isLoading, setIsLoading] = useState(false); const [subscriptions, setSubscriptions] = useState( null diff --git a/spa/src/services/subscription-manager/index.ts b/spa/src/services/subscription-manager/index.ts index 895fb44ed..d8e181fe1 100644 --- a/spa/src/services/subscription-manager/index.ts +++ b/spa/src/services/subscription-manager/index.ts @@ -73,12 +73,29 @@ async function deleteGHEServer(uuid: string): Promise { return e as AxiosError; } } +async function deleteGHEApp(uuid: string): Promise { + try { + const response= await Api.subscriptions.deleteGHEApp(uuid); + const isSuccessful = response.status === 204; + if(!isSuccessful) { + reportError( + { message: "Response status for deleting GHE server is not 204", status: response.status }, + { path: "deleteGHEApp" } + ); + } + return isSuccessful; + } catch (e: unknown) { + reportError(new Error("Unable to delete GHE server", { cause: e }), { path: "deleteGHEApp" }); + return e as AxiosError; + } +} export default { getSubscriptions, deleteSubscription, deleteGHEServer, - syncSubscription + syncSubscription, + deleteGHEApp, }; diff --git a/src/rest/rest-router.ts b/src/rest/rest-router.ts index fac4c802b..61251d21d 100644 --- a/src/rest/rest-router.ts +++ b/src/rest/rest-router.ts @@ -11,7 +11,7 @@ import { JiraAdminEnforceMiddleware } from "./middleware/jira-admin/jira-admin-c import { AnalyticsProxyHandler } from "./routes/analytics-proxy"; import { SubscriptionsRouter } from "./routes/subscriptions"; import { DeferredRouter } from "./routes/deferred"; -import { deleteEnterpriseServerHandler } from "./routes/enterprise"; +import { deleteEnterpriseServerHandler, deleteEnterpriseAppHandler } from "./routes/enterprise"; export const RestRouter = Router({ mergeParams: true }); @@ -43,7 +43,8 @@ subRouter.use(JwtHandler); subRouter.use(JiraAdminEnforceMiddleware); // This is to delete GHE server with specific UUID subRouter.delete("/", deleteEnterpriseServerHandler); - +// This is to delete GHE app which is associated with specific server having UUID +subRouter.delete("/ghe-app", deleteEnterpriseAppHandler); subRouter.post("/analytics-proxy", AnalyticsProxyHandler); diff --git a/src/rest/routes/enterprise/delete-ghe-app.test.ts b/src/rest/routes/enterprise/delete-ghe-app.test.ts new file mode 100644 index 000000000..9c02b3279 --- /dev/null +++ b/src/rest/routes/enterprise/delete-ghe-app.test.ts @@ -0,0 +1,99 @@ +import { getFrontendApp } from "~/src/app"; +import { Installation } from "models/installation"; +import { Subscription } from "models/subscription"; +import express, { Express } from "express"; +import { RootRouter } from "routes/router"; +import supertest from "supertest"; +import { encodeSymmetric } from "atlassian-jwt"; +import { GitHubServerApp } from "models/github-server-app"; +import { v4 as newUUID } from "uuid"; + +jest.mock("~/src/sqs/queues"); +jest.mock("config/feature-flags"); + +describe("Checking the sync request parsing route", () => { + let app: Express; + let installation: Installation; + const installationIdForCloud = 1; + const installationIdForServer = 2; + const uuid = newUUID(); + let gitHubServerApp: GitHubServerApp; + const testSharedSecret = "test-secret"; + const clientKey = "jira-client-key"; + const getToken = ({ + secret = testSharedSecret, + iss = clientKey, + exp = Date.now() / 1000 + 10000, + qsh = "context-qsh", + sub = "myAccount" + } = {}): string => { + return encodeSymmetric( + { + qsh, + iss, + exp, + sub + }, + secret + ); + }; + beforeEach(async () => { + app = getFrontendApp(); + installation = await Installation.install({ + host: jiraHost, + sharedSecret: testSharedSecret, + clientKey: clientKey + }); + await Subscription.install({ + installationId: installationIdForCloud, + host: jiraHost, + hashedClientKey: installation.clientKey, + gitHubAppId: undefined + }); + gitHubServerApp = await GitHubServerApp.install( + { + uuid: uuid, + appId: 123, + gitHubAppName: "My GitHub Server App", + gitHubBaseUrl: gheUrl, + gitHubClientId: "lvl.1234", + gitHubClientSecret: "myghsecret", + webhookSecret: "mywebhooksecret", + privateKey: "myprivatekey", + installationId: installation.id + }, + jiraHost + ); + await Subscription.install({ + installationId: installationIdForServer, + host: jiraHost, + hashedClientKey: installation.clientKey, + gitHubAppId: gitHubServerApp.id + }); + app = express(); + app.use(RootRouter); + }); + + describe("cloud", () => { + it("should throw 401 error when no github token is passed", async () => { + const resp = await supertest(app).delete( + `/rest/app/${gitHubServerApp.uuid}/ghe-app` + ); + expect(resp.status).toEqual(401); + }); + + it("should return 400 on no uuid", async () => { + const resp = await supertest(app) + .delete(`/rest/app/cloud/ghe-app`) + .set("authorization", `${getToken()}`); + expect(resp.status).toEqual(400); + }); + + it("should return 200 on correct uuid", async () => { + const resp = await supertest(app) + .delete(`/rest/app/${gitHubServerApp.uuid}/ghe-app`) + .set("authorization", `${getToken()}`); + expect(resp.status).toEqual(200); + }); + }); +}); diff --git a/src/rest/routes/enterprise/delete-ghe-app.ts b/src/rest/routes/enterprise/delete-ghe-app.ts new file mode 100644 index 000000000..950d0c1c0 --- /dev/null +++ b/src/rest/routes/enterprise/delete-ghe-app.ts @@ -0,0 +1,38 @@ +import { Request, Response } from "express"; +import { ParamsDictionary } from "express-serve-static-core"; +import { errorWrapper } from "../../helper"; +import { BaseLocals } from ".."; +import { GitHubServerApp } from "~/src/models/github-server-app"; +import { isConnected } from "~/src/util/is-connected"; +import { saveConfiguredAppProperties } from "~/src/util/app-properties-utils"; +import { InvalidArgumentError } from "~/src/config/errors"; + +const deleteEnterpriseApp = async ( + req: Request, + res: Response +): Promise => { + req.log.debug("Received Jira Connect Enterprise App DELETE request"); + const { installation } = res.locals; + + const cloudOrUUID = req.params.cloudOrUUID; + const gheUUID = cloudOrUUID === "cloud" ? undefined : cloudOrUUID; //TODO: validate the uuid regex + + if (!gheUUID) { + throw new InvalidArgumentError( + "Invalid route, couldn't determine UUID of enterprise server!" + ); + } + + await GitHubServerApp.uninstallApp(gheUUID); + + if (!(await isConnected(installation.jiraHost))) { + await saveConfiguredAppProperties(installation.jiraHost, req.log, false); + } + res.status(200).json("Success"); + req.log.debug("Jira Connect Enterprise App deleted successfully."); +}; + +export const deleteEnterpriseAppHandler = errorWrapper( + "deleteEnterpriseAppHandler", + deleteEnterpriseApp +); diff --git a/src/rest/routes/enterprise/delete-ghe-server.test.ts b/src/rest/routes/enterprise/delete-ghe-server.test.ts index 3c5df7eeb..ea02e8834 100644 --- a/src/rest/routes/enterprise/delete-ghe-server.test.ts +++ b/src/rest/routes/enterprise/delete-ghe-server.test.ts @@ -14,12 +14,10 @@ jest.mock("config/feature-flags"); describe("Checking the sync request parsing route", () => { let app: Express; let installation: Installation; - // let installation1: Installation; const installationIdForCloud = 1; const installationIdForServer = 2; const uuid = newUUID(); let gitHubServerApp: GitHubServerApp; - // let gitHubServerApp1: GitHubServerApp; const testSharedSecret = "test-secret"; const clientKey = "jira-client-key"; const getToken = ({ @@ -46,11 +44,6 @@ describe("Checking the sync request parsing route", () => { sharedSecret: testSharedSecret, clientKey: clientKey }); - // installation1 = await Installation.install({ - // host: jiraHost, - // sharedSecret: testSharedSecret+"1", - // clientKey: clientKey+"1" - // }); await Subscription.install({ installationId: installationIdForCloud, host: jiraHost, @@ -71,20 +64,6 @@ describe("Checking the sync request parsing route", () => { }, jiraHost ); - // gitHubServerApp1 = await GitHubServerApp.install( - // { - // uuid: uuid, - // appId: 123, - // gitHubAppName: "My GitHub Server App", - // gitHubBaseUrl: gheUrl, - // gitHubClientId: "lvl.1234", - // gitHubClientSecret: "myghsecret", - // webhookSecret: "mywebhooksecret", - // privateKey: "myprivatekey", - // installationId: installation1.id - // }, - // jiraHost - // ); await Subscription.install({ installationId: installationIdForServer, host: jiraHost, diff --git a/src/rest/routes/enterprise/index.ts b/src/rest/routes/enterprise/index.ts index 37a10c97b..59c3db300 100644 --- a/src/rest/routes/enterprise/index.ts +++ b/src/rest/routes/enterprise/index.ts @@ -1 +1,2 @@ -export { deleteEnterpriseServerHandler } from "./delete-ghe-server"; \ No newline at end of file +export { deleteEnterpriseServerHandler } from "./delete-ghe-server"; +export { deleteEnterpriseAppHandler } from "./delete-ghe-app"; \ No newline at end of file diff --git a/test/snapshots/app.test.ts.snap b/test/snapshots/app.test.ts.snap index 170605bcd..c4f0285ae 100644 --- a/test/snapshots/app.test.ts.snap +++ b/test/snapshots/app.test.ts.snap @@ -39,6 +39,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,GitHubTokenHandler,ParseRequestId :DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,deleteEnterpriseServerHandler +:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/ghe-app/?$ + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,deleteEnterpriseAppHandler :POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/analytics-proxy/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,AnalyticsProxyHandler :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/installation/?(?=/|$)^/new/?$ From 9591f9ed236143b23cd9fd4481b03bfa7322fbe3 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Fri, 29 Dec 2023 11:27:04 +1100 Subject: [PATCH 10/14] chore: refactor code --- .../GHEnterpriseAppHeader.tsx | 102 ++++++++++++++++++ .../GHEnterpriseApplication.tsx | 89 +++------------ 2 files changed, 118 insertions(+), 73 deletions(-) create mode 100644 spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseAppHeader.tsx diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseAppHeader.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseAppHeader.tsx new file mode 100644 index 000000000..919fbaa6f --- /dev/null +++ b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseAppHeader.tsx @@ -0,0 +1,102 @@ +/** @jsxImportSource @emotion/react */ +import DropdownMenu, { + DropdownItem, + DropdownItemGroup, +} from "@atlaskit/dropdown-menu"; +import Button from "@atlaskit/button"; +import { Flex, xcss } from "@atlaskit/primitives"; +import MoreIcon from "@atlaskit/icon/glyph/more"; +import Heading from "@atlaskit/heading"; +import ChevronRightIcon from "@atlaskit/icon/glyph/chevron-right"; +import ChevronDownIcon from "@atlaskit/icon/glyph/chevron-down"; +import { + BackfillPageModalTypes, + GitHubEnterpriseApplication, + SuccessfulConnection, +} from "../../../rest-interfaces"; +import { css } from "@emotion/react"; + +const applicationHeaderStyle = css` + cursor: pointer; + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; +`; + +const appHeaderContainerStyle = xcss({ + width: "100%", + justifyContent: "space-between", + marginBottom: "20px", +}); + +type GitHubEnterpriseApplicationProps = { + application: GitHubEnterpriseApplication; + setDataForModal: (dataForModal: SuccessfulConnection | GitHubEnterpriseApplication) => void; + setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; + setIsModalOpened: (isModalOpen: boolean) => void; + showAppContent: boolean; + toggleShowAppContent: () => void; +}; + +const GitHubEnterpriseApp = ({ + application, + setIsModalOpened, + setDataForModal, + setSelectedModal, + showAppContent, + toggleShowAppContent +}: GitHubEnterpriseApplicationProps) => { + + + const onEditGitHubApp = () =>{ + const uuid = application.uuid; + AP.navigator.go( + "addonmodule", + { + moduleKey: "github-edit-app-page", + customData: { uuid } + } + ); + }; + return ( + +
{ + toggleShowAppContent(); + }} + > + {showAppContent ? ( + + ) : ( + + )} + {application.gitHubAppName} +
+
+ ( +
+
+ ); +}; + +export default GitHubEnterpriseApp; diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx index c6e6cf78d..9c4250296 100644 --- a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx +++ b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx @@ -1,16 +1,8 @@ /** @jsxImportSource @emotion/react */ import { DynamicTableStateless } from "@atlaskit/dynamic-table"; -import DropdownMenu, { - DropdownItem, - DropdownItemGroup, -} from "@atlaskit/dropdown-menu"; import { useState } from "react"; -import Button from "@atlaskit/button"; -import { Flex, xcss } from "@atlaskit/primitives"; -import MoreIcon from "@atlaskit/icon/glyph/more"; import Heading from "@atlaskit/heading"; -import ChevronRightIcon from "@atlaskit/icon/glyph/chevron-right"; -import ChevronDownIcon from "@atlaskit/icon/glyph/chevron-down"; +import { css } from "@emotion/react"; import { head, getGHSubscriptionsRows, @@ -20,7 +12,7 @@ import { GitHubEnterpriseApplication, SuccessfulConnection, } from "../../../rest-interfaces"; -import { css } from "@emotion/react"; +import GHEnterpriseAppHeader from "./GHEnterpriseAppHeader"; const connectNewAppLinkStyle = css` text-decoration: none; @@ -44,27 +36,15 @@ const noConnectionsBodyStyle = css` padding-top: 10px; `; -const applicationHeaderStyle = css` - cursor: pointer; - display: flex; - align-items: center; - justify-content: flex-start; - width: 100%; -`; - const applicationContentStyle = css` width: 100%; `; -const appHeaderContainerStyle = xcss({ - width: "100%", - justifyContent: "space-between", - marginBottom: "20px", -}); - type GitHubEnterpriseApplicationProps = { application: GitHubEnterpriseApplication; - setDataForModal: (dataForModal: SuccessfulConnection | GitHubEnterpriseApplication) => void; + setDataForModal: ( + dataForModal: SuccessfulConnection | GitHubEnterpriseApplication + ) => void; setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; setIsModalOpened: (isModalOpen: boolean) => void; }; @@ -86,8 +66,9 @@ const GitHubEnterpriseApp = ({ setDataForModal, setSelectedModal, }: GitHubEnterpriseApplicationProps) => { - const [showAppContent, setShowAppContent] = useState(true); + const toggleShowAppContent = () => + setShowAppContent((prevState) => !prevState); const onConnectNewApp = () => { return AP.context.getToken((token: string) => { const child: Window | null = openChildWindow( @@ -99,55 +80,17 @@ const GitHubEnterpriseApp = ({ } }); }; - const onEditGitHubApp = () =>{ - const uuid = application.uuid; - AP.navigator.go( - "addonmodule", - { - moduleKey: "github-edit-app-page", - customData: { uuid } - } - ); - }; + return (
- -
{ - setShowAppContent((prevState) => !prevState); - }} - > - {showAppContent ? ( - - ) : ( - - )} - {application.gitHubAppName} -
-
- ( -
-
- + {showAppContent && ( <> {application.successfulConnections.length > 0 ? ( From c82feeddcaa822b31d611337c1666898c6120659 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Tue, 2 Jan 2024 16:31:19 +1100 Subject: [PATCH 11/14] chore: pr review comments --- spa/src/api/subscriptions/index.ts | 6 +-- .../GHEnterpriseAppHeader.tsx | 5 +- .../GHEnterpriseApplication.tsx | 42 +++++++-------- .../GHEnterpriseConnections/index.tsx | 11 ++-- .../Modals/DisconnectGHEServerModal.tsx | 51 +++++++++++++------ .../Modals/DisconnectSubscriptionModal.tsx | 27 ++++++---- spa/src/pages/Connections/index.tsx | 23 ++++++--- .../services/subscription-manager/index.ts | 8 +-- spa/src/utils/index.ts | 11 ++++ src/rest/rest-router.ts | 10 ++-- .../routes/enterprise/delete-ghe-app.test.ts | 6 +-- .../enterprise/delete-ghe-server.test.ts | 8 +-- .../routes/enterprise/delete-ghe-server.ts | 16 +----- 13 files changed, 135 insertions(+), 89 deletions(-) diff --git a/spa/src/api/subscriptions/index.ts b/spa/src/api/subscriptions/index.ts index 1e6f23652..50e615d51 100644 --- a/spa/src/api/subscriptions/index.ts +++ b/spa/src/api/subscriptions/index.ts @@ -3,10 +3,10 @@ import { RestSyncReqBody } from "~/src/rest-interfaces"; export default { getSubscriptions: () => axiosRest.get("/rest/subscriptions"), - deleteGHEServer: (uuid: string) => - axiosRest.delete(`/rest/app/${uuid}`), + deleteGHEServer: (serverUrl: string) => + axiosRest.delete(`/rest/ghes-servers/${serverUrl}`), deleteGHEApp: (uuid: string) => - axiosRest.delete(`/rest/app/${uuid}/ghe-app`), + axiosRest.delete(`/rest/app/${uuid}`), deleteSubscription: (subscriptionId: number) => axiosRest.delete(`/rest/app/cloud/subscriptions/${subscriptionId}`), syncSubscriptions: (subscriptionId: number, reqBody: RestSyncReqBody) => diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseAppHeader.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseAppHeader.tsx index 919fbaa6f..cdc8ee8a5 100644 --- a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseAppHeader.tsx +++ b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseAppHeader.tsx @@ -39,7 +39,7 @@ type GitHubEnterpriseApplicationProps = { toggleShowAppContent: () => void; }; -const GitHubEnterpriseApp = ({ +const GHEnterpriseAppHeader = ({ application, setIsModalOpened, setDataForModal, @@ -48,7 +48,6 @@ const GitHubEnterpriseApp = ({ toggleShowAppContent }: GitHubEnterpriseApplicationProps) => { - const onEditGitHubApp = () =>{ const uuid = application.uuid; AP.navigator.go( @@ -99,4 +98,4 @@ const GitHubEnterpriseApp = ({ ); }; -export default GitHubEnterpriseApp; +export default GHEnterpriseAppHeader; diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx index 9c4250296..cfd0eb527 100644 --- a/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx +++ b/spa/src/pages/Connections/GHEnterpriseConnections/GHEnterpriseApplication.tsx @@ -13,6 +13,7 @@ import { SuccessfulConnection, } from "../../../rest-interfaces"; import GHEnterpriseAppHeader from "./GHEnterpriseAppHeader"; +import { openChildWindow } from "../../../utils"; const connectNewAppLinkStyle = css` text-decoration: none; @@ -47,38 +48,39 @@ type GitHubEnterpriseApplicationProps = { ) => void; setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; setIsModalOpened: (isModalOpen: boolean) => void; + setIsLoading: (isLoading: boolean) => void; }; -function openChildWindow(url: string) { - const child: Window | null = window.open(url); - const interval = setInterval(function () { - if (child?.closed) { - clearInterval(interval); - AP.navigator.reload(); - } - }, 100); - return child; -} - const GitHubEnterpriseApp = ({ application, setIsModalOpened, setDataForModal, setSelectedModal, + setIsLoading, }: GitHubEnterpriseApplicationProps) => { const [showAppContent, setShowAppContent] = useState(true); const toggleShowAppContent = () => setShowAppContent((prevState) => !prevState); const onConnectNewApp = () => { - return AP.context.getToken((token: string) => { - const child: Window | null = openChildWindow( - `/session/github/${application.uuid}/configuration?ghRedirect=to` - ); - if (child) { - /* eslint-disable @typescript-eslint/no-explicit-any*/ - (child as any).window.jwt = token; - } - }); + try{ + setIsLoading(true); + return AP.context.getToken((token: string) => { + const child: Window | null = openChildWindow( + `/session/github/${application.uuid}/configuration?ghRedirect=to` + ); + if (child) { + /* eslint-disable @typescript-eslint/no-explicit-any*/ + (child as any).window.jwt = token; + } + }); + } + catch(e){ + // TODO: handle this error in UI/Modal ? + console.error("Could not get the token for GHE flow of connecting new App : ", e); + } + finally{ + setIsLoading(false); + } }; return ( diff --git a/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx b/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx index dcc1a30ca..58055ff16 100644 --- a/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx +++ b/spa/src/pages/Connections/GHEnterpriseConnections/index.tsx @@ -11,7 +11,7 @@ import { SuccessfulConnection, GitHubEnterpriseApplication } from "../../../rest-interfaces"; -import GHEApplication from "./GHEnterpriseApplication"; +import GitHubEnterpriseApp from "./GHEnterpriseApplication"; const enterpriserServerHeaderStyle = css` display: flex; @@ -22,8 +22,8 @@ const enterpriserServerHeaderStyle = css` `; const enterpriserAppsHeaderStyle = css` - padding-left: 25px; - padding-bottom: 30px; + padding-left: ${token("space.300")}; + padding-bottom: ${token("space.300")}; `; const containerStyles = xcss({ @@ -62,12 +62,14 @@ type GitHubEnterpriseConnectionsProps = { ) => void; setSelectedModal: (selectedModal: BackfillPageModalTypes) => void; setIsModalOpened: (isModalOpen: boolean) => void; + setIsLoading: (isLoading: boolean) => void; }; const GitHubEnterpriseConnections = ({ ghEnterpriseServers, setIsModalOpened, setDataForModal, setSelectedModal, + setIsLoading, }: GitHubEnterpriseConnectionsProps) => { return ( <> @@ -95,11 +97,12 @@ const GitHubEnterpriseConnections = ({ APPLICATIONS
{connection.applications.map((application) => ( - ))} diff --git a/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx b/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx index cadcd4436..edda0a703 100644 --- a/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx +++ b/spa/src/pages/Connections/Modals/DisconnectGHEServerModal.tsx @@ -30,14 +30,23 @@ const DisconnectGHEServerAppModal = ({ const [isLoading, setIsLoading] = useState(false); const disconnect = async () => { - setIsLoading(true); - const response: boolean | AxiosError = - await SubscriptionManager.deleteGHEApp(gheServer.uuid); - if (response instanceof AxiosError) { - // TODO: Handle the error once we have the designs - console.error("Error", response); - } else { - setSelectedModal("DELETE_GHE_APP"); + try{ + setIsLoading(true); + const response: boolean | AxiosError = + await SubscriptionManager.deleteGHEApp(gheServer.uuid); + if (response instanceof AxiosError) { + // TODO: Handle the error once we have the designs + console.error("Error", response); + } else { + setSelectedModal("DELETE_GHE_APP"); + } + } + catch(e){ + // TODO: handle this error in UI/Modal ? + console.error("Could not disconnect GHE app : ", e); + } + finally{ + setIsLoading(false); } }; @@ -86,14 +95,24 @@ const DisconnectGHEServerModal = ({ const [isLoading, setIsLoading] = useState(false); const disconnect = async () => { - setIsLoading(true); - const response: boolean | AxiosError = - await SubscriptionManager.deleteGHEServer(gheServer.uuid); - if (response instanceof AxiosError) { - // TODO: Handle the error once we have the designs - console.error("Error", response); - } else { - setSelectedModal("DELETE_GHE_APP"); + try{ + setIsLoading(true); + const encodedGHEBaseUrl = encodeURIComponent(gheServer.gitHubBaseUrl); + const response: boolean | AxiosError = + await SubscriptionManager.deleteGHEServer(encodedGHEBaseUrl); + if (response instanceof AxiosError) { + // TODO: Handle the error once we have the designs + console.error("Error", response); + } else { + setSelectedModal("DELETE_GHE_APP"); + } + } + catch(e){ + // TODO: handle this error in UI/Modal ? + console.error("Could not disconnect GHE server : ", e); + } + finally{ + setIsLoading(false); } }; diff --git a/spa/src/pages/Connections/Modals/DisconnectSubscriptionModal.tsx b/spa/src/pages/Connections/Modals/DisconnectSubscriptionModal.tsx index 78c0143ac..24f5f7175 100644 --- a/spa/src/pages/Connections/Modals/DisconnectSubscriptionModal.tsx +++ b/spa/src/pages/Connections/Modals/DisconnectSubscriptionModal.tsx @@ -26,16 +26,25 @@ const DisconnectSubscriptionModal = ({ const [isLoading, setIsLoading] = useState(false); const disconnect = async () => { - setIsLoading(true); - const response: boolean | AxiosError = - await SubscriptionManager.deleteSubscription(subscription.subscriptionId); - if (response instanceof AxiosError) { - // TODO: Handle the error once we have the designs - console.error("Error", response); - } else { - await refetch(); + try{ + setIsLoading(true); + const response: boolean | AxiosError = + await SubscriptionManager.deleteSubscription(subscription.subscriptionId); + if (response instanceof AxiosError) { + // TODO: Handle the error once we have the designs + console.error("Error", response); + } else { + await refetch(); + } + setIsModalOpened(false); + } + catch(e){ + // TODO: handle this error in UI/Modal ? + console.error("Could not disconnect subscription : ", e); + } + finally{ + setIsLoading(false); } - setIsModalOpened(false); }; return ( diff --git a/spa/src/pages/Connections/index.tsx b/spa/src/pages/Connections/index.tsx index 0e6e34728..694831ff0 100644 --- a/spa/src/pages/Connections/index.tsx +++ b/spa/src/pages/Connections/index.tsx @@ -84,16 +84,24 @@ const Connections = () => { const [subscriptions, setSubscriptions] = useState( null ); + const fetchGHSubscriptions = async () => { - setIsLoading(true); - const response = await SubscriptionManager.getSubscriptions(); - if (response instanceof AxiosError) { - // TODO: Handle the error once we have the designs - console.error("Error", response); + try { + setIsLoading(true); + const response = await SubscriptionManager.getSubscriptions(); + if (response instanceof AxiosError) { + // TODO: Handle the error once we have the designs + console.error("Error", response); + } + setSubscriptions(response as GHSubscriptions); + } catch (e) { + // TODO: handle this error in UI/Modal ? + console.error("Could not fetch ghe subscriptions: ", e); + } finally { + setIsLoading(false); } - setSubscriptions(response as GHSubscriptions); - setIsLoading(false); }; + useEffect(() => { fetchGHSubscriptions(); }, []); @@ -131,6 +139,7 @@ const Connections = () => { subscriptions.ghEnterpriseServers?.length > 0 && ( { +async function deleteGHEServer(serverUrl: string): Promise { try { - const response= await Api.subscriptions.deleteGHEServer(uuid); + const response= await Api.subscriptions.deleteGHEServer(serverUrl); const isSuccessful = response.status === 204; if(!isSuccessful) { reportError( @@ -79,14 +79,14 @@ async function deleteGHEApp(uuid: string): Promise { const isSuccessful = response.status === 204; if(!isSuccessful) { reportError( - { message: "Response status for deleting GHE server is not 204", status: response.status }, + { message: "Response status for deleting GHE app is not 204", status: response.status }, { path: "deleteGHEApp" } ); } return isSuccessful; } catch (e: unknown) { - reportError(new Error("Unable to delete GHE server", { cause: e }), { path: "deleteGHEApp" }); + reportError(new Error("Unable to delete GHE app", { cause: e }), { path: "deleteGHEApp" }); return e as AxiosError; } } diff --git a/spa/src/utils/index.ts b/spa/src/utils/index.ts index 604fe44a7..0d4254022 100644 --- a/spa/src/utils/index.ts +++ b/spa/src/utils/index.ts @@ -47,3 +47,14 @@ function extractKeyErrorInfo(e: AxiosError) { errBody: e.response?.data }; } + +export function openChildWindow(url: string) { + const child: Window | null = window.open(url); + const interval = setInterval(function () { + if (child?.closed) { + clearInterval(interval); + AP.navigator.reload(); + } + }, 100); + return child; +} diff --git a/src/rest/rest-router.ts b/src/rest/rest-router.ts index 61251d21d..ad9b2a148 100644 --- a/src/rest/rest-router.ts +++ b/src/rest/rest-router.ts @@ -11,18 +11,22 @@ import { JiraAdminEnforceMiddleware } from "./middleware/jira-admin/jira-admin-c import { AnalyticsProxyHandler } from "./routes/analytics-proxy"; import { SubscriptionsRouter } from "./routes/subscriptions"; import { DeferredRouter } from "./routes/deferred"; -import { deleteEnterpriseServerHandler, deleteEnterpriseAppHandler } from "./routes/enterprise"; +import { deleteEnterpriseAppHandler, deleteEnterpriseServerHandler } from "./routes/enterprise"; export const RestRouter = Router({ mergeParams: true }); const subRouter = Router({ mergeParams: true }); +const gheServerRouter = Router({ mergeParams: true }); /** * Separate route which returns the list of both cloud and server subscriptions */ RestRouter.use("/subscriptions", JwtHandler, JiraAdminEnforceMiddleware, SubscriptionsRouter); +RestRouter.use("/ghes-servers/:serverUrl",JwtHandler, JiraAdminEnforceMiddleware, gheServerRouter); +gheServerRouter.delete("/", deleteEnterpriseServerHandler); + /** * For cloud flow, the path will be `/rest/app/cloud/XXX`, * For enterprise flow, the path will be `/rest/app/SERVER-UUID/XXX` @@ -42,9 +46,9 @@ subRouter.use("/deferred", DeferredRouter); subRouter.use(JwtHandler); subRouter.use(JiraAdminEnforceMiddleware); // This is to delete GHE server with specific UUID -subRouter.delete("/", deleteEnterpriseServerHandler); +subRouter.delete("/", deleteEnterpriseAppHandler); // This is to delete GHE app which is associated with specific server having UUID -subRouter.delete("/ghe-app", deleteEnterpriseAppHandler); +// subRouter.delete("/ghe-app", deleteEnterpriseAppHandler); subRouter.post("/analytics-proxy", AnalyticsProxyHandler); diff --git a/src/rest/routes/enterprise/delete-ghe-app.test.ts b/src/rest/routes/enterprise/delete-ghe-app.test.ts index 9c02b3279..ea02e8834 100644 --- a/src/rest/routes/enterprise/delete-ghe-app.test.ts +++ b/src/rest/routes/enterprise/delete-ghe-app.test.ts @@ -77,21 +77,21 @@ describe("Checking the sync request parsing route", () => { describe("cloud", () => { it("should throw 401 error when no github token is passed", async () => { const resp = await supertest(app).delete( - `/rest/app/${gitHubServerApp.uuid}/ghe-app` + `/rest/app/${gitHubServerApp.uuid}` ); expect(resp.status).toEqual(401); }); it("should return 400 on no uuid", async () => { const resp = await supertest(app) - .delete(`/rest/app/cloud/ghe-app`) + .delete(`/rest/app/cloud`) .set("authorization", `${getToken()}`); expect(resp.status).toEqual(400); }); it("should return 200 on correct uuid", async () => { const resp = await supertest(app) - .delete(`/rest/app/${gitHubServerApp.uuid}/ghe-app`) + .delete(`/rest/app/${gitHubServerApp.uuid}`) .set("authorization", `${getToken()}`); expect(resp.status).toEqual(200); }); diff --git a/src/rest/routes/enterprise/delete-ghe-server.test.ts b/src/rest/routes/enterprise/delete-ghe-server.test.ts index ea02e8834..4ff9d0a34 100644 --- a/src/rest/routes/enterprise/delete-ghe-server.test.ts +++ b/src/rest/routes/enterprise/delete-ghe-server.test.ts @@ -76,22 +76,24 @@ describe("Checking the sync request parsing route", () => { describe("cloud", () => { it("should throw 401 error when no github token is passed", async () => { + const encodedGHEBaseUrl = encodeURIComponent(gitHubServerApp.gitHubBaseUrl); const resp = await supertest(app).delete( - `/rest/app/${gitHubServerApp.uuid}` + `/rest/ghes-servers/${encodedGHEBaseUrl}` ); expect(resp.status).toEqual(401); }); it("should return 400 on no uuid", async () => { const resp = await supertest(app) - .delete(`/rest/app/cloud`) + .delete(`/rest/ghes-servers/`) .set("authorization", `${getToken()}`); expect(resp.status).toEqual(400); }); it("should return 200 on correct uuid", async () => { + const encodedGHEBaseUrl = encodeURIComponent(gitHubServerApp.gitHubBaseUrl); const resp = await supertest(app) - .delete(`/rest/app/${gitHubServerApp.uuid}`) + .delete(`/rest/app/${encodedGHEBaseUrl}`) .set("authorization", `${getToken()}`); expect(resp.status).toEqual(200); }); diff --git a/src/rest/routes/enterprise/delete-ghe-server.ts b/src/rest/routes/enterprise/delete-ghe-server.ts index da0cec463..82c07470f 100644 --- a/src/rest/routes/enterprise/delete-ghe-server.ts +++ b/src/rest/routes/enterprise/delete-ghe-server.ts @@ -13,20 +13,8 @@ const deleteEnterpriseServer = async ( ): Promise => { const { installation } = res.locals; - const cloudOrUUID = req.params.cloudOrUUID; - const gheUUID = cloudOrUUID === "cloud" ? undefined : cloudOrUUID; //TODO: validate the uuid regex - - if (!gheUUID) { - throw new InvalidArgumentError( - "Invalid route, couldn't determine UUID of enterprise server!" - ); - } - - const gitHubApp = await GitHubServerApp.getForUuidAndInstallationId( - cloudOrUUID, - installation.id - ); - const gitHubBaseUrl = gitHubApp?.gitHubBaseUrl; + const encodedGHEBaseUrl = req.params.serverUrl; + const gitHubBaseUrl = decodeURIComponent(encodedGHEBaseUrl); if (!gitHubBaseUrl) { throw new InvalidArgumentError( "Invalid route, couldn't determine gitHubBaseUrl for enterprise server!" From b18602a9e492c5fac254a48eedec3d12667ca7a3 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Wed, 3 Jan 2024 09:42:48 +1100 Subject: [PATCH 12/14] chore: update code as per PR comments --- src/rest/routes/enterprise/delete-ghe-app.ts | 2 +- .../enterprise/delete-ghe-server.test.ts | 101 ------------------ .../routes/enterprise/delete-ghe-server.ts | 2 +- test/snapshots/app.test.ts.snap | 4 +- 4 files changed, 4 insertions(+), 105 deletions(-) delete mode 100644 src/rest/routes/enterprise/delete-ghe-server.test.ts diff --git a/src/rest/routes/enterprise/delete-ghe-app.ts b/src/rest/routes/enterprise/delete-ghe-app.ts index 950d0c1c0..958eafac4 100644 --- a/src/rest/routes/enterprise/delete-ghe-app.ts +++ b/src/rest/routes/enterprise/delete-ghe-app.ts @@ -24,7 +24,7 @@ const deleteEnterpriseApp = async ( } await GitHubServerApp.uninstallApp(gheUUID); - + // TODO: manually delete subscriptions after GHE app is removed if (!(await isConnected(installation.jiraHost))) { await saveConfiguredAppProperties(installation.jiraHost, req.log, false); } diff --git a/src/rest/routes/enterprise/delete-ghe-server.test.ts b/src/rest/routes/enterprise/delete-ghe-server.test.ts deleted file mode 100644 index 4ff9d0a34..000000000 --- a/src/rest/routes/enterprise/delete-ghe-server.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { getFrontendApp } from "~/src/app"; -import { Installation } from "models/installation"; -import { Subscription } from "models/subscription"; -import express, { Express } from "express"; -import { RootRouter } from "routes/router"; -import supertest from "supertest"; -import { encodeSymmetric } from "atlassian-jwt"; -import { GitHubServerApp } from "models/github-server-app"; -import { v4 as newUUID } from "uuid"; - -jest.mock("~/src/sqs/queues"); -jest.mock("config/feature-flags"); - -describe("Checking the sync request parsing route", () => { - let app: Express; - let installation: Installation; - const installationIdForCloud = 1; - const installationIdForServer = 2; - const uuid = newUUID(); - let gitHubServerApp: GitHubServerApp; - const testSharedSecret = "test-secret"; - const clientKey = "jira-client-key"; - const getToken = ({ - secret = testSharedSecret, - iss = clientKey, - exp = Date.now() / 1000 + 10000, - qsh = "context-qsh", - sub = "myAccount" - } = {}): string => { - return encodeSymmetric( - { - qsh, - iss, - exp, - sub - }, - secret - ); - }; - beforeEach(async () => { - app = getFrontendApp(); - installation = await Installation.install({ - host: jiraHost, - sharedSecret: testSharedSecret, - clientKey: clientKey - }); - await Subscription.install({ - installationId: installationIdForCloud, - host: jiraHost, - hashedClientKey: installation.clientKey, - gitHubAppId: undefined - }); - gitHubServerApp = await GitHubServerApp.install( - { - uuid: uuid, - appId: 123, - gitHubAppName: "My GitHub Server App", - gitHubBaseUrl: gheUrl, - gitHubClientId: "lvl.1234", - gitHubClientSecret: "myghsecret", - webhookSecret: "mywebhooksecret", - privateKey: "myprivatekey", - installationId: installation.id - }, - jiraHost - ); - await Subscription.install({ - installationId: installationIdForServer, - host: jiraHost, - hashedClientKey: installation.clientKey, - gitHubAppId: gitHubServerApp.id - }); - app = express(); - app.use(RootRouter); - }); - - describe("cloud", () => { - it("should throw 401 error when no github token is passed", async () => { - const encodedGHEBaseUrl = encodeURIComponent(gitHubServerApp.gitHubBaseUrl); - const resp = await supertest(app).delete( - `/rest/ghes-servers/${encodedGHEBaseUrl}` - ); - expect(resp.status).toEqual(401); - }); - - it("should return 400 on no uuid", async () => { - const resp = await supertest(app) - .delete(`/rest/ghes-servers/`) - .set("authorization", `${getToken()}`); - expect(resp.status).toEqual(400); - }); - - it("should return 200 on correct uuid", async () => { - const encodedGHEBaseUrl = encodeURIComponent(gitHubServerApp.gitHubBaseUrl); - const resp = await supertest(app) - .delete(`/rest/app/${encodedGHEBaseUrl}`) - .set("authorization", `${getToken()}`); - expect(resp.status).toEqual(200); - }); - }); -}); diff --git a/src/rest/routes/enterprise/delete-ghe-server.ts b/src/rest/routes/enterprise/delete-ghe-server.ts index 82c07470f..6def52177 100644 --- a/src/rest/routes/enterprise/delete-ghe-server.ts +++ b/src/rest/routes/enterprise/delete-ghe-server.ts @@ -25,7 +25,7 @@ const deleteEnterpriseServer = async ( gitHubBaseUrl, installation.id ); - + // TODO: manually delete subscriptions after GHE server is removed if (!(await isConnected(installation.jiraHost))) { await saveConfiguredAppProperties(installation.jiraHost, req.log, false); } diff --git a/test/snapshots/app.test.ts.snap b/test/snapshots/app.test.ts.snap index c4f0285ae..7207a0698 100644 --- a/test/snapshots/app.test.ts.snap +++ b/test/snapshots/app.test.ts.snap @@ -19,6 +19,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionDelete :POST ^/?(?=/|$)^/rest/?(?=/|$)^/subscriptions/?(?=/|$)^/sync/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SyncRouterHandler +:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/ghes-servers/(?:([^/]+?))/?(?=/|$)^/?$ + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,deleteEnterpriseServerHandler :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/github-callback/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,OAuthCallbackHandler :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/github-installed/?$ @@ -38,8 +40,6 @@ exports[`app getFrontendApp please review routes and update snapshot when adding :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/deferred/?(?=/|$)^/parse/(?:([^/]+?))/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,GitHubTokenHandler,ParseRequestId :DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,deleteEnterpriseServerHandler -:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/ghe-app/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,deleteEnterpriseAppHandler :POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/analytics-proxy/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,AnalyticsProxyHandler From 32b233ae68cc97d2a28bfa29b7a1549257413f0f Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Wed, 3 Jan 2024 13:32:26 +1100 Subject: [PATCH 13/14] chore: update code as per PR comments --- src/rest/rest-router.ts | 14 +-- .../routes/enterprise/delete-ghe-app.test.ts | 16 +--- src/rest/routes/enterprise/delete-ghe-app.ts | 2 +- .../enterprise/delete-ghe-server.test.ts | 94 +++++++++++++++++++ .../routes/enterprise/delete-ghe-server.ts | 10 +- src/rest/routes/enterprise/index.ts | 8 +- 6 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 src/rest/routes/enterprise/delete-ghe-server.test.ts diff --git a/src/rest/rest-router.ts b/src/rest/rest-router.ts index ad9b2a148..faafd0510 100644 --- a/src/rest/rest-router.ts +++ b/src/rest/rest-router.ts @@ -10,22 +10,23 @@ import { RestErrorHandler } from "./middleware/error"; import { JiraAdminEnforceMiddleware } from "./middleware/jira-admin/jira-admin-check"; import { AnalyticsProxyHandler } from "./routes/analytics-proxy"; import { SubscriptionsRouter } from "./routes/subscriptions"; +import { gheServerRouter, deleteEnterpriseAppHandler } from "./routes/enterprise"; import { DeferredRouter } from "./routes/deferred"; -import { deleteEnterpriseAppHandler, deleteEnterpriseServerHandler } from "./routes/enterprise"; export const RestRouter = Router({ mergeParams: true }); const subRouter = Router({ mergeParams: true }); -const gheServerRouter = Router({ mergeParams: true }); /** * Separate route which returns the list of both cloud and server subscriptions */ RestRouter.use("/subscriptions", JwtHandler, JiraAdminEnforceMiddleware, SubscriptionsRouter); -RestRouter.use("/ghes-servers/:serverUrl",JwtHandler, JiraAdminEnforceMiddleware, gheServerRouter); -gheServerRouter.delete("/", deleteEnterpriseServerHandler); +/** + * Separate route which deletes the GHE server for given serverUrl + */ +RestRouter.use("/ghes-servers/:serverUrl", JwtHandler, JiraAdminEnforceMiddleware, gheServerRouter); /** * For cloud flow, the path will be `/rest/app/cloud/XXX`, @@ -45,10 +46,9 @@ subRouter.use("/deferred", DeferredRouter); // have done authentication only)? subRouter.use(JwtHandler); subRouter.use(JiraAdminEnforceMiddleware); -// This is to delete GHE server with specific UUID -subRouter.delete("/", deleteEnterpriseAppHandler); + // This is to delete GHE app which is associated with specific server having UUID -// subRouter.delete("/ghe-app", deleteEnterpriseAppHandler); +subRouter.delete("/", deleteEnterpriseAppHandler); subRouter.post("/analytics-proxy", AnalyticsProxyHandler); diff --git a/src/rest/routes/enterprise/delete-ghe-app.test.ts b/src/rest/routes/enterprise/delete-ghe-app.test.ts index ea02e8834..84dfc2e3c 100644 --- a/src/rest/routes/enterprise/delete-ghe-app.test.ts +++ b/src/rest/routes/enterprise/delete-ghe-app.test.ts @@ -8,13 +8,9 @@ import { encodeSymmetric } from "atlassian-jwt"; import { GitHubServerApp } from "models/github-server-app"; import { v4 as newUUID } from "uuid"; -jest.mock("~/src/sqs/queues"); -jest.mock("config/feature-flags"); - describe("Checking the sync request parsing route", () => { let app: Express; let installation: Installation; - const installationIdForCloud = 1; const installationIdForServer = 2; const uuid = newUUID(); let gitHubServerApp: GitHubServerApp; @@ -44,12 +40,6 @@ describe("Checking the sync request parsing route", () => { sharedSecret: testSharedSecret, clientKey: clientKey }); - await Subscription.install({ - installationId: installationIdForCloud, - host: jiraHost, - hashedClientKey: installation.clientKey, - gitHubAppId: undefined - }); gitHubServerApp = await GitHubServerApp.install( { uuid: uuid, @@ -74,7 +64,7 @@ describe("Checking the sync request parsing route", () => { app.use(RootRouter); }); - describe("cloud", () => { + describe("GHE server app delete", () => { it("should throw 401 error when no github token is passed", async () => { const resp = await supertest(app).delete( `/rest/app/${gitHubServerApp.uuid}` @@ -89,11 +79,11 @@ describe("Checking the sync request parsing route", () => { expect(resp.status).toEqual(400); }); - it("should return 200 on correct uuid", async () => { + it("should return 204 on correct uuid", async () => { const resp = await supertest(app) .delete(`/rest/app/${gitHubServerApp.uuid}`) .set("authorization", `${getToken()}`); - expect(resp.status).toEqual(200); + expect(resp.status).toEqual(204); }); }); }); diff --git a/src/rest/routes/enterprise/delete-ghe-app.ts b/src/rest/routes/enterprise/delete-ghe-app.ts index 958eafac4..eeda3fadd 100644 --- a/src/rest/routes/enterprise/delete-ghe-app.ts +++ b/src/rest/routes/enterprise/delete-ghe-app.ts @@ -28,7 +28,7 @@ const deleteEnterpriseApp = async ( if (!(await isConnected(installation.jiraHost))) { await saveConfiguredAppProperties(installation.jiraHost, req.log, false); } - res.status(200).json("Success"); + res.sendStatus(204); req.log.debug("Jira Connect Enterprise App deleted successfully."); }; diff --git a/src/rest/routes/enterprise/delete-ghe-server.test.ts b/src/rest/routes/enterprise/delete-ghe-server.test.ts new file mode 100644 index 000000000..41a8e547a --- /dev/null +++ b/src/rest/routes/enterprise/delete-ghe-server.test.ts @@ -0,0 +1,94 @@ +import { getFrontendApp } from "~/src/app"; +import { Installation } from "models/installation"; +import { Subscription } from "models/subscription"; +import express, { Express } from "express"; +import { RootRouter } from "routes/router"; +import supertest from "supertest"; +import { encodeSymmetric } from "atlassian-jwt"; +import { GitHubServerApp } from "models/github-server-app"; +import { v4 as newUUID } from "uuid"; + +describe("Checking the sync request parsing route", () => { + let app: Express; + let installation: Installation; + // const installationIdForCloud = 1; + const installationIdForServer = 2; + const uuid = newUUID(); + let gitHubServerApp: GitHubServerApp; + const testSharedSecret = "test-secret"; + const clientKey = "jira-client-key"; + const getToken = ({ + secret = testSharedSecret, + iss = clientKey, + exp = Date.now() / 1000 + 10000, + qsh = "context-qsh", + sub = "myAccount" + } = {}): string => { + return encodeSymmetric( + { + qsh, + iss, + exp, + sub + }, + secret + ); + }; + beforeEach(async () => { + app = getFrontendApp(); + installation = await Installation.install({ + host: jiraHost, + sharedSecret: testSharedSecret, + clientKey: clientKey + }); + gitHubServerApp = await GitHubServerApp.install( + { + uuid: uuid, + appId: 123, + gitHubAppName: "My GitHub Server App", + gitHubBaseUrl: gheUrl, + gitHubClientId: "lvl.1234", + gitHubClientSecret: "myghsecret", + webhookSecret: "mywebhooksecret", + privateKey: "myprivatekey", + installationId: installation.id + }, + jiraHost + ); + await Subscription.install({ + installationId: installationIdForServer, + host: jiraHost, + hashedClientKey: installation.clientKey, + gitHubAppId: gitHubServerApp.id + }); + app = express(); + app.use(RootRouter); + }); + + describe("GHE server delete", () => { + it("should throw 404 error when api path is not found", async () => { + const resp = await supertest(app).delete( + `/rest/ghes-serverss/${gitHubServerApp.gitHubBaseUrl}` + ); + expect(resp.status).toEqual(404); + }); + it("should throw 401 error when no github token is passed", async () => { + const encodedGHEBaseUrl = encodeURIComponent( + gitHubServerApp.gitHubBaseUrl + ); + const resp = await supertest(app) + .delete(`/rest/ghes-servers/${encodedGHEBaseUrl}`); + expect(resp.status).toEqual(500); + }); + + it("should return 204 on correct uuid", async () => { + const encodedGHEBaseUrl = encodeURIComponent( + gitHubServerApp.gitHubBaseUrl + ); + const resp = await supertest(app) + .delete(`/rest/ghes-servers/${encodedGHEBaseUrl}`) + .set("authorization", `${getToken()}`); + expect(resp.status).toEqual(204); + }); + }); +}); diff --git a/src/rest/routes/enterprise/delete-ghe-server.ts b/src/rest/routes/enterprise/delete-ghe-server.ts index 6def52177..41a3534dd 100644 --- a/src/rest/routes/enterprise/delete-ghe-server.ts +++ b/src/rest/routes/enterprise/delete-ghe-server.ts @@ -12,14 +12,14 @@ const deleteEnterpriseServer = async ( res: Response ): Promise => { const { installation } = res.locals; - const encodedGHEBaseUrl = req.params.serverUrl; - const gitHubBaseUrl = decodeURIComponent(encodedGHEBaseUrl); - if (!gitHubBaseUrl) { + + if (!encodedGHEBaseUrl){ throw new InvalidArgumentError( - "Invalid route, couldn't determine gitHubBaseUrl for enterprise server!" + "Invalid route, couldn't find encodedGHEBaseUrl in rest api req params!" ); } + const gitHubBaseUrl = decodeURIComponent(encodedGHEBaseUrl); await GitHubServerApp.uninstallServer( gitHubBaseUrl, @@ -29,7 +29,7 @@ const deleteEnterpriseServer = async ( if (!(await isConnected(installation.jiraHost))) { await saveConfiguredAppProperties(installation.jiraHost, req.log, false); } - res.status(200).json("Success"); + res.sendStatus(204); }; export const deleteEnterpriseServerHandler = errorWrapper( diff --git a/src/rest/routes/enterprise/index.ts b/src/rest/routes/enterprise/index.ts index 59c3db300..7bf1d1b3f 100644 --- a/src/rest/routes/enterprise/index.ts +++ b/src/rest/routes/enterprise/index.ts @@ -1,2 +1,6 @@ -export { deleteEnterpriseServerHandler } from "./delete-ghe-server"; -export { deleteEnterpriseAppHandler } from "./delete-ghe-app"; \ No newline at end of file +import { Router } from "express"; +import { deleteEnterpriseServerHandler } from "./delete-ghe-server"; +export { deleteEnterpriseAppHandler } from "./delete-ghe-app"; +export const gheServerRouter = Router({ mergeParams: true }); + +gheServerRouter.delete("/", deleteEnterpriseServerHandler); From e37091fd16879f43b6f1f1b3f3f822778ce6f33a Mon Sep 17 00:00:00 2001 From: Gary Xue <105693507+gxueatlassian@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:55:02 +1100 Subject: [PATCH 14/14] Enforce security check on GHES app (#2624) --- src/rest/rest-router.ts | 19 ++++++++++++++++--- test/snapshots/app.test.ts.snap | 20 +++++++++++--------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/rest/rest-router.ts b/src/rest/rest-router.ts index faafd0510..ce9e7c1ed 100644 --- a/src/rest/rest-router.ts +++ b/src/rest/rest-router.ts @@ -1,5 +1,6 @@ import { Router } from "express"; import { JwtHandler } from "./middleware/jwt/jwt-handler"; +import { errorWrapper } from "./helper"; import { OAuthRouter } from "./routes/oauth"; import { OAuthCallbackHandler, OrgsInstalledHandler, OrgsInstallRequestedHandler } from "./routes/github-callback"; import { GitHubOrgsRouter } from "./routes/github-orgs"; @@ -12,7 +13,7 @@ import { AnalyticsProxyHandler } from "./routes/analytics-proxy"; import { SubscriptionsRouter } from "./routes/subscriptions"; import { gheServerRouter, deleteEnterpriseAppHandler } from "./routes/enterprise"; import { DeferredRouter } from "./routes/deferred"; - +import { GithubServerAppMiddleware } from "middleware/github-server-app-middleware"; export const RestRouter = Router({ mergeParams: true }); @@ -47,11 +48,23 @@ subRouter.use("/deferred", DeferredRouter); subRouter.use(JwtHandler); subRouter.use(JiraAdminEnforceMiddleware); +subRouter.post("/analytics-proxy", AnalyticsProxyHandler); + +subRouter.use(function tempReplaceUUID(req, _, next) { + //This only temporarily add the cloudOrUUID to uuid so that + //we don't have to modify the existing GithubServerAppMiddleware + //Once all migrated, we can remove this. + const cloudOrUUID = req.params.cloudOrUUID; + if (cloudOrUUID !== "cloud") { + req.params.uuid = cloudOrUUID; + } + next(); +}, errorWrapper("GithubServerAppMiddleware", GithubServerAppMiddleware)); +// This is to delete GHE server with specific UUID +subRouter.delete("/", deleteEnterpriseAppHandler); // This is to delete GHE app which is associated with specific server having UUID subRouter.delete("/", deleteEnterpriseAppHandler); -subRouter.post("/analytics-proxy", AnalyticsProxyHandler); - subRouter.use("/installation", GitHubAppsRoute); subRouter.use("/jira/cloudid", JiraCloudIDRouter); diff --git a/test/snapshots/app.test.ts.snap b/test/snapshots/app.test.ts.snap index 7207a0698..c81a6a48a 100644 --- a/test/snapshots/app.test.ts.snap +++ b/test/snapshots/app.test.ts.snap @@ -39,24 +39,26 @@ exports[`app getFrontendApp please review routes and update snapshot when adding query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,GitHubTokenHandler,CheckOwnershipAndConnectRoute :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/deferred/?(?=/|$)^/parse/(?:([^/]+?))/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,GitHubTokenHandler,ParseRequestId -:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,deleteEnterpriseAppHandler :POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/analytics-proxy/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,AnalyticsProxyHandler +:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/?$ + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,deleteEnterpriseAppHandler +:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/?$ + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,deleteEnterpriseAppHandler :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/installation/?(?=/|$)^/new/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,GetGitHubAppsUrl + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,GetGitHubAppsUrl :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/jira/cloudid/?(?=/|$)^/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,JiraCloudIDGet + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,JiraCloudIDGet :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionsGet + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,SubscriptionsGet :DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionDelete + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,SubscriptionDelete :POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/sync/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SyncRouterHandler + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,SyncRouterHandler :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/org/?(?=/|$)^/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,GitHubTokenHandler,GitHubOrgsFetchOrgs + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,GitHubTokenHandler,GitHubOrgsFetchOrgs :POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/org/?(?=/|$)^/?$ - query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,GitHubTokenHandler,GitHubOrgsConnectJira + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,GitHubTokenHandler,GitHubOrgsConnectJira :GET ^/?(?=/|$)^/?(?=/|$)^/deepcheck/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,DeepcheckGet :GET ^/?(?=/|$)^/?(?=/|$)^/healthcheck/?$