Feature/Add teams, gmail, outlook tools (#4577)

* add teams, gmail, outlook tools

* update docs link

* update credentials for oauth2

* add jira tool

* add google drive, google calendar, google sheets tools, powerpoint, excel, word doc loader

* update jira logo

* Refactor Gmail and Outlook tools to remove maxOutputLength parameter and enhance request handling. Update response formatting to include parameters in the output. Adjust Google Drive tools to simplify success messages by removing unnecessary parameter details.
This commit is contained in:
Henry Heng
2025-06-06 19:52:04 +01:00
committed by GitHub
parent 6dcb65cedb
commit 30c4180d97
62 changed files with 16832 additions and 144 deletions
+2
View File
@@ -31,6 +31,7 @@ import nodeCustomFunctionRouter from './node-custom-functions'
import nodeIconRouter from './node-icons'
import nodeLoadMethodRouter from './node-load-methods'
import nodesRouter from './nodes'
import oauth2Router from './oauth2'
import openaiAssistantsRouter from './openai-assistants'
import openaiAssistantsFileRouter from './openai-assistants-files'
import openaiAssistantsVectorStoreRouter from './openai-assistants-vector-store'
@@ -100,6 +101,7 @@ router.use('/node-custom-function', nodeCustomFunctionRouter)
router.use('/node-icon', nodeIconRouter)
router.use('/node-load-method', nodeLoadMethodRouter)
router.use('/nodes', nodesRouter)
router.use('/oauth2-credential', oauth2Router)
router.use('/openai-assistants', openaiAssistantsRouter)
router.use('/openai-assistants-file', openaiAssistantsFileRouter)
router.use('/openai-assistants-vector-store', openaiAssistantsVectorStoreRouter)
+422
View File
@@ -0,0 +1,422 @@
/**
* OAuth2 Authorization Code Flow Implementation
*
* This module implements a complete OAuth2 authorization code flow for Flowise credentials.
* It supports Microsoft Graph and other OAuth2 providers.
*
* CREDENTIAL DATA STRUCTURE:
* The credential's encryptedData should contain a JSON object with the following fields:
*
* Required fields:
* - client_id: OAuth2 application client ID
* - client_secret: OAuth2 application client secret
*
* Optional fields (provider-specific):
* - tenant_id: Microsoft Graph tenant ID (if using Microsoft Graph)
* - authorization_endpoint: Custom authorization URL (defaults to Microsoft Graph if tenant_id provided)
* - token_endpoint: Custom token URL (defaults to Microsoft Graph if tenant_id provided)
* - redirect_uri: Custom redirect URI (defaults to this callback endpoint)
* - scope: OAuth2 scopes to request (e.g., "user.read mail.read")
* - response_type: OAuth2 response type (defaults to "code")
* - response_mode: OAuth2 response mode (defaults to "query")
*
* ENDPOINTS:
*
* 1. POST /api/v1/oauth2/authorize/:credentialId
* - Generates authorization URL for initiating OAuth2 flow
* - Uses credential ID as state parameter for security
* - Returns authorization URL to redirect user to
*
* 2. GET /api/v1/oauth2/callback
* - Handles OAuth2 callback with authorization code
* - Exchanges code for access token
* - Updates credential with token data
* - Supports Microsoft Graph and custom OAuth2 providers
*
* 3. POST /api/v1/oauth2/refresh/:credentialId
* - Refreshes expired access tokens using refresh token
* - Updates credential with new token data
*
* USAGE FLOW:
* 1. Create a credential with OAuth2 configuration (client_id, client_secret, etc.)
* 2. Call POST /oauth2/authorize/:credentialId to get authorization URL
* 3. Redirect user to authorization URL
* 4. User authorizes and gets redirected to callback endpoint
* 5. Callback endpoint exchanges code for tokens and saves them
* 6. Use POST /oauth2/refresh/:credentialId when tokens expire
*
* TOKEN STORAGE:
* After successful authorization, the credential will contain additional fields:
* - access_token: OAuth2 access token
* - refresh_token: OAuth2 refresh token (if provided)
* - token_type: Token type (usually "Bearer")
* - expires_in: Token lifetime in seconds
* - expires_at: Token expiry timestamp (ISO string)
* - granted_scope: Actual scopes granted by provider
* - token_received_at: When token was received (ISO string)
*/
import express from 'express'
import axios from 'axios'
import { Request, Response, NextFunction } from 'express'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { Credential } from '../../database/entities/Credential'
import { decryptCredentialData, encryptCredentialData } from '../../utils'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
import { generateSuccessPage, generateErrorPage } from './templates'
const router = express.Router()
// Initiate OAuth2 authorization flow
router.post('/authorize/:credentialId', async (req: Request, res: Response, next: NextFunction) => {
try {
const { credentialId } = req.params
const appServer = getRunningExpressApp()
const credentialRepository = appServer.AppDataSource.getRepository(Credential)
// Find credential by ID
const credential = await credentialRepository.findOneBy({
id: credentialId
})
if (!credential) {
return res.status(404).json({
success: false,
message: 'Credential not found'
})
}
// Decrypt the credential data to get OAuth configuration
const decryptedData = await decryptCredentialData(credential.encryptedData)
const {
clientId,
authorizationUrl,
redirect_uri,
scope,
response_type = 'code',
response_mode = 'query',
additionalParameters = ''
} = decryptedData
if (!clientId) {
return res.status(400).json({
success: false,
message: 'Missing clientId in credential data'
})
}
if (!authorizationUrl) {
return res.status(400).json({
success: false,
message: 'No authorizationUrl specified in credential data'
})
}
const defaultRedirectUri = `${req.protocol}://${req.get('host')}/api/v1/oauth2-credential/callback`
const finalRedirectUri = redirect_uri || defaultRedirectUri
const authParams = new URLSearchParams({
client_id: clientId,
response_type,
response_mode,
state: credentialId, // Use credential ID as state parameter
redirect_uri: finalRedirectUri
})
if (scope) {
authParams.append('scope', scope)
}
let fullAuthorizationUrl = `${authorizationUrl}?${authParams.toString()}`
if (additionalParameters) {
fullAuthorizationUrl += `&${additionalParameters.toString()}`
}
res.json({
success: true,
message: 'Authorization URL generated successfully',
credentialId,
authorizationUrl: fullAuthorizationUrl,
redirectUri: finalRedirectUri
})
} catch (error) {
next(
new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`OAuth2 authorization error: ${error instanceof Error ? error.message : 'Unknown error'}`
)
)
}
})
// OAuth2 callback endpoint
router.get('/callback', async (req: Request, res: Response) => {
try {
const { code, state, error, error_description } = req.query
if (error) {
const errorHtml = generateErrorPage(
error as string,
(error_description as string) || 'An error occurred',
error_description ? `Description: ${error_description}` : undefined
)
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
if (!code || !state) {
const errorHtml = generateErrorPage('Missing required parameters', 'Missing code or state', 'Please try again later.')
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
const appServer = getRunningExpressApp()
const credentialRepository = appServer.AppDataSource.getRepository(Credential)
// Find credential by state (assuming state contains the credential ID)
const credential = await credentialRepository.findOneBy({
id: state as string
})
if (!credential) {
const errorHtml = generateErrorPage(
'Credential not found',
`Credential not found for the provided state: ${state}`,
'Please try the authorization process again.'
)
res.setHeader('Content-Type', 'text/html')
return res.status(404).send(errorHtml)
}
const decryptedData = await decryptCredentialData(credential.encryptedData)
const { clientId, clientSecret, accessTokenUrl, redirect_uri, scope } = decryptedData
if (!clientId || !clientSecret) {
const errorHtml = generateErrorPage(
'Missing OAuth configuration',
'Missing clientId or clientSecret',
'Please check your credential setup.'
)
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
let tokenUrl = accessTokenUrl
if (!tokenUrl) {
const errorHtml = generateErrorPage(
'Missing token endpoint URL',
'No Access Token URL specified in credential data',
'Please check your credential configuration.'
)
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
const defaultRedirectUri = `${req.protocol}://${req.get('host')}/api/v1/oauth2-credential/callback`
const finalRedirectUri = redirect_uri || defaultRedirectUri
const tokenRequestData: any = {
client_id: clientId,
client_secret: clientSecret,
code: code as string,
grant_type: 'authorization_code',
redirect_uri: finalRedirectUri
}
if (scope) {
tokenRequestData.scope = scope
}
const tokenResponse = await axios.post(tokenUrl, new URLSearchParams(tokenRequestData).toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json'
}
})
const tokenData = tokenResponse.data
// Update the credential data with token information
const updatedCredentialData: any = {
...decryptedData,
...tokenData,
token_received_at: new Date().toISOString()
}
// Add refresh token if provided
if (tokenData.refresh_token) {
updatedCredentialData.refresh_token = tokenData.refresh_token
}
// Calculate token expiry time
if (tokenData.expires_in) {
const expiryTime = new Date(Date.now() + tokenData.expires_in * 1000)
updatedCredentialData.expires_at = expiryTime.toISOString()
}
// Encrypt the updated credential data
const encryptedData = await encryptCredentialData(updatedCredentialData)
// Update the credential in the database
await credentialRepository.update(credential.id, {
encryptedData,
updatedDate: new Date()
})
// Return HTML that closes the popup window on success
const successHtml = generateSuccessPage(credential.id)
res.setHeader('Content-Type', 'text/html')
res.send(successHtml)
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error
const errorHtml = generateErrorPage(
axiosError.response?.data?.error || 'token_exchange_failed',
axiosError.response?.data?.error_description || 'Token exchange failed',
axiosError.response?.data?.error_description ? `Description: ${axiosError.response?.data?.error_description}` : undefined
)
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
// Generic error HTML page
const errorHtml = generateErrorPage(
'An unexpected error occurred',
'Please try again later.',
error instanceof Error ? error.message : 'Unknown error'
)
res.setHeader('Content-Type', 'text/html')
res.status(500).send(errorHtml)
}
})
// Refresh OAuth2 access token
router.post('/refresh/:credentialId', async (req: Request, res: Response, next: NextFunction) => {
try {
const { credentialId } = req.params
const appServer = getRunningExpressApp()
const credentialRepository = appServer.AppDataSource.getRepository(Credential)
const credential = await credentialRepository.findOneBy({
id: credentialId
})
if (!credential) {
return res.status(404).json({
success: false,
message: 'Credential not found'
})
}
const decryptedData = await decryptCredentialData(credential.encryptedData)
const { clientId, clientSecret, refresh_token, accessTokenUrl, scope } = decryptedData
if (!clientId || !clientSecret || !refresh_token) {
return res.status(400).json({
success: false,
message: 'Missing required OAuth configuration: clientId, clientSecret, or refresh_token'
})
}
let tokenUrl = accessTokenUrl
if (!tokenUrl) {
return res.status(400).json({
success: false,
message: 'No Access Token URL specified in credential data'
})
}
const refreshRequestData: any = {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token
}
if (scope) {
refreshRequestData.scope = scope
}
const tokenResponse = await axios.post(tokenUrl, new URLSearchParams(refreshRequestData).toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json'
}
})
// Extract token data from response
const tokenData = tokenResponse.data
// Update the credential data with new token information
const updatedCredentialData: any = {
...decryptedData,
...tokenData,
token_received_at: new Date().toISOString()
}
// Update refresh token if a new one was provided
if (tokenData.refresh_token) {
updatedCredentialData.refresh_token = tokenData.refresh_token
}
// Calculate token expiry time
if (tokenData.expires_in) {
const expiryTime = new Date(Date.now() + tokenData.expires_in * 1000)
updatedCredentialData.expires_at = expiryTime.toISOString()
}
// Encrypt the updated credential data
const encryptedData = await encryptCredentialData(updatedCredentialData)
// Update the credential in the database
await credentialRepository.update(credential.id, {
encryptedData,
updatedDate: new Date()
})
// Return success response
res.json({
success: true,
message: 'OAuth2 token refreshed successfully',
credentialId: credential.id,
tokenInfo: {
...tokenData,
has_new_refresh_token: !!tokenData.refresh_token,
expires_at: updatedCredentialData.expires_at
}
})
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error
return res.status(400).json({
success: false,
message: `Token refresh failed: ${axiosError.response?.data?.error_description || axiosError.message}`,
details: axiosError.response?.data
})
}
next(
new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`OAuth2 token refresh error: ${error instanceof Error ? error.message : 'Unknown error'}`
)
)
}
})
export default router
@@ -0,0 +1,128 @@
/**
* HTML Templates for OAuth2 Callback Pages
*
* This module contains reusable HTML templates for OAuth2 authorization responses.
* The templates provide consistent styling and behavior for success and error pages.
*/
export interface OAuth2PageOptions {
title: string
statusIcon: string
statusText: string
statusColor: string
message: string
details?: string
postMessageType: 'OAUTH2_SUCCESS' | 'OAUTH2_ERROR'
postMessageData: any
autoCloseDelay: number
}
export const generateOAuth2ResponsePage = (options: OAuth2PageOptions): string => {
const { title, statusIcon, statusText, statusColor, message, details, postMessageType, postMessageData, autoCloseDelay } = options
return `
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
text-align: center;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 500px;
}
.status {
color: ${statusColor};
font-size: 1.2rem;
margin-bottom: 1rem;
}
.message {
color: #666;
margin-bottom: 1rem;
}
.details {
background: #f9f9f9;
padding: 1rem;
border-radius: 4px;
font-size: 0.9rem;
color: #333;
text-align: left;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="status">${statusIcon} ${statusText}</div>
<div class="message">${message}</div>
${details ? `<div class="details">${details}</div>` : ''}
</div>
<script>
// Notify parent window
try {
if (window.opener) {
window.opener.postMessage(${JSON.stringify({
type: postMessageType,
...postMessageData
})}, '*');
}
} catch (error) {
console.log('Could not notify parent window:', error);
}
// Close window after delay
setTimeout(function() {
window.close();
}, ${autoCloseDelay});
</script>
</body>
</html>
`
}
export const generateSuccessPage = (credentialId: string): string => {
return generateOAuth2ResponsePage({
title: 'OAuth2 Authorization Success',
statusIcon: '✓',
statusText: 'Authorization Successful',
statusColor: '#4caf50',
message: 'You can close this window now.',
postMessageType: 'OAUTH2_SUCCESS',
postMessageData: {
credentialId,
success: true,
message: 'OAuth2 authorization completed successfully'
},
autoCloseDelay: 1000
})
}
export const generateErrorPage = (error: string, message: string, details?: string): string => {
return generateOAuth2ResponsePage({
title: 'OAuth2 Authorization Error',
statusIcon: '✗',
statusText: 'Authorization Failed',
statusColor: '#f44336',
message,
details,
postMessageType: 'OAUTH2_ERROR',
postMessageData: {
success: false,
message,
error
},
autoCloseDelay: 3000
})
}
@@ -23,6 +23,7 @@ import { Organization } from '../../enterprise/database/entities/organization.en
const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n'
const ARTIFACTS_PREFIX = '\n\n----FLOWISE_ARTIFACTS----\n\n'
const TOOL_ARGS_PREFIX = '\n\n----FLOWISE_TOOL_ARGS----\n\n'
const buildAndInitTool = async (chatflowid: string, _chatId?: string, _apiMessageId?: string) => {
const appServer = getRunningExpressApp()
@@ -211,6 +212,11 @@ const executeAgentTool = async (
}
}
if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) {
const _splitted = toolOutput.split(TOOL_ARGS_PREFIX)
toolOutput = _splitted[0]
}
return {
output: toolOutput,
sourceDocuments,
+2
View File
@@ -39,6 +39,8 @@ export const WHITELIST_URLS = [
'/api/v1/loginmethod',
'/api/v1/pricing',
'/api/v1/user/test',
'/api/v1/oauth2-credential/callback',
'/api/v1/oauth2-credential/refresh',
AzureSSO.LOGIN_URI,
AzureSSO.LOGOUT_URI,
AzureSSO.CALLBACK_URI,