import bodyParser from "body-parser" ; import express , { Express , NextFunction , Request , Response } from "express" ; import path from "path" ; import cookieSession from "cookie-session" ; import csrf from "csurf" ; import * as Sentry from "@sentry/node" ; import GithubOAuth from "./github-oauth" ; import getGitHubSetup from "./get-github-setup" ; import postGitHubSetup from "./post-github-setup" ; import getGitHubConfiguration from "./get-github-configuration" ; import postGitHubConfiguration from "./post-github-configuration" ; import getGitHubSubscriptions from "./get-github-subscriptions" ; import deleteGitHubSubscription from "./delete-github-subscription" ; import getJiraConfiguration from "./get-jira-configuration" ; import deleteJiraConfiguration from "./delete-jira-configuration" ; import getGithubClientMiddleware from "./github-client-middleware" ; import getJiraConnect , { postInstallUrl } from "../jira/connect" ; import postJiraInstall from "../jira/install" ; import postJiraUninstall from "../jira/uninstall" ; import extractInstallationFromJiraCallback from "../jira/extract-installation-from-jira-callback" ; import retrySync from "./retry-sync" ; import getMaintenance from "./get-maintenance" ; import api from "../api" ; import healthcheck from "./healthcheck" ; import version from "./version" ; import logMiddleware from "../middleware/frontend-log-middleware" ; import { App } from "@octokit/app" ; import statsd from "../config/statsd" ; import envVars from "../config/env" ; import { metricError } from "../config/metric-names" ; import { authenticateInstallCallback , authenticateUninstallCallback , verifyJiraContextJwtTokenMiddleware , verifyJiraJwtTokenMiddleware } from "./verify-jira-jwt-middleware" ; import { booleanFlag , BooleanFlags } from "../config/feature-flags" ; import { isNodeProd , isNodeTest } from "../util/isNodeEnv" ; import { registerHandlebarsPartials } from "../util/handlebars/partials" ; import { registerHandlebarsHelpers } from "../util/handlebars/helpers" ; import { Errors } from "../config/errors" ; import cookieParser from "cookie-parser" ; declare global { namespace Express { interface Request { body ? : Record < string , any > ; session : { jiraHost ? : string ; githubToken ? : string ; } ; } } } const throwError = ( msg ) => { throw new Error ( msg ) ; } ; const oauth = GithubOAuth ( { githubClient : process . env . GITHUB_CLIENT_ID || throwError ( "Missing GITHUB_CLIENT_ID" ) , githubSecret : process . env . GITHUB_CLIENT_SECRET || throwError ( "Missing GITHUB_CLIENT_SECRET" ) , baseURL : process . env . APP_URL || throwError ( "Missing APP_URL" ) , loginURI : "/github/login" , callbackURI : "/github/callback" } ) ; const csrfProtection = csrf ( isNodeTest ( ) ? { ignoreMethods : [ "GET" , "HEAD" , "OPTIONS" , "POST" , "PUT" ] } : undefined ) ; export default ( octokitApp ) : Express => { const githubClientMiddleware = getGithubClientMiddleware ( octokitApp ) ; const app = express ( ) ; const rootPath = path . resolve ( __dirname , ".." , ".." ) ; app . use ( Sentry . Handlers . requestHandler ( ) ) ; app . use ( bodyParser . urlencoded ( { extended : false } ) ) ; app . use ( bodyParser . json ( ) ) ; app . use ( cookieParser ( ) ) ; app . set ( "trust proxy" , true ) ; app . use ( cookieSession ( { keys : [ process . env . GITHUB_CLIENT_SECRET || throwError ( "Missing GITHUB_CLIENT_SECRET" ) ] , maxAge : 30 * 24 * 60 * 60 * 1000 , signed : true , sameSite : "none" , secure : true , httpOnly : false } ) ) ; app . use ( logMiddleware ) ; app . set ( "view engine" , "hbs" ) ; app . set ( "views" , path . join ( rootPath , "views" ) ) ; registerHandlebarsPartials ( rootPath ) ; registerHandlebarsHelpers ( ) ; app . use ( "/public" , express . static ( path . join ( rootPath , "static" ) ) ) ; app . use ( "/public/css-reset" , express . static ( path . join ( rootPath , "node_modules/@atlaskit/css-reset/dist" ) ) ) ; app . use ( "/public/primer" , express . static ( path . join ( rootPath , "node_modules/primer/build" ) ) ) ; app . use ( "/public/atlassian-ui-kit" , express . static ( path . join ( rootPath , "node_modules/@atlaskit/reduced-ui-pack/dist" ) ) ) ; app . use ( "/public/aui" , express . static ( path . join ( rootPath , "node_modules/@atlassian/aui/dist/aui" ) ) ) ; app . get ( [ "/session" , "/session/*" ] , ( req , res , next ) => { if ( ! req . params [ 0 ] ) { return next ( new Error ( "Missing redirect url for session.  Needs to be in format `/session/:redirectUrl`" ) ) ; } return res . render ( "session.hbs" , { title : "Logging you into GitHub" , APP_URL : process . env . APP_URL , redirectUrl : new URL ( req . params [ 0 ] , process . env . APP_URL ) . href , nonce : res . locals . nonce } ) ; } ) ; app . use ( ( req , res , next ) => { if ( req . cookies . jiraHost ) { req . session . jiraHost = req . cookies . jiraHost ; res . clearCookie ( "jiraHost" ) ; } if ( req . path == postInstallUrl && req . method == "GET" ) { res . locals . jiraHost = req . query . xdm_e as string ; } else if ( ( req . path == postInstallUrl && req . method != "GET" ) || req . path == "/jira/sync" ) { res . locals . jiraHost = req . body ?. jiraHost ; } else { res . locals . jiraHost = req . session . jiraHost ; } next ( ) ; } ) ; app . use ( githubClientMiddleware ) ; app . use ( "/" , healthcheck ) ; app . use ( "/api" , api ) ; app . use ( "/" , oauth . router ) ; app . get ( "/jira/atlassian-connect.json" , getJiraConnect ) ; app . use ( async ( req , res , next ) => { if ( await booleanFlag ( BooleanFlags . MAINTENANCE_MODE , false , res . locals . jiraHost ) ) { return getMaintenance ( req , res ) ; } next ( ) ; } ) ; app . get ( "/version" , version ) ; app . get ( "/maintenance" , csrfProtection , getMaintenance ) ; app . get ( "/github/setup" , csrfProtection , getGitHubSetup ) ; app . post ( "/github/setup" , csrfProtection , postGitHubSetup ) ; app . get ( "/github/configuration" , csrfProtection , oauth . checkGithubAuth , getGitHubConfiguration ) ; app . post ( "/github/configuration" , csrfProtection , oauth . checkGithubAuth , postGitHubConfiguration ) ; app . get ( "/github/subscriptions/:installationId" , csrfProtection , getGitHubSubscriptions ) ; app . post ( "/github/subscription" , csrfProtection , oauth . checkGithubAuth , deleteGitHubSubscription ) ; app . get ( "/jira/configuration" , csrfProtection , verifyJiraJwtTokenMiddleware , getJiraConfiguration ) ; app . delete ( "/jira/configuration" , verifyJiraContextJwtTokenMiddleware , deleteJiraConfiguration ) ; app . post ( "/jira/sync" , verifyJiraContextJwtTokenMiddleware , retrySync ) ; app . post ( "/jira/events/disabled" , ( _ , res ) => { return res . sendStatus ( 204 ) ; } ) ; app . post ( "/jira/events/enabled" , ( _ , res ) => { return res . sendStatus ( 204 ) ; } ) ; app . post ( "/jira/events/installed" , authenticateInstallCallback , postJiraInstall ) ; app . post ( "/jira/events/uninstalled" , authenticateUninstallCallback , extractInstallationFromJiraCallback , postJiraUninstall ) ; app . get ( "/" , async ( _ , res ) => { const { data : info } = await res . locals . client . apps . getAuthenticated ( { } ) ; return res . redirect ( info . external_url ) ; } ) ; app . use ( ( err , req , res , next ) => { Sentry . withScope ( ( scope ) : void => { const jiraHost = res . locals . jiraHost ; if ( jiraHost ) { scope . setTag ( "jiraHost" , jiraHost ) ; } if ( req . body ) { Sentry . setExtra ( "Body" , req . body ) ; } next ( err ) ; } ) ; } ) ; app . get ( [ "/error" , "/error/:message" , "/error/:message/:name" ] , ( req , res , next ) => { res . locals . showError = true ; const error = new Error ( req . params . message ) ; if ( req . params . name ) { error . name = req . params . name ; } next ( error ) ; } ) ; app . use ( Sentry . Handlers . errorHandler ( ) ) ; app . use ( async ( err , req , res , next ) => { req . log . error ( { payload : req . body , err , req , res } , "Error in frontend app." ) ; if ( ! isNodeProd ( ) && ! res . locals . showError ) { return next ( err ) ; } if ( err . name == "HttpError" && err . message ?. includes ( "organization has an IP allow list enabled" ) ) { err . message = Errors . IP_ALLOWLIST_MISCONFIGURED ; } const errorCodes = { Unauthorized : 401 , Forbidden : 403 , "Not Found" : 404 } ; const messages = { [ Errors . MISSING_JIRA_HOST ] : "Session information missing - please enable all cookies in your browser settings." , [ Errors . IP_ALLOWLIST_MISCONFIGURED ] : `The GitHub org you are trying to connect is currently blocking our requests. To configure the GitHub IP Allow List correctly, <a href="${ envVars . GITHUB_REPO_URL }/blob/main/docs/ip-allowlist.md">please follow these instructions</a>.` } ; const errorStatusCode = errorCodes [ err . message ] || 500 ; const message = messages [ err . message ] ; const tags = [ `status: ${ errorStatusCode }` ] ; statsd . increment ( metricError . githubErrorRendered , tags ) ; return res . status ( errorStatusCode ) . render ( "error.hbs" , { title : "GitHub + Jira integration" , message , nonce : res . locals . nonce , githubRepoUrl : envVars . GITHUB_REPO_URL } ) ; } ) ; return app ; } ;