Skip to content

Feat: jira integration #3002

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 93 additions & 7 deletions backend/src/services/integrationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@crowd/common'
import {
NangoIntegration,
connectNangoIntegration,
createNangoConnection,
createNangoGithubConnection,
deleteNangoConnection,
Expand Down Expand Up @@ -60,6 +61,7 @@ import {
import { getOrganizations } from '../serverless/integrations/usecases/linkedin/getOrganizations'
import getToken from '../serverless/integrations/usecases/nango/getToken'
import { getIntegrationRunWorkerEmitter } from '../serverless/utils/queueService'
import { JiraIntegrationData } from '../types/jiraTypes'
import { encryptData } from '../utils/crypto'

import { IServiceOptions } from './IServiceOptions'
Expand Down Expand Up @@ -1618,35 +1620,119 @@ export default class IntegrationService {
}

/**
* Adds/updates Jira integration
* Adds/updates Jira integration (using nango)
* @param integrationData to create the integration object
* @returns integration object
* @remarks
* Supports the following authentication methods:
* 1. Jira Cloud (basic auth): Requires URL, username, and password (API key)
* 2. Jira Data Center (PAT): Requires URL and optionally a Personal Access Token
* 3. Jira Data Center (basic auth): Requires URL, username, and password (API key)
*/
async jiraConnectOrUpdate(integrationData) {
async jiraConnectOrUpdate(integrationData: JiraIntegrationData) {
const transaction = await SequelizeRepository.createTransaction(this.options)
let integration: any
let connectionId: string
try {
const constructNangoConnectionPayload = (
integrationData: JiraIntegrationData,
): Record<string, any> => {
let jiraIntegrationType: NangoIntegration
// nangoPayload is different for each integration
// check https://github.com/NangoHQ/nango/blob/bf0aa529ad3b6af1c72ca6a30ccdde7a3e47d064/packages/providers/providers.yaml#L5007
let nangoPayload: any
const ATLASSIAN_CLOUD_SUFFIX = '.atlassian.net' as const

const baseUrl = integrationData.url.trim()
const hostname = new URL(baseUrl).hostname
const isCloudUrl = hostname.endsWith(ATLASSIAN_CLOUD_SUFFIX)
const subdomain = isCloudUrl ? hostname.split(ATLASSIAN_CLOUD_SUFFIX)[0] : null

if (isCloudUrl && integrationData.username && integrationData.apiToken) {
jiraIntegrationType = NangoIntegration.JIRA_CLOUD_BASIC
nangoPayload = {
params: {
subdomain,
},
credentials: {
username: integrationData.username,
password: integrationData.apiToken,
},
}
return { jiraIntegrationType, nangoPayload }
}

if (!isCloudUrl && integrationData.username && integrationData.apiToken) {
jiraIntegrationType = NangoIntegration.JIRA_DATA_CENTER_BASIC
nangoPayload = {
params: {
baseUrl,
},
credentials: {
username: integrationData.username,
password: integrationData.apiToken,
},
}
return { jiraIntegrationType, nangoPayload }
}

jiraIntegrationType = NangoIntegration.JIRA_DATA_CENTER_API_KEY
nangoPayload = {
params: {
baseUrl,
},
credentials: {
apiKey: integrationData.personalAccessToken,
},
}

return { jiraIntegrationType, nangoPayload }
}

const { jiraIntegrationType, nangoPayload } = constructNangoConnectionPayload(integrationData)
this.options.log.info(
`jira integration type determined: ${jiraIntegrationType}, starting nango connection...`,
)
connectionId = await connectNangoIntegration(jiraIntegrationType, nangoPayload)

if (integrationData.projects && integrationData.projects.length > 0) {
await setNangoMetadata(jiraIntegrationType, connectionId, {
projectIdsToSync: integrationData.projects.map((project) => project.toUpperCase()),
})
}

integration = await this.createOrUpdate(
{
id: connectionId,
platform: PlatformType.JIRA,
settings: {
url: integrationData.url,
auth: {
username: integrationData.username,
personalAccessToken: integrationData.personalAccessToken,
apiToken: integrationData.apiToken,
personalAccessToken: integrationData.personalAccessToken
? encryptData(integrationData.personalAccessToken)
: null,
apiToken: integrationData.apiToken ? encryptData(integrationData.apiToken) : null,
},
nangoIntegrationName: jiraIntegrationType,
projects: integrationData.projects.map((project) => project.toUpperCase()),
},
status: 'done',
},
transaction,
)

await startNangoSync(jiraIntegrationType, connectionId)
await SequelizeRepository.commitTransaction(transaction)
} catch (err) {
} catch (error) {
await SequelizeRepository.rollbackTransaction(transaction)
throw err
if (error instanceof TypeError && error.message.includes('Invalid URL')) {
this.options.log.error(`Invalid url: ${integrationData.url}`)
throw new Error400(this.options.language, 'errors.jira.invalidUrl')
}
if (error && error.message.includes('credentials')) {
throw new Error400(this.options.language, 'errors.jira.invalidCredentials')
}
throw error
}
return integration
}
Expand Down
7 changes: 7 additions & 0 deletions backend/src/types/jiraTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface JiraIntegrationData {
url: string
username?: string
personalAccessToken?: string
apiToken?: string
projects?: string[]
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
<template>
<lf-tooltip
placement="top"
content-class="!max-w-76 !p-3 !text-start"
content="These integrations are temporarily disabled. Please contact the CM team for further questions."
>
<lf-button
:disabled="true"
type="secondary"
@click="isJiraSettingsDrawerVisible = true"
>
<div class="flex items-center gap-4">
<!-- <lf-button type="secondary-ghost" @click="isDetailsModalOpen = true">-->
<!-- <lf-icon name="circle-info" type="regular" />-->
<!-- Details-->
<!-- </lf-button>-->
<lf-button type="secondary" @click="isJiraSettingsDrawerVisible = true">
<lf-icon name="link-simple" />
<slot>Connect</slot>
</lf-button>
</lf-tooltip>
</div>
<lf-jira-settings-drawer
v-if="isJiraSettingsDrawerVisible"
v-model="isJiraSettingsDrawerVisible"
Expand All @@ -26,7 +22,6 @@
import { defineProps, ref } from 'vue';
import LfIcon from '@/ui-kit/icon/Icon.vue';
import LfButton from '@/ui-kit/button/Button.vue';
import LfTooltip from '@/ui-kit/tooltip/Tooltip.vue';
import LfJiraSettingsDrawer from '@/config/integrations/jira/components/jira-settings-drawer.vue';

const props = defineProps<{
Expand Down
1 change: 0 additions & 1 deletion frontend/src/modules/integration/integration-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,6 @@ export default {
});
} catch (error) {
Errors.handle(error);
Message.error('Something went wrong. Please try again later.');
commit('CREATE_ERROR');
}
},
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/modules/lf/layout/components/lf-banners.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,16 @@
</router-link>
</div>
</banner>
<!-- TODO: Remove this banner once Jira and Confluence integrations are back up -->
<!-- TODO: Remove this banner once Confluence integrations is back up -->
<banner
variant="alert"
>
<div
class="flex flex-wrap items-center justify-center grow text-sm py-2"
>
<span class="font-semibold">Temporary Disruption of Confluence and Jira Integrations</span>
<span>&nbsp;Confluence and Jira integrations are currently stopped.
The team is actively working on bringing the integrations back and restore full functionality.</span>
<span class="font-semibold">Temporary Disruption of Confluence Integration</span>
<span>&nbsp;Confluence integration is currently stopped.
The team is actively working on bringing the integration back and restore full functionality.</span>
</div>
</banner>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ const job: IJobDefinition = {
await initNangoCloudClient()
const dbConnection = await getDbConnection(READ_DB_CONFIG(), 3, 0)

const allIntegrations = await fetchNangoIntegrationData(
pgpQx(dbConnection),
ALL_NANGO_INTEGRATIONS.map(nangoIntegrationToPlatform),
)
const allIntegrations = await fetchNangoIntegrationData(pgpQx(dbConnection), [
...new Set(ALL_NANGO_INTEGRATIONS.map(nangoIntegrationToPlatform)),
])

const nangoConnections = await getNangoConnections()

Expand Down
11 changes: 6 additions & 5 deletions services/apps/cron_service/src/jobs/nangoMonitoring.job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ const job: IJobDefinition = {
await initNangoCloudClient()
const dbConnection = await getDbConnection(READ_DB_CONFIG(), 3, 0)

const allIntegrations = await fetchNangoIntegrationData(
pgpQx(dbConnection),
ALL_NANGO_INTEGRATIONS.map(nangoIntegrationToPlatform),
)
const allIntegrations = await fetchNangoIntegrationData(pgpQx(dbConnection), [
...new Set(ALL_NANGO_INTEGRATIONS.map(nangoIntegrationToPlatform)),
])

const nangoConnections = await getNangoConnections()

Expand Down Expand Up @@ -75,7 +74,9 @@ const job: IJobDefinition = {
ctx.log.warn(`${int.platform} integration with id "${int.id}" is not connected to Nango!`)
} else {
const results = await getNangoConnectionStatus(
int.platform as NangoIntegration,
int.platform == PlatformType.JIRA
? (int.settings.nangoIntegrationName as NangoIntegration)
: (int.platform as NangoIntegration),
nangoConnection.connection_id,
)

Expand Down
9 changes: 4 additions & 5 deletions services/apps/cron_service/src/jobs/nangoTrigger.job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,14 @@ const job: IJobDefinition = {

const dbConnection = await getDbConnection(READ_DB_CONFIG(), 3, 0)

const integrationsToTrigger = await fetchNangoIntegrationData(
pgpQx(dbConnection),
ALL_NANGO_INTEGRATIONS.map(nangoIntegrationToPlatform),
)
const integrationsToTrigger = await fetchNangoIntegrationData(pgpQx(dbConnection), [
...new Set(ALL_NANGO_INTEGRATIONS.map(nangoIntegrationToPlatform)),
])

for (const int of integrationsToTrigger) {
const { id, settings } = int

const platform = platformToNangoIntegration(int.platform as PlatformType)
const platform = platformToNangoIntegration(int.platform as PlatformType, settings)

if (platform === NangoIntegration.GITHUB && !settings.nangoMapping) {
// ignore non-nango github integrations
Expand Down
5 changes: 4 additions & 1 deletion services/apps/nango_worker/src/bin/full-resync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ setImmediate(async () => {
)

if (integration) {
const nangoIntegration = platformToNangoIntegration(integration.platform as PlatformType)
const nangoIntegration = platformToNangoIntegration(
integration.platform as PlatformType,
integration.settings,
)

try {
const toTrigger: string[] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ setImmediate(async () => {
log.info(
`Triggering nango integration check for integrationId '${integrationId}' and connectionId '${connectionId}'!`,
)
const providerConfigKey = platformToNangoIntegration(integration.platform as PlatformType)
const providerConfigKey = platformToNangoIntegration(
integration.platform as PlatformType,
integration.settings,
)

await dbConnection.none(
`
Expand Down
6 changes: 6 additions & 0 deletions services/libs/common/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ const en = {
git: {
noIntegration: 'The Git integration is not configured.',
},
jira: {
invalidUrl:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We dont use translations anymore so in further prs you can also include directly in code.

'The URL provided is invalid. Please enter a valid URL format such as https://example.com or https://example.atlassian.net',
invalidCredentials:
'The given credentials were found to be invalid. Please check the credentials and try again',
},
alreadyExists: '{0}',
},

Expand Down
21 changes: 21 additions & 0 deletions services/libs/nango/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@ export const createNangoGithubConnection = async (
}
}

export const connectNangoIntegration = async (
integration: NangoIntegration,
params: any,
): Promise<string> => {
ensureBackendClient()

log.info({ params, integration }, 'Creating a nango connection...')
const data = await getNangoCloudSessionToken()

if (!frontendModule) {
frontendModule = await import('@nangohq/frontend')
}

const frontendClient = new frontendModule.default({
connectSessionToken: data.token,
}) as Nango

const result = await frontendClient.auth(integration, params)
return result.connectionId
}

export const createNangoConnection = async (
integration: NangoIntegration,
params: any,
Expand Down
Loading
Loading