import express , { NextFunction , Request , Response } from "express" ; import { check , oneOf , validationResult } from "express-validator" ; import format from "date-fns/format" ; import rateLimit from "express-rate-limit" ; import RedisStore from "rate-limit-redis" ; import Redis from "ioredis" ; import BodyParser from "body-parser" ; import GithubAPI from "../config/github-api" ; import { Installation , RepoSyncState , Subscription } from "../models" ; import verifyInstallation from "../jira/verify-installation" ; import logMiddleware from "../middleware/frontend-log-middleware" ; import JiraClient from "../models/jira-client" ; import uninstall from "../jira/uninstall" ; import { serializeJiraInstallation , serializeSubscription } from "./serializers" ; import getRedisInfo from "../config/redis-info" ; import { WhereOptions } from "sequelize" ; import getJiraClient from "../jira/client" ; import { findOrStartSync } from "../sync/sync-utils" ; const router = express . Router ( ) ; const bodyParser = BodyParser . urlencoded ( { extended : false } ) ; async function getInstallation ( client , subscription ) { const id = subscription . gitHubInstallationId ; try { const response = await client . apps . getInstallation ( { installation_id : id } ) ; response . data . syncStatus = subscription . syncStatus ; return response . data ; } catch ( err ) { return { error : err , id , deleted : err . status === 404 } ; } } function validAdminPermission ( viewer ) { return viewer . organization ?. viewerCanAdminister || false ; } /**
 * Finds the validation errors in this request and wraps them in an object with handy functions
 */ function returnOnValidationError ( req , res , next ) { const errors = validationResult ( req ) ; if ( ! errors . isEmpty ( ) ) { res . status ( 422 ) . json ( { errors : errors . array ( ) } ) ; } next ( ) ; } const viewerPermissionQuery = `{
  viewer {
    login
    organization(login: "fusion-arc") {
      viewerCanAdminister
    }
  }
}
` ; router . use ( rateLimit ( { store : new RedisStore ( { client : new Redis ( getRedisInfo ( "express-rate-limit" ) ) } ) , windowMs : 60 * 1000 , max : 60 } ) ) ; router . use ( logMiddleware ) ; router . use ( async ( req , res , next ) : Promise < void > => { const token = req . get ( "Authorization" ) ; if ( ! token ) { res . sendStatus ( 404 ) ; return ; } try { const octokit = GithubAPI ( { auth : token . split ( " " ) [ 1 ] } ) ; const { data , errors } = ( await octokit . request ( { headers : { Accept : "application/json" , "Content-Type" : "application/json" } , method : "POST" , query : viewerPermissionQuery , url : "/graphql" } ) ) . data ; req . addLogFields ( { login : data && data . viewer && data . viewer . login } ) ; if ( errors ) { res . status ( 401 ) . json ( { errors , viewerPermissionQuery } ) ; return ; } if ( ! validAdminPermission ( data . viewer ) ) { req . log . info ( `User attempted to access staff routes: login=${ data . viewer . login }, viewerCanAdminister=${ data . viewer . organization ?. viewerCanAdminister }` ) ; res . status ( 401 ) . json ( { error : "Unauthorized" , message : "Token provided does not have required access" } ) ; return ; } req . log . info ( `Staff routes accessed: login=${ data . viewer . login }, viewerCanAdminister=${ data . viewer . organization ?. viewerCanAdminister }` ) ; next ( ) ; } catch ( err ) { req . log . info ( { err } ) ; if ( err . status === 401 ) { res . status ( 401 ) . send ( err . HttpError ) ; return ; } res . sendStatus ( 500 ) ; } } ) ; router . get ( "/" , ( _ , res ) : void => { res . send ( { } ) ; } ) ; router . get ( "/:installationId/:jiraHost/syncstate" , check ( "installationId" ) . isInt ( ) , check ( "jiraHost" ) . isString ( ) , returnOnValidationError , async ( req , res ) : Promise < void > => { const githubInstallationId = Number ( req . params . installationId ) ; const jiraHost = req . params . jiraHost ; if ( ! jiraHost || ! githubInstallationId ) { const msg = "Missing Jira Host or Installation ID" ; req . log . warn ( { req , res } , msg ) ; res . status ( 400 ) . send ( msg ) ; return ; } try { const subscription = await Subscription . getSingleInstallation ( jiraHost , githubInstallationId ) ; if ( ! subscription ) { res . status ( 404 ) . send ( `No Subscription found for jiraHost "${ jiraHost }" and installationId "${ githubInstallationId }"` ) ; return ; } res . json ( await RepoSyncState . toRepoJson ( subscription ) ) ; } catch ( err ) { res . status ( 500 ) . json ( err ) ; } } ) ; router . post ( "/:installationId/sync" , bodyParser , check ( "installationId" ) . isInt ( ) , returnOnValidationError , async ( req , res ) : Promise < void > => { const githubInstallationId = Number ( req . params . installationId ) ; req . log . info ( req . body ) ; const { jiraHost , resetType } = req . body ; try { req . log . info ( jiraHost , githubInstallationId ) ; const subscription = await Subscription . getSingleInstallation ( jiraHost , githubInstallationId ) ; if ( ! subscription ) { res . sendStatus ( 404 ) ; return ; } await findOrStartSync ( subscription , req . log , resetType ) ; res . status ( 202 ) . json ( { message : `Successfully (re)started sync for ${ githubInstallationId }` } ) ; } catch ( err ) { req . log . info ( err ) ; res . sendStatus ( 401 ) ; } } ) ; router . post ( "/resync" , bodyParser , async ( req , res ) : Promise < void > => { const syncType = req . body . syncType || "partial" ; const statusTypes = req . body . statusTypes as string [ ] ; const installationIds = req . body . installationIds as number [ ] ; const limit = Number ( req . body . limit ) || undefined ; const offset = Number ( req . body . offset ) || 0 ; const inactiveForSeconds = Number ( req . body . inactiveForSeconds ) || undefined ; const subscriptions = await Subscription . getAllFiltered ( installationIds , statusTypes , offset , limit , inactiveForSeconds ) ; await Promise . all ( subscriptions . map ( ( subscription ) => findOrStartSync ( subscription , req . log , syncType ) ) ) ; res . json ( subscriptions . map ( serializeSubscription ) ) ; } ) ; router . post ( "/dedupInstallationQueue" , bodyParser , async ( _ , res ) : Promise < void > => { res . send ( `This endpoint doesn't do anything useful anymore` ) ; } ) ; router . post ( "/requeue" , bodyParser , async ( _ , res ) : Promise < void > => { res . send ( `This endpoint doesn't do anything useful anymore` ) ; } ) ; router . get ( "/jira/:clientKeyOrJiraHost" , [ bodyParser , oneOf ( [ check ( "clientKeyOrJiraHost" ) . isURL ( ) , check ( "clientKeyOrJiraHost" ) . isHexadecimal ( ) ] ) , returnOnValidationError ] , async ( req , res ) : Promise < void > => { const where = req . params . clientKeyOrJiraHost . startsWith ( "http" ) ? { jiraHost : req . params . clientKeyOrJiraHost } : { clientKey : req . params . clientKeyOrJiraHost } ; const jiraInstallations = await Installation . findAll ( { where } ) ; if ( ! jiraInstallations . length ) { res . sendStatus ( 404 ) ; return ; } res . json ( jiraInstallations . map ( ( jiraInstallation ) => serializeJiraInstallation ( jiraInstallation , req . log ) ) ) ; } ) ; router . post ( "/jira/:clientKey/uninstall" , bodyParser , check ( "clientKey" ) . isHexadecimal ( ) , returnOnValidationError , async ( request , response ) : Promise < void > => { response . locals . installation = await Installation . findOne ( { where : { clientKey : request . params . clientKey } } ) ; if ( ! response . locals . installation ) { response . sendStatus ( 404 ) ; return ; } const jiraClient = new JiraClient ( response . locals . installation , request . log ) ; const checkAuthorization = request . body . force !== "true" ; if ( checkAuthorization && ( await jiraClient . isAuthorized ( ) ) ) { response . status ( 400 ) . json ( { message : "Refusing to uninstall authorized Jira installation" } ) ; return ; } request . log . info ( `Forcing uninstall for ${ response . locals . installation . clientKey }` ) ; await uninstall ( request , response ) ; } ) ; router . post ( "/jira/:installationId/verify" , bodyParser , check ( "installationId" ) . isInt ( ) , returnOnValidationError , async ( req , res ) : Promise < void > => { const { installationId } = req . params ; const installation = await Installation . findByPk ( installationId ) ; const isValid = await verifyInstallation ( installation , req . log ) ( ) ; res . json ( { message : isValid ? "Verification successful" : "Verification failed" , installation : { enabled : isValid , id : installation . id , jiraHost : installation . jiraHost } } ) ; } ) ; router . get ( "/:installationId" , check ( "installationId" ) . isInt ( ) , returnOnValidationError , async ( req , res ) : Promise < void > => { const { installationId } = req . params ; const { client } = res . locals ; try { const subscriptions = await Subscription . getAllForInstallation ( Number ( installationId ) ) ; if ( ! subscriptions . length ) { res . sendStatus ( 404 ) ; return ; } const { jiraHost } = subscriptions [ 0 ] ; const installations = await Promise . all ( subscriptions . map ( ( subscription ) => getInstallation ( client , subscription ) ) ) ; const connections = installations . filter ( ( response ) => ! response . error ) . map ( ( data ) => ( { ... data , isGlobalInstall : data . repository_selection === "all" , updated_at : format ( data . updated_at , "MMMM D, YYYY h:mm a" ) , syncState : data . syncState } ) ) ; const failedConnections = installations . filter ( ( response ) => { req . log . error ( { ... response } , "Failed installation" ) ; return response . error ; } ) ; res . json ( { host : jiraHost , installationId , connections , failedConnections , hasConnections : connections . length > 0 || failedConnections . length > 0 , syncStateUrl : `${ req . protocol }://${ req . get ( "host" ) }/api/${ installationId }/${ encodeURIComponent ( jiraHost ) }/syncstate` } ) ; } catch ( err ) { req . log . error ( { installationId , err } , "Error getting installation" ) ; res . status ( 500 ) . json ( err ) ; } } ) ; router . delete ( "/deleteInstallation/:installationId/:jiraHost" , check ( "installationId" ) . isInt ( ) , check ( "jiraHost" ) . isString ( ) , returnOnValidationError , async ( req , res ) : Promise < void > => { const githubInstallationId = req . params . installationId ; const jiraHost = req . params . jiraHost ; if ( ! jiraHost || ! githubInstallationId ) { const msg = "Missing Jira Host or Installation ID" ; req . log . warn ( { req , res } , msg ) ; res . status ( 400 ) . send ( msg ) ; return ; } const subscription = await Subscription . getSingleInstallation ( jiraHost , Number ( githubInstallationId ) ) ; if ( ! subscription ) { req . log . info ( "no subscription" ) ; res . sendStatus ( 404 ) ; return ; } try { const jiraClient = await getJiraClient ( jiraHost , Number ( githubInstallationId ) , req . log ) ; req . log . info ( `Deleting dev info for jiraHost: ${ jiraHost } githubInstallationId: ${ githubInstallationId }` ) ; await jiraClient . devinfo . installation . delete ( githubInstallationId ) ; res . status ( 200 ) . send ( `devinfo deleted for jiraHost: ${ jiraHost } githubInstallationId: ${ githubInstallationId }` ) ; } catch ( err ) { res . status ( 500 ) . json ( err ) ; } } ) ; export default router ;