Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 11 additions & 6 deletions integrations/monday/hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@

## Configuration

This integration makes use of a personal access token from Monday.com. You need to acquire your personal access token and provide it to the integration when you install it with your bot.
This integration connects to Monday.com with OAuth by default. You can also select manual configuration and provide a personal access token if you prefer to configure access manually.

### Monday

Along with your personal access token, you will need to identify the Board IDs of the Monday.com Boards you would like your bot to interact with.
You will need to identify the Board IDs of the Monday.com boards you would like your bot to interact with.

#### Access token
#### OAuth

Please refer to the [Authentication Guide](https://developer.monday.com/api-reference/docs/authentication#get-your-token) in the Monday documentation to learn how to acquire your personal access token.
Use the authorization button in Botpress to connect your Monday.com account. During the OAuth flow, Monday will ask you to approve access for this integration.

#### Personal access token

If you prefer manual configuration, refer to the [Authentication Guide](https://developer.monday.com/api-reference/docs/authentication#get-your-token) in the Monday documentation to learn how to acquire your token.

#### Board ID

Expand All @@ -27,5 +31,6 @@ In the URL provided above, the Board ID would be `9012345678`. Keep this (and an
### Botpress

1. Install the Monday integration in your Botpress bot.
2. Paste the personal access token in the configuration field.
3. Save configuration.
2. Use the default OAuth configuration and click the authorization button to complete the OAuth flow.
3. To configure manually, select **Manual Configuration** and paste your Monday.com personal access token.
4. Save configuration.
31 changes: 28 additions & 3 deletions integrations/monday/integration.definition.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { IntegrationDefinition, z } from '@botpress/sdk'
import { configurationSchema, createItemSchema } from 'src/misc/custom-schemas'
import { configurationSchema, createItemSchema, manualConfigurationSchema } from 'src/misc/custom-schemas'

export default new IntegrationDefinition({
name: 'monday',
title: 'Monday',
description: 'Manage items in Monday boards.',
version: '1.0.2',
version: '1.1.2',
readme: 'hub.md',
icon: 'icon.svg',
states: {},
states: {
oAuthCredentials: {
type: 'integration',
schema: z.object({
accessToken: z.string().secret().title('Access Token').describe('The Monday OAuth access token.'),
}),
},
},
actions: {
createItem: {
title: 'Create Item',
Expand All @@ -21,6 +28,24 @@ export default new IntegrationDefinition({
},
configuration: {
schema: configurationSchema,
identifier: {
linkTemplateScript: 'linkTemplate.vrl',
},
},
configurations: {
manual: {
title: 'Manual Configuration',
description: 'Configure with your Personal Access Token',
schema: manualConfigurationSchema,
},
},
secrets: {
CLIENT_ID: {
description: 'The client ID of the OAuth app.',
},
CLIENT_SECRET: {
description: 'The client secret of the OAuth app.',
},
},
attributes: {
category: 'Project Management',
Expand Down
4 changes: 4 additions & 0 deletions integrations/monday/linkTemplate.vrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
webhookId = to_string!(.webhookId)
webhookUrl = to_string!(.webhookUrl)

"{{ webhookUrl }}/oauth/wizard/oauth-redirect?state={{ webhookId }}"
1 change: 1 addition & 0 deletions integrations/monday/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"dependencies": {
"@botpress/client": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*",
"axios": "^1.4.0"
}
Expand Down
14 changes: 6 additions & 8 deletions integrations/monday/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { MondayClient } from 'src/misc/monday-client'
import { IntegrationProps } from '.botpress'
import { getMondayClient } from 'src/misc/auth'
import * as bp from '.botpress'

type CreateItem = IntegrationProps['actions']['createItem']
type CreateItem = bp.IntegrationProps['actions']['createItem']

export const createItem: CreateItem = async ({ input, ctx }) => {
const client = MondayClient.create({
personalAccessToken: ctx.configuration.personalAccessToken,
})
export const createItem: CreateItem = async ({ input, ctx, client }) => {
const mondayClient = await getMondayClient({ client, ctx })

await client.createItem(input.boardId, {
await mondayClient.createItem(input.boardId, {
name: input.itemName,
})

Expand Down
28 changes: 26 additions & 2 deletions integrations/monday/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import { RuntimeError } from '@botpress/sdk'
import * as actions from 'src/actions'
import { getMondayClient } from 'src/misc/auth'
import { isOAuthWizardUrl, oauthWizardHandler } from './oauth-wizard'
import * as bp from '.botpress'

export default new bp.Integration({
register: async () => {},
register: async ({ client, ctx }) => {
try {
const mondayClient = await getMondayClient({ client, ctx })
await mondayClient.validateAccessToken()

await client.configureIntegration({
identifier: ctx.webhookId,
})
} catch (thrown) {
if (thrown instanceof RuntimeError) {
throw thrown
}

const message = thrown instanceof Error ? thrown.message : String(thrown)
throw new RuntimeError(`Failed to configure Monday integration. Please reconnect your account. (${message})`)
}
},
unregister: async () => {},
actions,
channels: {},
handler: async () => {},
handler: async (props) => {
if (isOAuthWizardUrl(props.req.path)) {
return await oauthWizardHandler(props)
}
return { status: 404, body: 'Invalid endpoint' }
},
})
57 changes: 57 additions & 0 deletions integrations/monday/src/misc/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { RuntimeError, isApiError } from '@botpress/sdk'
import { MondayClient } from './monday-client'
import * as bp from '.botpress'

type AuthProps = {
client: bp.Client
ctx: bp.Context
}

export const getOAuthAccessToken = async ({ client, ctx }: AuthProps) => {
try {
const { state } = await client.getState({
type: 'integration',
name: 'oAuthCredentials',
id: ctx.integrationId,
})

return state?.payload.accessToken || undefined
} catch (thrown) {
if (isApiError(thrown) && thrown.code === 404) {
return undefined
}

throw thrown
}
}

export const getMondayClient = async (props: AuthProps) => {
if (props.ctx.configurationType === 'manual') {
const { personalAccessToken } = props.ctx.configuration

if (!personalAccessToken) {
throw new RuntimeError('Monday credentials are missing. Please provide a personal access token.')
}

return MondayClient.create({ authorization: personalAccessToken })
}

let oAuthAccessToken: string | undefined
try {
oAuthAccessToken = await getOAuthAccessToken(props)
} catch (thrown) {
const message = thrown instanceof Error ? thrown.message : String(thrown)
throw new RuntimeError(`Failed to load Monday OAuth credentials. Please reconnect your account. (${message})`)
}

if (!oAuthAccessToken) {
throw new RuntimeError(
'Monday credentials are missing. Please connect your Monday account or provide a personal access token.'
)
}

return createOAuthMondayClient(oAuthAccessToken)
}

export const createOAuthMondayClient = (accessToken: string) =>
MondayClient.create({ authorization: `Bearer ${accessToken}` })
9 changes: 5 additions & 4 deletions integrations/monday/src/misc/custom-schemas.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { z } from '@botpress/sdk'

export const configurationSchema = z.object({
export const configurationSchema = z.object({})

export const manualConfigurationSchema = z.object({
personalAccessToken: z
.string()
.min(1)
.secret()
.title('Personal Access Token')
.describe(
'The personal access token for your Monday.com account with sufficient access to manage items on your Monday.com boards.'
),
.describe('A Monday.com personal access token with sufficient access to manage items on your Monday.com boards.'),
})

export const createItemSchema = z.object({
Expand Down
14 changes: 14 additions & 0 deletions integrations/monday/src/misc/graphql-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ type GraphQLQuery<TInput, TResponse> = {
}

export const GRAPHQL_QUERIES = {
validateAccessToken: {
query: `
query ValidateAccessToken {
boards(limit: 1) {
id
}
}`,
[QUERY_INPUT]: {} as Record<string, never>,
[QUERY_RESPONSE]: {} as {
boards: {
id: string
}[]
},
},
createItem: {
query: `
mutation CreateNewItem($boardId: ID!, $itemName: String!) {
Expand Down
48 changes: 46 additions & 2 deletions integrations/monday/src/misc/monday-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@ import axios, { Axios } from 'axios'
import { GRAPHQL_QUERIES, QUERY_INPUT, QUERY_RESPONSE } from './graphql-queries'

export type MondayClientConfiguration = {
personalAccessToken: string
authorization: string
}

type MondayOAuthTokenResponse = {
access_token: string
}

export type MondayOAuthCredentials = {
accessToken: string
}

export type ExchangeCodeForTokensInput = {
clientId: string
clientSecret: string
redirectUri: string
code: string
}

export type CreateItemOptions = {
Expand All @@ -19,6 +34,24 @@ export type ItemsPageResponse = {
nextToken: string | undefined
}

export const exchangeCodeForTokens = async ({
clientId,
clientSecret,
redirectUri,
code,
}: ExchangeCodeForTokensInput): Promise<MondayOAuthCredentials> => {
const response = await axios.post<MondayOAuthTokenResponse>('https://auth.monday.com/oauth2/token', {
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
code,
})

return {
accessToken: response.data.access_token,
}
}

export class MondayClient {
private constructor(private readonly _client: Axios) {}

Expand All @@ -27,7 +60,7 @@ export class MondayClient {
baseURL: 'https://api.monday.com/v2',
timeout: 10_000,
headers: {
Authorization: config.personalAccessToken,
Authorization: config.authorization,
'API-Version': '2023-07',
'Content-Type': 'application/json',
},
Expand All @@ -48,6 +81,17 @@ export class MondayClient {
return response.data.data
}

public async validateAccessToken(): Promise<void> {
try {
const response = await this._executeGraphqlQuery('validateAccessToken', {})
if (!Array.isArray(response.boards)) {
throw new Error('Monday credentials validation returned an unexpected response.')
}
} catch (thrown) {
throw thrown instanceof Error ? thrown : new Error('Monday credentials validation failed.')
}
}

public async createItem(
boardId: string,
item: CreateItemOptions
Expand Down
15 changes: 15 additions & 0 deletions integrations/monday/src/oauth-wizard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as wizard from './wizard'
import * as bp from '.botpress'

export const oauthWizardHandler: bp.IntegrationProps['handler'] = async (props) => {
const { logger } = props
try {
return await wizard.handler(props)
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
logger.forBot().error(`OAuth wizard error: ${error.message}`)
return wizard.redirectToInterstitial(false, error.message)
}
}

export const isOAuthWizardUrl = wizard.isOAuthWizardUrl
Loading
Loading