diff --git a/docker/.env.example b/docker/.env.example index b6cc050b0..dab25248a 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -99,6 +99,7 @@ JWT_TOKEN_EXPIRY_IN_MINUTES=360 JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # EXPIRE_AUTH_TOKENS_ON_RESTART=true # (if you need to expire all tokens on app restart) # EXPRESS_SESSION_SECRET=flowise +# SECURE_COOKIES= # INVITE_TOKEN_EXPIRY_IN_HOURS=24 # PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=15 diff --git a/docker/docker-compose-queue-prebuilt.yml b/docker/docker-compose-queue-prebuilt.yml index 51a18e96d..0063eeb1f 100644 --- a/docker/docker-compose-queue-prebuilt.yml +++ b/docker/docker-compose-queue-prebuilt.yml @@ -89,6 +89,7 @@ services: - PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=${PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS} - PASSWORD_SALT_HASH_ROUNDS=${PASSWORD_SALT_HASH_ROUNDS} - TOKEN_HASH_SECRET=${TOKEN_HASH_SECRET} + - SECURE_COOKIES=${SECURE_COOKIES} # EMAIL - SMTP_HOST=${SMTP_HOST} @@ -232,6 +233,7 @@ services: - PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=${PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS} - PASSWORD_SALT_HASH_ROUNDS=${PASSWORD_SALT_HASH_ROUNDS} - TOKEN_HASH_SECRET=${TOKEN_HASH_SECRET} + - SECURE_COOKIES=${SECURE_COOKIES} # EMAIL - SMTP_HOST=${SMTP_HOST} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 54bcac359..f66d7106d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -74,6 +74,7 @@ services: - PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=${PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS} - PASSWORD_SALT_HASH_ROUNDS=${PASSWORD_SALT_HASH_ROUNDS} - TOKEN_HASH_SECRET=${TOKEN_HASH_SECRET} + - SECURE_COOKIES=${SECURE_COOKIES} # EMAIL - SMTP_HOST=${SMTP_HOST} diff --git a/docker/worker/.env.example b/docker/worker/.env.example index 6c2ce8c52..769286dff 100644 --- a/docker/worker/.env.example +++ b/docker/worker/.env.example @@ -99,6 +99,7 @@ JWT_TOKEN_EXPIRY_IN_MINUTES=360 JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # EXPIRE_AUTH_TOKENS_ON_RESTART=true # (if you need to expire all tokens on app restart) # EXPRESS_SESSION_SECRET=flowise +# SECURE_COOKIES= # INVITE_TOKEN_EXPIRY_IN_HOURS=24 # PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=15 diff --git a/docker/worker/docker-compose.yml b/docker/worker/docker-compose.yml index 71de912a7..952dc04cd 100644 --- a/docker/worker/docker-compose.yml +++ b/docker/worker/docker-compose.yml @@ -74,6 +74,7 @@ services: - PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=${PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS} - PASSWORD_SALT_HASH_ROUNDS=${PASSWORD_SALT_HASH_ROUNDS} - TOKEN_HASH_SECRET=${TOKEN_HASH_SECRET} + - SECURE_COOKIES=${SECURE_COOKIES} # EMAIL - SMTP_HOST=${SMTP_HOST} diff --git a/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts index 38a0a6291..7498a1957 100644 --- a/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts +++ b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts @@ -272,6 +272,22 @@ class GoogleCalendar_Tools implements INode { additionalParams: true, optional: true }, + { + label: 'Send Updates to', + name: 'sendUpdates', + type: 'options', + description: 'Send Updates to attendees', + options: [ + { label: 'All', name: 'all' }, + { label: 'External Only', name: 'externalOnly' }, + { label: 'None', name: 'none' } + ], + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, { label: 'Recurrence Rules', name: 'recurrence', @@ -560,7 +576,6 @@ class GoogleCalendar_Tools implements INode { } const defaultParams = this.transformNodeInputsToToolArgs(nodeData) - const tools = createGoogleCalendarTools({ accessToken, actions, @@ -587,6 +602,7 @@ class GoogleCalendar_Tools implements INode { if (nodeData.inputs?.startDate) defaultParams.startDate = nodeData.inputs.startDate if (nodeData.inputs?.endDate) defaultParams.endDate = nodeData.inputs.endDate if (nodeData.inputs?.attendees) defaultParams.attendees = nodeData.inputs.attendees + if (nodeData.inputs?.sendUpdates) defaultParams.sendUpdates = nodeData.inputs.sendUpdates if (nodeData.inputs?.recurrence) defaultParams.recurrence = nodeData.inputs.recurrence if (nodeData.inputs?.reminderMinutes) defaultParams.reminderMinutes = nodeData.inputs.reminderMinutes if (nodeData.inputs?.visibility) defaultParams.visibility = nodeData.inputs.visibility diff --git a/packages/components/nodes/tools/GoogleCalendar/core.ts b/packages/components/nodes/tools/GoogleCalendar/core.ts index b613d0d1f..00f21d85d 100644 --- a/packages/components/nodes/tools/GoogleCalendar/core.ts +++ b/packages/components/nodes/tools/GoogleCalendar/core.ts @@ -48,6 +48,7 @@ const CreateEventSchema = z.object({ endDate: z.string().optional().describe('End date for all-day events (YYYY-MM-DD)'), timeZone: z.string().optional().describe('Time zone (e.g., America/New_York)'), attendees: z.string().optional().describe('Comma-separated list of attendee emails'), + sendUpdates: z.enum(['all', 'externalOnly', 'none']).optional().default('all').describe('Whether to send notifications to attendees'), recurrence: z.string().optional().describe('Recurrence rules (RRULE format)'), reminderMinutes: z.number().optional().describe('Minutes before event to send reminder'), visibility: z.enum(['default', 'public', 'private', 'confidential']).optional().describe('Event visibility') @@ -70,6 +71,7 @@ const UpdateEventSchema = z.object({ endDate: z.string().optional().describe('Updated end date for all-day events (YYYY-MM-DD)'), timeZone: z.string().optional().describe('Updated time zone'), attendees: z.string().optional().describe('Updated comma-separated list of attendee emails'), + sendUpdates: z.enum(['all', 'externalOnly', 'none']).optional().default('all').describe('Whether to send notifications to attendees'), recurrence: z.string().optional().describe('Updated recurrence rules'), reminderMinutes: z.number().optional().describe('Updated reminder minutes'), visibility: z.enum(['default', 'public', 'private', 'confidential']).optional().describe('Updated event visibility') @@ -286,8 +288,11 @@ class CreateEventTool extends BaseGoogleCalendarTool { } if (params.visibility) eventData.visibility = params.visibility + const queryParams = new URLSearchParams() + if (params.sendUpdates) queryParams.append('sendUpdates', params.sendUpdates) + + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events?${queryParams.toString()}` - const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events` const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: eventData, params }) return response } catch (error) { @@ -395,8 +400,12 @@ class UpdateEventTool extends BaseGoogleCalendarTool { } if (params.visibility) updateData.visibility = params.visibility + const queryParams = new URLSearchParams() + if (params.sendUpdates) queryParams.append('sendUpdates', params.sendUpdates) - const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}` + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent( + params.eventId + )}?${queryParams.toString()}` const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'PUT', body: updateData, params }) return response } catch (error) { diff --git a/packages/components/nodes/tools/OpenAPIToolkit/OpenAPIToolkit.ts b/packages/components/nodes/tools/OpenAPIToolkit/OpenAPIToolkit.ts index d4c664638..5f0a8bc20 100644 --- a/packages/components/nodes/tools/OpenAPIToolkit/OpenAPIToolkit.ts +++ b/packages/components/nodes/tools/OpenAPIToolkit/OpenAPIToolkit.ts @@ -5,6 +5,7 @@ import $RefParser from '@apidevtools/json-schema-ref-parser' import { z, ZodSchema, ZodTypeAny } from 'zod' import { defaultCode, DynamicStructuredTool, howToUseCode } from './core' import { DataSource } from 'typeorm' +import fetch from 'node-fetch' class OpenAPIToolkit_Tools implements INode { label: string @@ -21,17 +22,64 @@ class OpenAPIToolkit_Tools implements INode { constructor() { this.label = 'OpenAPI Toolkit' this.name = 'openAPIToolkit' - this.version = 2.0 + this.version = 2.1 this.type = 'OpenAPIToolkit' this.icon = 'openapi.svg' this.category = 'Tools' this.description = 'Load OpenAPI specification, and converts each API endpoint to a tool' this.inputs = [ { - label: 'YAML File', - name: 'yamlFile', + label: 'Input Type', + name: 'inputType', + type: 'options', + options: [ + { + label: 'Upload File', + name: 'file' + }, + { + label: 'Provide Link', + name: 'link' + } + ], + default: 'file', + description: 'Choose how to provide the OpenAPI specification' + }, + { + label: 'OpenAPI File', + name: 'openApiFile', type: 'file', - fileType: '.yaml' + fileType: '.yaml,.json', + description: 'Upload your OpenAPI specification file (YAML or JSON)', + show: { + inputType: 'file' + } + }, + { + label: 'OpenAPI Link', + name: 'openApiLink', + type: 'string', + placeholder: 'https://api.example.com/openapi.yaml or https://api.example.com/openapi.json', + description: 'Provide a link to your OpenAPI specification (YAML or JSON)', + show: { + inputType: 'link' + } + }, + { + label: 'Server', + name: 'selectedServer', + type: 'asyncOptions', + loadMethod: 'listServers', + description: 'Select which server to use for API calls', + refresh: true + }, + { + label: 'Available Endpoints', + name: 'selectedEndpoints', + type: 'asyncMultiOptions', + loadMethod: 'listEndpoints', + description: 'Select which endpoints to expose as tools', + refresh: true }, { label: 'Return Direct', @@ -46,8 +94,7 @@ class OpenAPIToolkit_Tools implements INode { type: 'json', description: 'Request headers to be sent with the API request. For example, {"Authorization": "Bearer token"}', additionalParams: true, - optional: true, - acceptVariable: true + optional: true }, { label: 'Remove null parameters', @@ -76,49 +123,237 @@ class OpenAPIToolkit_Tools implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const toolReturnDirect = nodeData.inputs?.returnDirect as boolean - const yamlFileBase64 = nodeData.inputs?.yamlFile as string + const inputType = nodeData.inputs?.inputType as string + const openApiFile = nodeData.inputs?.openApiFile as string + const openApiLink = nodeData.inputs?.openApiLink as string + const selectedServer = nodeData.inputs?.selectedServer as string const customCode = nodeData.inputs?.customCode as string const _headers = nodeData.inputs?.headers as string const removeNulls = nodeData.inputs?.removeNulls as boolean const headers = typeof _headers === 'object' ? _headers : _headers ? JSON.parse(_headers) : {} - let data - if (yamlFileBase64.startsWith('FILE-STORAGE::')) { - const file = yamlFileBase64.replace('FILE-STORAGE::', '') - const orgId = options.orgId - const chatflowid = options.chatflowid - const fileData = await getFileFromStorage(file, orgId, chatflowid) - const utf8String = fileData.toString('utf-8') + const specData = await this.loadOpenApiSpec( + { + inputType, + openApiFile, + openApiLink + }, + options + ) + if (!specData) throw new Error('Failed to load OpenAPI spec') - data = load(utf8String) + const _data: any = await $RefParser.dereference(specData) + + // Use selected server or fallback to first server + let baseUrl: string + if (selectedServer && selectedServer !== 'error') { + baseUrl = selectedServer } else { - const splitDataURI = yamlFileBase64.split(',') - splitDataURI.pop() - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - const utf8String = bf.toString('utf-8') - data = load(utf8String) - } - if (!data) { - throw new Error('Failed to load OpenAPI spec') + baseUrl = _data.servers?.[0]?.url } - const _data: any = await $RefParser.dereference(data) - - const baseUrl = _data.servers[0]?.url - if (!baseUrl) { - throw new Error('OpenAPI spec does not contain a server URL') - } + if (!baseUrl) throw new Error('OpenAPI spec does not contain a server URL') const appDataSource = options.appDataSource as DataSource const databaseEntities = options.databaseEntities as IDatabaseEntity const variables = await getVars(appDataSource, databaseEntities, nodeData, options) - const flow = { chatflowId: options.chatflowid } - const tools = getTools(_data.paths, baseUrl, headers, variables, flow, toolReturnDirect, customCode, removeNulls) + let tools = getTools(_data.paths, baseUrl, headers, variables, flow, toolReturnDirect, customCode, removeNulls) + + // Filter by selected endpoints if provided + const _selected = nodeData.inputs?.selectedEndpoints + let selected: string[] = [] + if (_selected) { + try { + selected = typeof _selected === 'string' ? JSON.parse(_selected) : _selected + } catch (e) { + selected = [] + } + } + if (selected.length) { + tools = tools.filter((t: any) => selected.includes(t.name)) + } + return tools } + + //@ts-ignore + loadMethods = { + listServers: async (nodeData: INodeData, options: ICommonObject) => { + try { + const inputType = nodeData.inputs?.inputType as string + const openApiFile = nodeData.inputs?.openApiFile as string + const openApiLink = nodeData.inputs?.openApiLink as string + const specData: any = await this.loadOpenApiSpec( + { + inputType, + openApiFile, + openApiLink + }, + options + ) + if (!specData) return [] + const _data: any = await $RefParser.dereference(specData) + const items: { label: string; name: string; description?: string }[] = [] + const servers = _data.servers || [] + + if (servers.length === 0) { + return [ + { + label: 'No Servers Found', + name: 'error', + description: 'No servers defined in the OpenAPI specification' + } + ] + } + + for (let i = 0; i < servers.length; i++) { + const server = servers[i] + const serverUrl = server.url || `Server ${i + 1}` + const serverDesc = server.description || serverUrl + items.push({ + label: serverUrl, + name: serverUrl, + description: serverDesc + }) + } + + return items + } catch (e) { + return [ + { + label: 'No Servers Found', + name: 'error', + description: 'No available servers, check the link/file and refresh' + } + ] + } + }, + listEndpoints: async (nodeData: INodeData, options: ICommonObject) => { + try { + const inputType = nodeData.inputs?.inputType as string + const openApiFile = nodeData.inputs?.openApiFile as string + const openApiLink = nodeData.inputs?.openApiLink as string + const specData: any = await this.loadOpenApiSpec( + { + inputType, + openApiFile, + openApiLink + }, + options + ) + if (!specData) return [] + const _data: any = await $RefParser.dereference(specData) + const items: { label: string; name: string; description?: string }[] = [] + const paths = _data.paths || {} + for (const path in paths) { + const methods = paths[path] + for (const method in methods) { + if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) { + const spec = methods[method] + const opId = spec.operationId || `${method.toUpperCase()} ${path}` + const desc = spec.description || spec.summary || opId + items.push({ label: opId, name: opId, description: desc }) + } + } + } + items.sort((a, b) => a.label.localeCompare(b.label)) + return items + } catch (e) { + return [ + { + label: 'No Endpoints Found', + name: 'error', + description: 'No available endpoints, check the link/file and refresh' + } + ] + } + } + } + + private async loadOpenApiSpec( + args: { + inputType?: string + openApiFile?: string + openApiLink?: string + }, + options: ICommonObject + ): Promise { + const { inputType = 'file', openApiFile = '', openApiLink = '' } = args + try { + if (inputType === 'link' && openApiLink) { + const res = await fetch(openApiLink) + const text = await res.text() + + // Auto-detect format from URL extension or content + const isJsonUrl = openApiLink.toLowerCase().includes('.json') + const isYamlUrl = openApiLink.toLowerCase().includes('.yaml') || openApiLink.toLowerCase().includes('.yml') + + if (isJsonUrl) { + return JSON.parse(text) + } else if (isYamlUrl) { + return load(text) + } else { + // Auto-detect format from content + try { + return JSON.parse(text) + } catch (_) { + return load(text) + } + } + } + + if (inputType === 'file' && openApiFile) { + let utf8String: string + let fileName = '' + + if (openApiFile.startsWith('FILE-STORAGE::')) { + const file = openApiFile.replace('FILE-STORAGE::', '') + fileName = file + const orgId = options.orgId + const chatflowid = options.chatflowid + const fileData = await getFileFromStorage(file, orgId, chatflowid) + utf8String = fileData.toString('utf-8') + } else { + // Extract filename from data URI if possible + const splitDataURI = openApiFile.split(',') + const mimeType = splitDataURI[0] || '' + if (mimeType.includes('filename=')) { + const filenameMatch = mimeType.match(/filename=([^;]+)/) + if (filenameMatch) { + fileName = filenameMatch[1] + } + } + splitDataURI.pop() + const bf = Buffer.from(splitDataURI.pop() || '', 'base64') + utf8String = bf.toString('utf-8') + } + + // Auto-detect format from file extension or content + const isJsonFile = fileName.toLowerCase().endsWith('.json') + const isYamlFile = fileName.toLowerCase().endsWith('.yaml') || fileName.toLowerCase().endsWith('.yml') + + if (isJsonFile) { + return JSON.parse(utf8String) + } else if (isYamlFile) { + return load(utf8String) + } else { + // Auto-detect format from content + try { + return JSON.parse(utf8String) + } catch (_) { + return load(utf8String) + } + } + } + } catch (e) { + console.error('Error loading OpenAPI spec:', e) + return null + } + return null + } } const jsonSchemaToZodSchema = (schema: any, requiredList: string[], keyName: string): ZodSchema => { diff --git a/packages/server/.env.example b/packages/server/.env.example index f8ba1c485..219a17d1d 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -99,6 +99,7 @@ JWT_TOKEN_EXPIRY_IN_MINUTES=360 JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # EXPIRE_AUTH_TOKENS_ON_RESTART=true # (if you need to expire all tokens on app restart) # EXPRESS_SESSION_SECRET=flowise +# SECURE_COOKIES= # INVITE_TOKEN_EXPIRY_IN_HOURS=24 # PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=15 diff --git a/packages/server/src/enterprise/controllers/account.controller.ts b/packages/server/src/enterprise/controllers/account.controller.ts index f29af60fc..9c360a6a9 100644 --- a/packages/server/src/enterprise/controllers/account.controller.ts +++ b/packages/server/src/enterprise/controllers/account.controller.ts @@ -2,7 +2,6 @@ import { Request, Response, NextFunction } from 'express' import { StatusCodes } from 'http-status-codes' import { AccountService } from '../services/account.service' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' -import axios from 'axios' export class AccountController { public async register(req: Request, res: Response, next: NextFunction) { @@ -84,30 +83,6 @@ export class AccountController { } } - public async cancelPreviousCloudSubscrption(req: Request, res: Response, next: NextFunction) { - try { - const { email } = req.body - if (!email) { - return res.status(StatusCodes.BAD_REQUEST).json({ message: 'Email is required' }) - } - - const headers = { - 'Content-Type': 'application/json', - Accept: 'application/json' - } - - const response = await axios.post(`${process.env.ENGINE_URL}/cancel-subscription`, { email }, { headers }) - - if (response.status === 200) { - return res.status(StatusCodes.OK).json(response.data) - } else { - return res.status(response.status).json(response.data) - } - } catch (error) { - next(error) - } - } - public async logout(req: Request, res: Response, next: NextFunction) { try { if (req.user) { diff --git a/packages/server/src/enterprise/emails/verify_email_cloud.hbs b/packages/server/src/enterprise/emails/verify_email_cloud.hbs index 5d7bd2797..98b276e72 100644 --- a/packages/server/src/enterprise/emails/verify_email_cloud.hbs +++ b/packages/server/src/enterprise/emails/verify_email_cloud.hbs @@ -79,7 +79,7 @@ style=' position: relative; background-color: #151719 !important; - background-image: url(https://auth.flowiseai.com/storage/v1/object/public/flowise-static-assets/flowise_email_bg.svg); + background-image: url(https://general-flowise.s3.us-east-1.amazonaws.com/flowise_email_bg.svg); background-position: center top; background-size: contain; background-repeat: no-repeat; @@ -220,7 +220,7 @@