add API authorization

This commit is contained in:
Henry 2023-04-29 18:01:40 +01:00
parent 419d9634b4
commit 2a98c9510f
12 changed files with 967 additions and 111 deletions

View File

@ -9,6 +9,7 @@ export interface IChatFlow {
id: string
name: string
flowData: string
apikeyid: string
deployed: boolean
updatedDate: Date
createdDate: Date

View File

@ -13,6 +13,9 @@ export class ChatFlow implements IChatFlow {
@Column()
flowData: string
@Column({ nullable: true })
apikeyid: string
@Column()
deployed: boolean

View File

@ -12,7 +12,12 @@ import {
getEndingNode,
constructGraphs,
resolveVariables,
isStartNodeDependOnInput
isStartNodeDependOnInput,
getAPIKeys,
addAPIKey,
updateAPIKey,
deleteAPIKey,
compareKeys
} from './utils'
import { cloneDeep } from 'lodash'
import { getDataSource } from './DataSource'
@ -42,6 +47,9 @@ export class App {
await this.nodesPool.initialize()
this.chatflowPool = new ChatflowPool()
// Initialize API keys
await getAPIKeys()
})
.catch((err) => {
console.error('❌[server]: Error during Data Source initialization:', err)
@ -195,93 +203,14 @@ export class App {
// 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) => {
try {
const chatflowid = req.params.id
const incomingInput: IncomingInput = req.body
await this.processPrediction(req, res)
})
let nodeToExecuteData: INodeData
/* 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 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)
}
// Send input message and get prediction result (Internal)
this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => {
await this.processPrediction(req, res, true)
})
// ----------------------------------------
@ -308,6 +237,34 @@ export class App {
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
// ----------------------------------------
@ -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() {
try {
const removePromises: any[] = []

View File

@ -1,5 +1,6 @@
import path from 'path'
import fs from 'fs'
import moment from 'moment'
import {
IComponentNodes,
IDepthQueue,
@ -14,6 +15,7 @@ import {
} from '../Interface'
import { cloneDeep, get } from 'lodash'
import { ICommonObject, getInputVariables } from 'flowise-components'
import { scryptSync, randomBytes, timingSafeEqual } from 'crypto'
const QUESTION_VAR_PREFIX = 'question'
@ -362,3 +364,119 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[]): boole
}
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
}

View File

@ -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
}

View File

@ -1,6 +1,6 @@
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 {
sendMessageAndGetPrediction

View File

@ -1,8 +1,8 @@
// assets
import { IconHierarchy, IconBuildingStore } from '@tabler/icons'
import { IconHierarchy, IconBuildingStore, IconKey } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconBuildingStore }
const icons = { IconHierarchy, IconBuildingStore, IconKey }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@ -26,6 +26,14 @@ const dashboard = {
url: '/marketplaces',
icon: icons.IconBuildingStore,
breadcrumbs: true
},
{
id: 'apikey',
title: 'API Keys',
type: 'item',
url: '/apikey',
icon: icons.IconKey,
breadcrumbs: true
}
]
}

View File

@ -10,6 +10,9 @@ const Chatflows = Loadable(lazy(() => import('views/chatflows')))
// marketplaces routing
const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
// apikey routing
const APIKey = Loadable(lazy(() => import('views/apikey')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@ -27,6 +30,10 @@ const MainRoutes = {
{
path: '/marketplaces',
element: <Marketplaces />
},
{
path: '/apikey',
element: <APIKey />
}
]
}

View File

@ -1,14 +1,31 @@
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 { Tabs, Tab, Dialog, DialogContent, DialogTitle, Box } from '@mui/material'
import { CopyBlock, atomOneDark } from 'react-code-blocks'
// Project import
import { Dropdown } from 'ui-component/dropdown/Dropdown'
// Const
import { baseURL } from 'store/constant'
import { SET_CHATFLOW } from 'store/actions'
// Images
import pythonSVG from 'assets/images/python.svg'
import javascriptSVG from 'assets/images/javascript.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) {
const { children, value, index, ...other } = props
return (
@ -39,8 +56,36 @@ function a11yProps(index) {
const APICodeDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const navigate = useNavigate()
const dispatch = useDispatch()
const codes = ['Python', 'JavaScript', 'cURL']
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) => {
setValue(newValue)
@ -83,6 +128,46 @@ output = query({
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) => {
if (codeLang === 'Python') {
return 'python'
@ -105,6 +190,40 @@ output = query({
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 ? (
<Dialog
open={show}
@ -118,29 +237,42 @@ output = query({
{dialogProps.title}
</DialogTitle>
<DialogContent>
<Tabs value={value} onChange={handleChange} aria-label='tabs'>
{codes.map((codeLang, index) => (
<Tab
icon={
<img
style={{ objectFit: 'cover', height: 'auto', width: 'auto', marginLeft: 10 }}
src={getSVG(codeLang)}
alt='code'
/>
}
iconPosition='left'
key={index}
label={codeLang}
{...a11yProps(index)}
></Tab>
))}
</Tabs>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div style={{ flex: 80 }}>
<Tabs value={value} onChange={handleChange} aria-label='tabs'>
{codes.map((codeLang, index) => (
<Tab
icon={
<img
style={{ objectFit: 'cover', height: 'auto', width: 'auto' }}
src={getSVG(codeLang)}
alt='code'
/>
}
iconPosition='start'
key={index}
label={codeLang}
{...a11yProps(index)}
></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>
{codes.map((codeLang, index) => (
<TabPanel key={index} value={value} index={index}>
<CopyBlock
theme={atomOneDark}
text={getCode(codeLang)}
text={chatflowApiKeyId ? getCodeWithAuthorization(codeLang) : getCode(codeLang)}
language={getLang(codeLang)}
showLineNumbers={false}
wrapLines

View File

@ -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

View File

@ -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&nbsp;</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

View File

@ -8,7 +8,7 @@ import { useTheme } from '@mui/material/styles'
import { Avatar, Box, ButtonBase, Typography, Stack, TextField } from '@mui/material'
// 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
import Settings from 'views/settings'
@ -82,7 +82,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const onAPIDialogClick = () => {
setAPIDialogProps({
title: 'Use this chatflow with API',
chatflowid: chatflow.id
chatflowid: chatflow.id,
chatflowApiKeyId: chatflow.apikeyid
})
setAPIDialogOpen(true)
}
@ -247,7 +248,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
color='inherit'
onClick={onAPIDialogClick}
>
<IconWorldWww stroke={1.5} size='1.3rem' />
<IconCode stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>