Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 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
18 changes: 15 additions & 3 deletions packages-answers/ui/src/PermissionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,33 @@ const PermissionContext = React.createContext<PermissionContextValue | undefined

const mergeUsers = (initialUser?: Partial<User>, runtimeUser?: Partial<User> | null): Partial<User> => {
const merged: Record<string, unknown> = {}

// Start with initialUser (server-enriched data from /api/v1/auth/me)
if (initialUser) {
Object.entries(initialUser).forEach(([key, value]) => {
if (value !== undefined) merged[key] = value
})
}

// Merge runtimeUser but preserve critical auth fields from server
if (runtimeUser) {
// Fields that MUST come from server enrichment, never from client
// These are set by the backend based on Auth0 roles and should not be overwritten
const preservedFields = ['roles', 'permissions', 'features', 'org_id', 'organizationId']

Object.entries(runtimeUser).forEach(([key, value]) => {
if (value === undefined) return
if (key === 'roles' && Array.isArray(merged[key]) && Array.isArray(value)) {
merged[key] = Array.from(new Set([...(merged[key] as unknown[]), ...value]))
return

// Don't overwrite server-enriched auth fields
if (preservedFields.includes(key) && merged[key] !== undefined) {
return // Keep server value
}

// Allow runtime updates for non-auth fields (name, email, picture, etc.)
merged[key] = value
})
}

return merged as Partial<User>
}

Expand Down
10 changes: 10 additions & 0 deletions packages-answers/utils/src/auth/enrichSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,18 @@ export async function enrichSessionWithFlowise(session: any, options: EnrichSess
const enrichedUser = data.user || data

if (enrichedUser) {
// Save Auth0 roles before merge — the ID token carries https://theanswer.ai/roles
// but the access token (used by Flowise) often does not, so Flowise returns roles: []
const auth0Roles = session.user?.['https://theanswer.ai/roles']

// Merge Auth0 user with Flowise enriched data (Flowise takes priority)
session.user = { ...session.user, ...enrichedUser }

// Restore Auth0 roles if Flowise couldn't extract them from the access token
if (auth0Roles?.length && (!session.user.roles || session.user.roles.length === 0)) {
session.user.roles = Array.isArray(auth0Roles) ? auth0Roles : [auth0Roles]
}

console.debug('[AAI enrichSession] User enriched successfully with fields:', Object.keys(enrichedUser).join(', '))
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class ChatPromptTemplate_Prompts implements INode {
libraries: ['axios', '@langchain/core']
})

const parsedResponse = JSON.parse(response)
const parsedResponse = typeof response === 'string' ? JSON.parse(response) : response

if (!Array.isArray(parsedResponse)) {
throw new Error('Returned message history must be an array')
Expand Down
9 changes: 5 additions & 4 deletions packages/components/nodes/tools/MCP/Jira/JiraMCP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['JiraApi']
credentialNames: ['jiraApi']
}
this.inputs = [
{
Expand Down Expand Up @@ -101,13 +101,14 @@
}

async getTools(nodeData: INodeData, options: ICommonObject): Promise<Tool[]> {

Check failure on line 104 in packages/components/nodes/tools/MCP/Jira/JiraMCP.ts

View workflow job for this annotation

GitHub Actions / build

Delete `⏎········`
const credentialData = await getCredentialData(nodeData.credential ?? '', options)

Check failure on line 106 in packages/components/nodes/tools/MCP/Jira/JiraMCP.ts

View workflow job for this annotation

GitHub Actions / build

Delete `·······`
const jiraApiKey = getCredentialParam('accessToken', credentialData, nodeData)
const jiraApiEmail = getCredentialParam('username', credentialData, nodeData)
const jiraUrl = getCredentialParam('host', credentialData, nodeData)

const packagePath = getNodeModulesPackagePath('@answerai/jira-mcp/build/index.js')

Check failure on line 111 in packages/components/nodes/tools/MCP/Jira/JiraMCP.ts

View workflow job for this annotation

GitHub Actions / build

Delete `······`
const serverParams = {
command: process.execPath,
args: [packagePath],
Expand All @@ -122,7 +123,7 @@
await toolkit.initialize()

const tools = toolkit.tools ?? []

Check failure on line 126 in packages/components/nodes/tools/MCP/Jira/JiraMCP.ts

View workflow job for this annotation

GitHub Actions / build

Delete `······`
return tools
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@qdrant/js-client-rest": "^1.9.0",
"@stripe/agent-toolkit": "^0.1.20",
"@supabase/supabase-js": "^2.29.0",
"@tsmztech/mcp-server-salesforce": "^0.0.2",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.1.1",
"@upstash/redis": "1.22.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/database/entities/ChatFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export enum ChatflowVisibility {
PUBLIC = 'Public',
ORGANIZATION = 'Organization',
ANSWERAI = 'AnswerAI',
MARKETPLACE = 'Marketplace'
MARKETPLACE = 'Marketplace',
BROWSER_EXTENSION = 'Browser Extension'
}

export enum EnumChatflowType {
Expand Down
44 changes: 33 additions & 11 deletions packages/server/src/services/browser-extension/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { StatusCodes } from 'http-status-codes'
import { ChatFlow } from '../../database/entities/ChatFlow'
import { ChatFlow, ChatflowVisibility } from '../../database/entities/ChatFlow'
import { User } from '../../database/entities/User'
import { IUser } from '../../Interface'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import checkOwnership from '../../utils/checkOwnership'

// Browser Extension visibility constant
const BROWSER_EXTENSION = 'Browser Extension'

/**
* Get all public chatflows that are available for the browser extension
* @param user - The authenticated user object
Expand All @@ -28,8 +25,22 @@ const getBrowserExtensionChatflows = async (user: IUser): Promise<ChatFlow[]> =>
}

// Query chatflows that:
// 1. Belong to the user or their organization
const queryBuilder = chatFlowRepository.createQueryBuilder('chatflow').where('chatflow.userId = :userId', { userId: user.id })
// 1. Belong to the user, scoped to their active workspace, with Browser Extension visibility
if (!user.activeWorkspaceId) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
'Error: browserExtensionService.getBrowserExtensionChatflows - activeWorkspaceId is required'
)
}

const queryBuilder = chatFlowRepository
.createQueryBuilder('chatflow')
.where('chatflow.workspaceId = :workspaceId', { workspaceId: user.activeWorkspaceId })
.andWhere('chatflow.visibility LIKE :ext', { ext: '%Browser Extension%' })
.andWhere('(chatflow.userId = :userId OR chatflow.visibility LIKE :org)', {
userId: user.id,
org: '%Organization%'
})

// Return the complete chatflow objects with all fields
const dbResponse = await queryBuilder.getMany()
Expand All @@ -44,6 +55,7 @@ const getBrowserExtensionChatflows = async (user: IUser): Promise<ChatFlow[]> =>
// Return the chatflows with the default flag
return chatflowsWithDefaultFlag
} catch (error) {
if (error instanceof InternalFlowiseError) throw error
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: browserExtensionService.getBrowserExtensionChatflows - ${getErrorMessage(error)}`
Expand All @@ -63,8 +75,17 @@ const updateBrowserExtensionVisibility = async (chatflowId: string, enabled: boo
const appServer = getRunningExpressApp()
const chatFlowRepository = appServer.AppDataSource.getRepository(ChatFlow)

// Find the chatflow
const chatflow = await chatFlowRepository.findOneBy({ id: chatflowId })
if (!user.activeWorkspaceId) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
'Error: browserExtensionService.updateBrowserExtensionVisibility - activeWorkspaceId is required'
)
}

const chatflow = await chatFlowRepository.findOneBy({
id: chatflowId,
workspaceId: user.activeWorkspaceId
})

if (!chatflow) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Chatflow with ID ${chatflowId} not found`)
Expand All @@ -83,11 +104,11 @@ const updateBrowserExtensionVisibility = async (chatflowId: string, enabled: boo
: []

// Add or remove "Browser Extension" from visibility
if (enabled && !visibilityArray.includes(BROWSER_EXTENSION as any)) {
visibilityArray.push(BROWSER_EXTENSION as any)
if (enabled && !visibilityArray.includes(ChatflowVisibility.BROWSER_EXTENSION)) {
visibilityArray.push(ChatflowVisibility.BROWSER_EXTENSION)
} else if (!enabled) {
// Filter out Browser Extension if it exists
const index = visibilityArray.findIndex((v) => String(v) === BROWSER_EXTENSION)
const index = visibilityArray.findIndex((v) => v === ChatflowVisibility.BROWSER_EXTENSION)
if (index >= 0) {
visibilityArray.splice(index, 1)
}
Expand All @@ -99,6 +120,7 @@ const updateBrowserExtensionVisibility = async (chatflowId: string, enabled: boo

return updatedChatflow
} catch (error) {
if (error instanceof InternalFlowiseError) throw error
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: browserExtensionService.updateBrowserExtensionVisibility - ${getErrorMessage(error)}`
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@

logger.debug(`[server]: [${orgId}]: Initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`)
const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${question}` : question

Check failure on line 668 in packages/server/src/utils/index.ts

View workflow job for this annotation

GitHub Actions / build

Delete `················`
let outputResult = await newNodeInstance.init(reactFlowNodeData, finalQuestion, {
orgId,
workspaceId,
Expand Down
30 changes: 2 additions & 28 deletions packages/ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react'
import { useSelector } from 'react-redux'

import { ThemeProvider } from '@mui/material/styles'
import { Button, CssBaseline, StyledEngineProvider } from '@mui/material'
import { CssBaseline, StyledEngineProvider } from '@mui/material'

// routing
import Routes from '@/routes'
Expand All @@ -13,39 +13,19 @@ import themes from '@/themes'

// project imports
import NavigationScroll from '@/layout/NavigationScroll'
import { useAuth0 } from '@auth0/auth0-react'
import useNotifyParentOfNavigation from './utils/useNotifyParentOfNavigation'

// ==============================|| APP ||============================== //

const App = () => {
const customization = useSelector((state) => state.customization)
const { user, isLoading, getAccessTokenSilently, error, signinWithRedirect } = useAuth0()
const user = useSelector((state) => state.auth.user)
useNotifyParentOfNavigation()
React.useEffect(() => {
if (user?.chatflowDomain) {
sessionStorage.setItem('baseURL', user.chatflowDomain.replace('8080', '4000'))
}
}, [user?.chatflowDomain])
React.useEffect(() => {
;(async () => {
try {
// console.log('user', { user, isLoading })
if (!user && !isLoading) {
await signinWithRedirect()
} else if (user) {
const newToken = await getAccessTokenSilently({
authorizationParams: {
// scope: 'write:admin'
}
})
sessionStorage.setItem('access_token', newToken)
}
} catch (err) {
console.log(err)
}
})()
}, [user, isLoading, getAccessTokenSilently, signinWithRedirect])

return (
<StyledEngineProvider injectFirst>
Expand All @@ -54,12 +34,6 @@ const App = () => {
<NavigationScroll>
<Routes />
</NavigationScroll>
{error && (
<>
<h1>{error.message}</h1>
<Button onClick={() => loginWithRedirect()}>Try Again</Button>
</>
)}
</ThemeProvider>
</StyledEngineProvider>
)
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/hooks/usePermissions.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useAuth0 } from '@auth0/auth0-react'
import { useSelector } from 'react-redux'
import { useMemo } from 'react'
import { createPermissionManager } from 'utils/src/auth/permissions'

export const usePermissions = () => {
const { user } = useAuth0()
const user = useSelector((state) => state.auth.user)
return useMemo(() => createPermissionManager(user ?? {}), [user])
}

Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/ui-component/extended/VisibilitySettings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const VisibilitySettings = ({ dialogProps }) => {

const dispatch = useDispatch()
const chatflow = useSelector((state) => state?.canvas?.chatflow) || dialogProps.chatflow
const user = useSelector((state) => state.auth?.user)
const isOwner = user?.id && chatflow?.userId && user.id === chatflow.userId

useNotifier()

Expand Down Expand Up @@ -111,8 +113,8 @@ const VisibilitySettings = ({ dialogProps }) => {
{visibilityOptions.map(({ name, description }) => {
const isDisabled =
name === 'Private' ||
(name === 'Browser Extension' && !canManageOrg) ||
(name === 'Organization' && !canManageOrg) ||
(name === 'Browser Extension' && !canManageOrg && !isOwner) ||
(name === 'Organization' && !canManageOrg && !isOwner) ||
(name === 'Marketplace' && !canShareInternally)
return (
<Box key={name} display='flex' alignItems='center'>
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/views/chatmessage/ChatMessage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1362,7 +1362,8 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP
setFormDescription(startNode.data.inputs?.formDescription)
}

getAllExecutionsApi.request({ agentflowId: chatflowid })
const storedSessionId = getLocalStorageChatflow(chatflowid)?.chatId
getAllExecutionsApi.request({ agentflowId: chatflowid, ...(storedSessionId && { sessionId: storedSessionId }) })
}
}

Expand Down Expand Up @@ -1468,8 +1469,10 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP

useEffect(() => {
if (open && chatflowid) {
// API request
getChatmessageApi.request(chatflowid)
const storedChatId = getLocalStorageChatflow(chatflowid)?.chatId
if (storedChatId) {
getChatmessageApi.request(chatflowid, { chatId: storedChatId })
}
getIsChatflowStreamingApi.request(chatflowid)
getAllowChatFlowUploads.request(chatflowid)
getChatflowConfig.request(chatflowid)
Expand Down
19 changes: 18 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading