mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 21:00:58 +03:00
add API authorization
This commit is contained in:
@@ -9,6 +9,7 @@ export interface IChatFlow {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
flowData: string
|
flowData: string
|
||||||
|
apikeyid: string
|
||||||
deployed: boolean
|
deployed: boolean
|
||||||
updatedDate: Date
|
updatedDate: Date
|
||||||
createdDate: Date
|
createdDate: Date
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export class ChatFlow implements IChatFlow {
|
|||||||
@Column()
|
@Column()
|
||||||
flowData: string
|
flowData: string
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
apikeyid: string
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
deployed: boolean
|
deployed: boolean
|
||||||
|
|
||||||
|
|||||||
+146
-86
@@ -12,7 +12,12 @@ import {
|
|||||||
getEndingNode,
|
getEndingNode,
|
||||||
constructGraphs,
|
constructGraphs,
|
||||||
resolveVariables,
|
resolveVariables,
|
||||||
isStartNodeDependOnInput
|
isStartNodeDependOnInput,
|
||||||
|
getAPIKeys,
|
||||||
|
addAPIKey,
|
||||||
|
updateAPIKey,
|
||||||
|
deleteAPIKey,
|
||||||
|
compareKeys
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import { getDataSource } from './DataSource'
|
import { getDataSource } from './DataSource'
|
||||||
@@ -42,6 +47,9 @@ export class App {
|
|||||||
await this.nodesPool.initialize()
|
await this.nodesPool.initialize()
|
||||||
|
|
||||||
this.chatflowPool = new ChatflowPool()
|
this.chatflowPool = new ChatflowPool()
|
||||||
|
|
||||||
|
// Initialize API keys
|
||||||
|
await getAPIKeys()
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('❌[server]: Error during Data Source initialization:', err)
|
console.error('❌[server]: Error during Data Source initialization:', err)
|
||||||
@@ -195,93 +203,14 @@ export class App {
|
|||||||
// Prediction
|
// Prediction
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
// Send input message and get prediction result
|
// Send input message and get prediction result (External)
|
||||||
this.app.post('/api/v1/prediction/:id', async (req: Request, res: Response) => {
|
this.app.post('/api/v1/prediction/:id', async (req: Request, res: Response) => {
|
||||||
try {
|
await this.processPrediction(req, res)
|
||||||
const chatflowid = req.params.id
|
})
|
||||||
const incomingInput: IncomingInput = req.body
|
|
||||||
|
|
||||||
let nodeToExecuteData: INodeData
|
// Send input message and get prediction result (Internal)
|
||||||
|
this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => {
|
||||||
/* Check if:
|
await this.processPrediction(req, res, true)
|
||||||
* - Node Data already exists in pool
|
|
||||||
* - Still in sync (i.e the flow has not been modified since)
|
|
||||||
* - Flow doesn't start with nodes that depend on incomingInput.question
|
|
||||||
***/
|
|
||||||
if (
|
|
||||||
Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) &&
|
|
||||||
this.chatflowPool.activeChatflows[chatflowid].inSync &&
|
|
||||||
!isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes)
|
|
||||||
) {
|
|
||||||
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
|
|
||||||
} else {
|
|
||||||
/*** Get chatflows and prepare data ***/
|
|
||||||
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
|
|
||||||
id: chatflowid
|
|
||||||
})
|
|
||||||
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
|
|
||||||
|
|
||||||
const flowData = chatflow.flowData
|
|
||||||
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
|
||||||
const nodes = parsedFlowData.nodes
|
|
||||||
const edges = parsedFlowData.edges
|
|
||||||
|
|
||||||
/*** Get Ending Node with Directed Graph ***/
|
|
||||||
const { graph, nodeDependencies } = constructGraphs(nodes, edges)
|
|
||||||
const directedGraph = graph
|
|
||||||
const endingNodeId = getEndingNode(nodeDependencies, directedGraph)
|
|
||||||
if (!endingNodeId) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
|
||||||
|
|
||||||
const endingNodeData = nodes.find((nd) => nd.id === endingNodeId)?.data
|
|
||||||
if (!endingNodeData) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
|
||||||
|
|
||||||
if (
|
|
||||||
endingNodeData.outputs &&
|
|
||||||
Object.keys(endingNodeData.outputs).length &&
|
|
||||||
!Object.values(endingNodeData.outputs).includes(endingNodeData.name)
|
|
||||||
) {
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.send(
|
|
||||||
`Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Get Starting Nodes with Non-Directed Graph ***/
|
|
||||||
const constructedObj = constructGraphs(nodes, edges, true)
|
|
||||||
const nonDirectedGraph = constructedObj.graph
|
|
||||||
const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId)
|
|
||||||
|
|
||||||
/*** BFS to traverse from Starting Nodes to Ending Node ***/
|
|
||||||
const reactFlowNodes = await buildLangchain(
|
|
||||||
startingNodeIds,
|
|
||||||
nodes,
|
|
||||||
graph,
|
|
||||||
depthQueue,
|
|
||||||
this.nodesPool.componentNodes,
|
|
||||||
incomingInput.question
|
|
||||||
)
|
|
||||||
|
|
||||||
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
|
|
||||||
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
|
|
||||||
|
|
||||||
const reactFlowNodeData: INodeData = resolveVariables(nodeToExecute.data, reactFlowNodes, incomingInput.question)
|
|
||||||
nodeToExecuteData = reactFlowNodeData
|
|
||||||
|
|
||||||
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
|
|
||||||
this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string
|
|
||||||
const nodeModule = await import(nodeInstanceFilePath)
|
|
||||||
const nodeInstance = new nodeModule.nodeClass()
|
|
||||||
|
|
||||||
const result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history })
|
|
||||||
|
|
||||||
return res.json(result)
|
|
||||||
} catch (e: any) {
|
|
||||||
return res.status(500).send(e.message)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
@@ -308,6 +237,34 @@ export class App {
|
|||||||
return res.json(templates)
|
return res.json(templates)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// API Keys
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// Get api keys
|
||||||
|
this.app.get('/api/v1/apikey', async (req: Request, res: Response) => {
|
||||||
|
const keys = await getAPIKeys()
|
||||||
|
return res.json(keys)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add new api key
|
||||||
|
this.app.post('/api/v1/apikey', async (req: Request, res: Response) => {
|
||||||
|
const keys = await addAPIKey(req.body.keyName)
|
||||||
|
return res.json(keys)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update api key
|
||||||
|
this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => {
|
||||||
|
const keys = await updateAPIKey(req.params.id, req.body.keyName)
|
||||||
|
return res.json(keys)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete new api key
|
||||||
|
this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => {
|
||||||
|
const keys = await deleteAPIKey(req.params.id)
|
||||||
|
return res.json(keys)
|
||||||
|
})
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Serve UI static
|
// Serve UI static
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
@@ -324,6 +281,109 @@ export class App {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async processPrediction(req: Request, res: Response, isInternal = false) {
|
||||||
|
try {
|
||||||
|
const chatflowid = req.params.id
|
||||||
|
const incomingInput: IncomingInput = req.body
|
||||||
|
|
||||||
|
let nodeToExecuteData: INodeData
|
||||||
|
|
||||||
|
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
|
||||||
|
id: chatflowid
|
||||||
|
})
|
||||||
|
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
|
||||||
|
|
||||||
|
if (!isInternal) {
|
||||||
|
const chatFlowApiKeyId = chatflow.apikeyid
|
||||||
|
const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
|
||||||
|
|
||||||
|
if (chatFlowApiKeyId && !authorizationHeader) return res.status(401).send(`Unauthorized`)
|
||||||
|
|
||||||
|
const suppliedKey = authorizationHeader.split(`Bearer `).pop()
|
||||||
|
if (chatFlowApiKeyId && suppliedKey) {
|
||||||
|
const keys = await getAPIKeys()
|
||||||
|
const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret
|
||||||
|
if (!compareKeys(apiSecret, suppliedKey)) return res.status(401).send(`Unauthorized`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if:
|
||||||
|
* - Node Data already exists in pool
|
||||||
|
* - Still in sync (i.e the flow has not been modified since)
|
||||||
|
* - Flow doesn't start with nodes that depend on incomingInput.question
|
||||||
|
***/
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) &&
|
||||||
|
this.chatflowPool.activeChatflows[chatflowid].inSync &&
|
||||||
|
!isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes)
|
||||||
|
) {
|
||||||
|
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
|
||||||
|
} else {
|
||||||
|
/*** Get chatflows and prepare data ***/
|
||||||
|
|
||||||
|
const flowData = chatflow.flowData
|
||||||
|
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
||||||
|
const nodes = parsedFlowData.nodes
|
||||||
|
const edges = parsedFlowData.edges
|
||||||
|
|
||||||
|
/*** Get Ending Node with Directed Graph ***/
|
||||||
|
const { graph, nodeDependencies } = constructGraphs(nodes, edges)
|
||||||
|
const directedGraph = graph
|
||||||
|
const endingNodeId = getEndingNode(nodeDependencies, directedGraph)
|
||||||
|
if (!endingNodeId) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
||||||
|
|
||||||
|
const endingNodeData = nodes.find((nd) => nd.id === endingNodeId)?.data
|
||||||
|
if (!endingNodeData) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
||||||
|
|
||||||
|
if (
|
||||||
|
endingNodeData.outputs &&
|
||||||
|
Object.keys(endingNodeData.outputs).length &&
|
||||||
|
!Object.values(endingNodeData.outputs).includes(endingNodeData.name)
|
||||||
|
) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.send(
|
||||||
|
`Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Get Starting Nodes with Non-Directed Graph ***/
|
||||||
|
const constructedObj = constructGraphs(nodes, edges, true)
|
||||||
|
const nonDirectedGraph = constructedObj.graph
|
||||||
|
const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId)
|
||||||
|
|
||||||
|
/*** BFS to traverse from Starting Nodes to Ending Node ***/
|
||||||
|
const reactFlowNodes = await buildLangchain(
|
||||||
|
startingNodeIds,
|
||||||
|
nodes,
|
||||||
|
graph,
|
||||||
|
depthQueue,
|
||||||
|
this.nodesPool.componentNodes,
|
||||||
|
incomingInput.question
|
||||||
|
)
|
||||||
|
|
||||||
|
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
|
||||||
|
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
|
||||||
|
|
||||||
|
const reactFlowNodeData: INodeData = resolveVariables(nodeToExecute.data, reactFlowNodes, incomingInput.question)
|
||||||
|
nodeToExecuteData = reactFlowNodeData
|
||||||
|
|
||||||
|
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
|
||||||
|
this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string
|
||||||
|
const nodeModule = await import(nodeInstanceFilePath)
|
||||||
|
const nodeInstance = new nodeModule.nodeClass()
|
||||||
|
|
||||||
|
const result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history })
|
||||||
|
|
||||||
|
return res.json(result)
|
||||||
|
} catch (e: any) {
|
||||||
|
return res.status(500).send(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async stopApp() {
|
async stopApp() {
|
||||||
try {
|
try {
|
||||||
const removePromises: any[] = []
|
const removePromises: any[] = []
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import moment from 'moment'
|
||||||
import {
|
import {
|
||||||
IComponentNodes,
|
IComponentNodes,
|
||||||
IDepthQueue,
|
IDepthQueue,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
} from '../Interface'
|
} from '../Interface'
|
||||||
import { cloneDeep, get } from 'lodash'
|
import { cloneDeep, get } from 'lodash'
|
||||||
import { ICommonObject, getInputVariables } from 'flowise-components'
|
import { ICommonObject, getInputVariables } from 'flowise-components'
|
||||||
|
import { scryptSync, randomBytes, timingSafeEqual } from 'crypto'
|
||||||
|
|
||||||
const QUESTION_VAR_PREFIX = 'question'
|
const QUESTION_VAR_PREFIX = 'question'
|
||||||
|
|
||||||
@@ -362,3 +364,119 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[]): boole
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the api key path
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const getAPIKeyPath = (): string => {
|
||||||
|
return path.join(__dirname, '..', '..', 'api.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the api key
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const generateAPIKey = (): string => {
|
||||||
|
const buffer = randomBytes(32)
|
||||||
|
return buffer.toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the secret key
|
||||||
|
* @param {string} apiKey
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const generateSecretHash = (apiKey: string): string => {
|
||||||
|
const salt = randomBytes(8).toString('hex')
|
||||||
|
const buffer = scryptSync(apiKey, salt, 64) as Buffer
|
||||||
|
return `${buffer.toString('hex')}.${salt}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify valid keys
|
||||||
|
* @param {string} storedKey
|
||||||
|
* @param {string} suppliedKey
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const compareKeys = (storedKey: string, suppliedKey: string): boolean => {
|
||||||
|
const [hashedPassword, salt] = storedKey.split('.')
|
||||||
|
const buffer = scryptSync(suppliedKey, salt, 64) as Buffer
|
||||||
|
return timingSafeEqual(Buffer.from(hashedPassword, 'hex'), buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API keys
|
||||||
|
* @returns {Promise<ICommonObject[]>}
|
||||||
|
*/
|
||||||
|
export const getAPIKeys = async (): Promise<ICommonObject[]> => {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(getAPIKeyPath(), 'utf8')
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch (error) {
|
||||||
|
const keyName = 'DefaultKey'
|
||||||
|
const apiKey = generateAPIKey()
|
||||||
|
const apiSecret = generateSecretHash(apiKey)
|
||||||
|
const content = [
|
||||||
|
{
|
||||||
|
keyName,
|
||||||
|
apiKey,
|
||||||
|
apiSecret,
|
||||||
|
createdAt: moment().format('DD-MMM-YY'),
|
||||||
|
id: randomBytes(16).toString('hex')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8')
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new API key
|
||||||
|
* @param {string} keyName
|
||||||
|
* @returns {Promise<ICommonObject[]>}
|
||||||
|
*/
|
||||||
|
export const addAPIKey = async (keyName: string): Promise<ICommonObject[]> => {
|
||||||
|
const existingAPIKeys = await getAPIKeys()
|
||||||
|
const apiKey = generateAPIKey()
|
||||||
|
const apiSecret = generateSecretHash(apiKey)
|
||||||
|
const content = [
|
||||||
|
...existingAPIKeys,
|
||||||
|
{
|
||||||
|
keyName,
|
||||||
|
apiKey,
|
||||||
|
apiSecret,
|
||||||
|
createdAt: moment().format('DD-MMM-YY'),
|
||||||
|
id: randomBytes(16).toString('hex')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8')
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update existing API key
|
||||||
|
* @param {string} keyIdToUpdate
|
||||||
|
* @param {string} newKeyName
|
||||||
|
* @returns {Promise<ICommonObject[]>}
|
||||||
|
*/
|
||||||
|
export const updateAPIKey = async (keyIdToUpdate: string, newKeyName: string): Promise<ICommonObject[]> => {
|
||||||
|
const existingAPIKeys = await getAPIKeys()
|
||||||
|
const keyIndex = existingAPIKeys.findIndex((key) => key.id === keyIdToUpdate)
|
||||||
|
if (keyIndex < 0) return []
|
||||||
|
existingAPIKeys[keyIndex].keyName = newKeyName
|
||||||
|
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(existingAPIKeys), 'utf8')
|
||||||
|
return existingAPIKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete API key
|
||||||
|
* @param {string} keyIdToDelete
|
||||||
|
* @returns {Promise<ICommonObject[]>}
|
||||||
|
*/
|
||||||
|
export const deleteAPIKey = async (keyIdToDelete: string): Promise<ICommonObject[]> => {
|
||||||
|
const existingAPIKeys = await getAPIKeys()
|
||||||
|
const result = existingAPIKeys.filter((key) => key.id !== keyIdToDelete)
|
||||||
|
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(result), 'utf8')
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import client from './client'
|
||||||
|
|
||||||
|
const getAllAPIKeys = () => client.get('/apikey')
|
||||||
|
|
||||||
|
const createNewAPI = (body) => client.post(`/apikey`, body)
|
||||||
|
|
||||||
|
const updateAPI = (id, body) => client.put(`/apikey/${id}`, body)
|
||||||
|
|
||||||
|
const deleteAPI = (id) => client.delete(`/apikey/${id}`)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAllAPIKeys,
|
||||||
|
createNewAPI,
|
||||||
|
updateAPI,
|
||||||
|
deleteAPI
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import client from './client'
|
import client from './client'
|
||||||
|
|
||||||
const sendMessageAndGetPrediction = (id, input) => client.post(`/prediction/${id}`, input)
|
const sendMessageAndGetPrediction = (id, input) => client.post(`/internal-prediction/${id}`, input)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
sendMessageAndGetPrediction
|
sendMessageAndGetPrediction
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// assets
|
// assets
|
||||||
import { IconHierarchy, IconBuildingStore } from '@tabler/icons'
|
import { IconHierarchy, IconBuildingStore, IconKey } from '@tabler/icons'
|
||||||
|
|
||||||
// constant
|
// constant
|
||||||
const icons = { IconHierarchy, IconBuildingStore }
|
const icons = { IconHierarchy, IconBuildingStore, IconKey }
|
||||||
|
|
||||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||||
|
|
||||||
@@ -26,6 +26,14 @@ const dashboard = {
|
|||||||
url: '/marketplaces',
|
url: '/marketplaces',
|
||||||
icon: icons.IconBuildingStore,
|
icon: icons.IconBuildingStore,
|
||||||
breadcrumbs: true
|
breadcrumbs: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'apikey',
|
||||||
|
title: 'API Keys',
|
||||||
|
type: 'item',
|
||||||
|
url: '/apikey',
|
||||||
|
icon: icons.IconKey,
|
||||||
|
breadcrumbs: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ const Chatflows = Loadable(lazy(() => import('views/chatflows')))
|
|||||||
// marketplaces routing
|
// marketplaces routing
|
||||||
const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
|
const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
|
||||||
|
|
||||||
|
// apikey routing
|
||||||
|
const APIKey = Loadable(lazy(() => import('views/apikey')))
|
||||||
|
|
||||||
// ==============================|| MAIN ROUTING ||============================== //
|
// ==============================|| MAIN ROUTING ||============================== //
|
||||||
|
|
||||||
const MainRoutes = {
|
const MainRoutes = {
|
||||||
@@ -27,6 +30,10 @@ const MainRoutes = {
|
|||||||
{
|
{
|
||||||
path: '/marketplaces',
|
path: '/marketplaces',
|
||||||
element: <Marketplaces />
|
element: <Marketplaces />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/apikey',
|
||||||
|
element: <APIKey />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { useState } from 'react'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import { Tabs, Tab, Dialog, DialogContent, DialogTitle, Box } from '@mui/material'
|
import { Tabs, Tab, Dialog, DialogContent, DialogTitle, Box } from '@mui/material'
|
||||||
import { CopyBlock, atomOneDark } from 'react-code-blocks'
|
import { CopyBlock, atomOneDark } from 'react-code-blocks'
|
||||||
|
|
||||||
|
// Project import
|
||||||
|
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||||
|
|
||||||
|
// Const
|
||||||
import { baseURL } from 'store/constant'
|
import { baseURL } from 'store/constant'
|
||||||
|
import { SET_CHATFLOW } from 'store/actions'
|
||||||
|
|
||||||
|
// Images
|
||||||
import pythonSVG from 'assets/images/python.svg'
|
import pythonSVG from 'assets/images/python.svg'
|
||||||
import javascriptSVG from 'assets/images/javascript.svg'
|
import javascriptSVG from 'assets/images/javascript.svg'
|
||||||
import cURLSVG from 'assets/images/cURL.svg'
|
import cURLSVG from 'assets/images/cURL.svg'
|
||||||
|
|
||||||
|
// API
|
||||||
|
import apiKeyApi from 'api/apikey'
|
||||||
|
import chatflowsApi from 'api/chatflows'
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
import useApi from 'hooks/useApi'
|
||||||
|
|
||||||
function TabPanel(props) {
|
function TabPanel(props) {
|
||||||
const { children, value, index, ...other } = props
|
const { children, value, index, ...other } = props
|
||||||
return (
|
return (
|
||||||
@@ -39,8 +56,36 @@ function a11yProps(index) {
|
|||||||
|
|
||||||
const APICodeDialog = ({ show, dialogProps, onCancel }) => {
|
const APICodeDialog = ({ show, dialogProps, onCancel }) => {
|
||||||
const portalElement = document.getElementById('portal')
|
const portalElement = document.getElementById('portal')
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const dispatch = useDispatch()
|
||||||
const codes = ['Python', 'JavaScript', 'cURL']
|
const codes = ['Python', 'JavaScript', 'cURL']
|
||||||
const [value, setValue] = useState(0)
|
const [value, setValue] = useState(0)
|
||||||
|
const [keyOptions, setKeyOptions] = useState([])
|
||||||
|
const [apiKeys, setAPIKeys] = useState([])
|
||||||
|
const [chatflowApiKeyId, setChatflowApiKeyId] = useState('')
|
||||||
|
const [selectedApiKey, setSelectedApiKey] = useState({})
|
||||||
|
|
||||||
|
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
|
||||||
|
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
|
||||||
|
|
||||||
|
const onApiKeySelected = (keyValue) => {
|
||||||
|
if (keyValue === 'addnewkey') {
|
||||||
|
navigate('/apikey')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setChatflowApiKeyId(keyValue)
|
||||||
|
setSelectedApiKey(apiKeys.find((key) => key.id === keyValue))
|
||||||
|
const updateBody = {
|
||||||
|
apikeyid: keyValue
|
||||||
|
}
|
||||||
|
updateChatflowApi.request(dialogProps.chatflowid, updateBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updateChatflowApi.data) {
|
||||||
|
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
|
||||||
|
}
|
||||||
|
}, [updateChatflowApi.data, dispatch])
|
||||||
|
|
||||||
const handleChange = (event, newValue) => {
|
const handleChange = (event, newValue) => {
|
||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
@@ -83,6 +128,46 @@ output = query({
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCodeWithAuthorization = (codeLang) => {
|
||||||
|
if (codeLang === 'Python') {
|
||||||
|
return `import requests
|
||||||
|
|
||||||
|
API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}"
|
||||||
|
headers = {"Authorization": "Bearer ${selectedApiKey?.apiKey}"}
|
||||||
|
|
||||||
|
def query(payload):
|
||||||
|
response = requests.post(API_URL, headers=headers, json=payload)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
output = query({
|
||||||
|
"question": "Hey, how are you?",
|
||||||
|
})
|
||||||
|
`
|
||||||
|
} else if (codeLang === 'JavaScript') {
|
||||||
|
return `async function query(data) {
|
||||||
|
const response = await fetch(
|
||||||
|
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||||
|
{
|
||||||
|
headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" },
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
"question": "Hey, how are you?"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
} else if (codeLang === 'cURL') {
|
||||||
|
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||||
|
-X POST \\
|
||||||
|
-d '{"question": "Hey, how are you?"}'
|
||||||
|
-H "Authorization: Bearer ${selectedApiKey?.apiKey}"`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
const getLang = (codeLang) => {
|
const getLang = (codeLang) => {
|
||||||
if (codeLang === 'Python') {
|
if (codeLang === 'Python') {
|
||||||
return 'python'
|
return 'python'
|
||||||
@@ -105,6 +190,40 @@ output = query({
|
|||||||
return pythonSVG
|
return pythonSVG
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getAllAPIKeysApi.data) {
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: 'No Authorization',
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for (const key of getAllAPIKeysApi.data) {
|
||||||
|
options.push({
|
||||||
|
label: key.keyName,
|
||||||
|
name: key.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
options.push({
|
||||||
|
label: '- Add New Key -',
|
||||||
|
name: 'addnewkey'
|
||||||
|
})
|
||||||
|
setKeyOptions(options)
|
||||||
|
setAPIKeys(getAllAPIKeysApi.data)
|
||||||
|
|
||||||
|
if (dialogProps.chatflowApiKeyId) {
|
||||||
|
setChatflowApiKeyId(dialogProps.chatflowApiKeyId)
|
||||||
|
setSelectedApiKey(getAllAPIKeysApi.data.find((key) => key.id === dialogProps.chatflowApiKeyId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [dialogProps, getAllAPIKeysApi.data])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) getAllAPIKeysApi.request()
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [show])
|
||||||
|
|
||||||
const component = show ? (
|
const component = show ? (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={show}
|
open={show}
|
||||||
@@ -118,29 +237,42 @@ output = query({
|
|||||||
{dialogProps.title}
|
{dialogProps.title}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Tabs value={value} onChange={handleChange} aria-label='tabs'>
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
{codes.map((codeLang, index) => (
|
<div style={{ flex: 80 }}>
|
||||||
<Tab
|
<Tabs value={value} onChange={handleChange} aria-label='tabs'>
|
||||||
icon={
|
{codes.map((codeLang, index) => (
|
||||||
<img
|
<Tab
|
||||||
style={{ objectFit: 'cover', height: 'auto', width: 'auto', marginLeft: 10 }}
|
icon={
|
||||||
src={getSVG(codeLang)}
|
<img
|
||||||
alt='code'
|
style={{ objectFit: 'cover', height: 'auto', width: 'auto' }}
|
||||||
/>
|
src={getSVG(codeLang)}
|
||||||
}
|
alt='code'
|
||||||
iconPosition='left'
|
/>
|
||||||
key={index}
|
}
|
||||||
label={codeLang}
|
iconPosition='start'
|
||||||
{...a11yProps(index)}
|
key={index}
|
||||||
></Tab>
|
label={codeLang}
|
||||||
))}
|
{...a11yProps(index)}
|
||||||
</Tabs>
|
></Tab>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 20 }}>
|
||||||
|
<Dropdown
|
||||||
|
name='SelectKey'
|
||||||
|
disableClearable={true}
|
||||||
|
options={keyOptions}
|
||||||
|
onSelect={(newValue) => onApiKeySelected(newValue)}
|
||||||
|
value={dialogProps.chatflowApiKeyId ?? chatflowApiKeyId ?? 'Choose an API key'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{ marginTop: 10 }}></div>
|
<div style={{ marginTop: 10 }}></div>
|
||||||
{codes.map((codeLang, index) => (
|
{codes.map((codeLang, index) => (
|
||||||
<TabPanel key={index} value={value} index={index}>
|
<TabPanel key={index} value={value} index={index}>
|
||||||
<CopyBlock
|
<CopyBlock
|
||||||
theme={atomOneDark}
|
theme={atomOneDark}
|
||||||
text={getCode(codeLang)}
|
text={chatflowApiKeyId ? getCodeWithAuthorization(codeLang) : getCode(codeLang)}
|
||||||
language={getLang(codeLang)}
|
language={getLang(codeLang)}
|
||||||
showLineNumbers={false}
|
showLineNumbers={false}
|
||||||
wrapLines
|
wrapLines
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Stack,
|
||||||
|
IconButton,
|
||||||
|
OutlinedInput,
|
||||||
|
Popover
|
||||||
|
} from '@mui/material'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import { IconX, IconCopy } from '@tabler/icons'
|
||||||
|
|
||||||
|
// API
|
||||||
|
import apikeyApi from 'api/apikey'
|
||||||
|
|
||||||
|
// utils
|
||||||
|
import useNotifier from 'utils/useNotifier'
|
||||||
|
|
||||||
|
const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
|
const portalElement = document.getElementById('portal')
|
||||||
|
|
||||||
|
const theme = useTheme()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
// ==============================|| Snackbar ||============================== //
|
||||||
|
|
||||||
|
useNotifier()
|
||||||
|
|
||||||
|
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||||
|
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||||
|
|
||||||
|
const [keyName, setKeyName] = useState('')
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
const openPopOver = Boolean(anchorEl)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dialogProps.type === 'EDIT' && dialogProps.key) {
|
||||||
|
setKeyName(dialogProps.key.keyName)
|
||||||
|
} else if (dialogProps.type === 'ADD') {
|
||||||
|
setKeyName('')
|
||||||
|
}
|
||||||
|
}, [dialogProps])
|
||||||
|
|
||||||
|
const handleClosePopOver = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewKey = async () => {
|
||||||
|
try {
|
||||||
|
const createResp = await apikeyApi.createNewAPI({ keyName })
|
||||||
|
if (createResp.data) {
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: 'New API key added',
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'success',
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: `Failed to add new API key: ${errorData}`,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveKey = async () => {
|
||||||
|
try {
|
||||||
|
const saveResp = await apikeyApi.updateAPI(dialogProps.key.id, { keyName })
|
||||||
|
if (saveResp.data) {
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: 'API Key saved',
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'success',
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: `Failed to save API key: ${errorData}`,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = show ? (
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
maxWidth='sm'
|
||||||
|
open={show}
|
||||||
|
onClose={onCancel}
|
||||||
|
aria-labelledby='alert-dialog-title'
|
||||||
|
aria-describedby='alert-dialog-description'
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||||
|
{dialogProps.title}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{dialogProps.type === 'EDIT' && (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant='overline'>API Key</Typography>
|
||||||
|
<Stack direction='row' sx={{ mb: 1 }}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
width: 'max-content',
|
||||||
|
height: 'max-content'
|
||||||
|
}}
|
||||||
|
variant='h5'
|
||||||
|
>
|
||||||
|
{dialogProps.key.apiKey}
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
title='Copy API Key'
|
||||||
|
color='success'
|
||||||
|
onClick={(event) => {
|
||||||
|
navigator.clipboard.writeText(dialogProps.key.apiKey)
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClosePopOver()
|
||||||
|
}, 1500)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCopy />
|
||||||
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
open={openPopOver}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClosePopOver}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='h6' sx={{ pl: 1, pr: 1, color: 'white', background: theme.palette.success.dark }}>
|
||||||
|
Copied!
|
||||||
|
</Typography>
|
||||||
|
</Popover>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||||
|
<Typography variant='overline'>Key Name</Typography>
|
||||||
|
</Stack>
|
||||||
|
<OutlinedInput
|
||||||
|
id='keyName'
|
||||||
|
type='string'
|
||||||
|
fullWidth
|
||||||
|
placeholder='My New Key'
|
||||||
|
value={keyName}
|
||||||
|
name='keyName'
|
||||||
|
onChange={(e) => setKeyName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<StyledButton variant='contained' onClick={() => (dialogProps.type === 'ADD' ? addNewKey() : saveKey())}>
|
||||||
|
{dialogProps.confirmButtonName}
|
||||||
|
</StyledButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return createPortal(component, portalElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
APIKeyDialog.propTypes = {
|
||||||
|
show: PropTypes.bool,
|
||||||
|
dialogProps: PropTypes.object,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
onConfirm: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default APIKeyDialog
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
// project imports
|
||||||
|
import MainCard from 'ui-component/cards/MainCard'
|
||||||
|
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||||
|
import APIKeyDialog from './APIKeyDialog'
|
||||||
|
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
||||||
|
|
||||||
|
// API
|
||||||
|
import apiKeyApi from 'api/apikey'
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
import useApi from 'hooks/useApi'
|
||||||
|
import useConfirm from 'hooks/useConfirm'
|
||||||
|
|
||||||
|
// utils
|
||||||
|
import useNotifier from 'utils/useNotifier'
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons'
|
||||||
|
import APIEmptySVG from 'assets/images/api_empty.svg'
|
||||||
|
|
||||||
|
// ==============================|| APIKey ||============================== //
|
||||||
|
|
||||||
|
const APIKey = () => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
useNotifier()
|
||||||
|
|
||||||
|
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||||
|
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||||
|
|
||||||
|
const [showDialog, setShowDialog] = useState(false)
|
||||||
|
const [dialogProps, setDialogProps] = useState({})
|
||||||
|
const [apiKeys, setAPIKeys] = useState([])
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
const [showApiKeys, setShowApiKeys] = useState([])
|
||||||
|
const openPopOver = Boolean(anchorEl)
|
||||||
|
|
||||||
|
const { confirm } = useConfirm()
|
||||||
|
|
||||||
|
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
|
||||||
|
|
||||||
|
const onShowApiKeyClick = (apikey) => {
|
||||||
|
const index = showApiKeys.indexOf(apikey)
|
||||||
|
if (index > -1) {
|
||||||
|
//showApiKeys.splice(index, 1)
|
||||||
|
const newShowApiKeys = showApiKeys.filter(function (item) {
|
||||||
|
return item !== apikey
|
||||||
|
})
|
||||||
|
setShowApiKeys(newShowApiKeys)
|
||||||
|
} else {
|
||||||
|
setShowApiKeys((prevkeys) => [...prevkeys, apikey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClosePopOver = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNew = () => {
|
||||||
|
const dialogProp = {
|
||||||
|
title: 'Add New API Key',
|
||||||
|
type: 'ADD',
|
||||||
|
cancelButtonName: 'Cancel',
|
||||||
|
confirmButtonName: 'Add'
|
||||||
|
}
|
||||||
|
setDialogProps(dialogProp)
|
||||||
|
setShowDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = (key) => {
|
||||||
|
const dialogProp = {
|
||||||
|
title: 'Edit API Key',
|
||||||
|
type: 'EDIT',
|
||||||
|
cancelButtonName: 'Cancel',
|
||||||
|
confirmButtonName: 'Save',
|
||||||
|
key
|
||||||
|
}
|
||||||
|
setDialogProps(dialogProp)
|
||||||
|
setShowDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteKey = async (key) => {
|
||||||
|
const confirmPayload = {
|
||||||
|
title: `Delete`,
|
||||||
|
description: `Delete key ${key.keyName}?`,
|
||||||
|
confirmButtonName: 'Delete',
|
||||||
|
cancelButtonName: 'Cancel'
|
||||||
|
}
|
||||||
|
const isConfirmed = await confirm(confirmPayload)
|
||||||
|
|
||||||
|
if (isConfirmed) {
|
||||||
|
try {
|
||||||
|
const deleteResp = await apiKeyApi.deleteAPI(key.id)
|
||||||
|
if (deleteResp.data) {
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: 'API key deleted',
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'success',
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: `Failed to delete API key: ${errorData}`,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConfirm = () => {
|
||||||
|
setShowDialog(false)
|
||||||
|
getAllAPIKeysApi.request()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAllAPIKeysApi.request()
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getAllAPIKeysApi.data) {
|
||||||
|
setAPIKeys(getAllAPIKeysApi.data)
|
||||||
|
}
|
||||||
|
}, [getAllAPIKeysApi.data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||||
|
<Stack flexDirection='row'>
|
||||||
|
<h1>API Keys </h1>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
|
<StyledButton variant='contained' sx={{ color: 'white', mr: 1, height: 37 }} onClick={addNew} startIcon={<IconPlus />}>
|
||||||
|
Create Key
|
||||||
|
</StyledButton>
|
||||||
|
</Stack>
|
||||||
|
{apiKeys.length <= 0 && (
|
||||||
|
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||||
|
<Box sx={{ p: 2, height: 'auto' }}>
|
||||||
|
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={APIEmptySVG} alt='APIEmptySVG' />
|
||||||
|
</Box>
|
||||||
|
<div>No API Keys Yet</div>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{apiKeys.length > 0 && (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Key Name</TableCell>
|
||||||
|
<TableCell>API Key</TableCell>
|
||||||
|
<TableCell>Created</TableCell>
|
||||||
|
<TableCell> </TableCell>
|
||||||
|
<TableCell> </TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{apiKeys.map((key, index) => (
|
||||||
|
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||||
|
<TableCell component='th' scope='row'>
|
||||||
|
{key.keyName}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{showApiKeys.includes(key.apiKey)
|
||||||
|
? key.apiKey
|
||||||
|
: `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring(
|
||||||
|
key.apiKey.length - 5
|
||||||
|
)}`}
|
||||||
|
<IconButton
|
||||||
|
title='Copy'
|
||||||
|
color='success'
|
||||||
|
onClick={(event) => {
|
||||||
|
navigator.clipboard.writeText(key.apiKey)
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClosePopOver()
|
||||||
|
}, 1500)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCopy />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton title='Show' color='inherit' onClick={() => onShowApiKeyClick(key.apiKey)}>
|
||||||
|
{showApiKeys.includes(key.apiKey) ? <IconEyeOff /> : <IconEye />}
|
||||||
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
open={openPopOver}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClosePopOver}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant='h6'
|
||||||
|
sx={{ pl: 1, pr: 1, color: 'white', background: theme.palette.success.dark }}
|
||||||
|
>
|
||||||
|
Copied!
|
||||||
|
</Typography>
|
||||||
|
</Popover>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{key.createdAt}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton title='Edit' color='primary' onClick={() => edit(key)}>
|
||||||
|
<IconEdit />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton title='Delete' color='error' onClick={() => deleteKey(key)}>
|
||||||
|
<IconTrash />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
</MainCard>
|
||||||
|
<APIKeyDialog
|
||||||
|
show={showDialog}
|
||||||
|
dialogProps={dialogProps}
|
||||||
|
onCancel={() => setShowDialog(false)}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
></APIKeyDialog>
|
||||||
|
<ConfirmDialog />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default APIKey
|
||||||
@@ -8,7 +8,7 @@ import { useTheme } from '@mui/material/styles'
|
|||||||
import { Avatar, Box, ButtonBase, Typography, Stack, TextField } from '@mui/material'
|
import { Avatar, Box, ButtonBase, Typography, Stack, TextField } from '@mui/material'
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX, IconWorldWww } from '@tabler/icons'
|
import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX, IconCode } from '@tabler/icons'
|
||||||
|
|
||||||
// project imports
|
// project imports
|
||||||
import Settings from 'views/settings'
|
import Settings from 'views/settings'
|
||||||
@@ -82,7 +82,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
|||||||
const onAPIDialogClick = () => {
|
const onAPIDialogClick = () => {
|
||||||
setAPIDialogProps({
|
setAPIDialogProps({
|
||||||
title: 'Use this chatflow with API',
|
title: 'Use this chatflow with API',
|
||||||
chatflowid: chatflow.id
|
chatflowid: chatflow.id,
|
||||||
|
chatflowApiKeyId: chatflow.apikeyid
|
||||||
})
|
})
|
||||||
setAPIDialogOpen(true)
|
setAPIDialogOpen(true)
|
||||||
}
|
}
|
||||||
@@ -247,7 +248,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
|||||||
color='inherit'
|
color='inherit'
|
||||||
onClick={onAPIDialogClick}
|
onClick={onAPIDialogClick}
|
||||||
>
|
>
|
||||||
<IconWorldWww stroke={1.5} size='1.3rem' />
|
<IconCode stroke={1.5} size='1.3rem' />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>
|
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user