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
+16
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
}
+1 -1
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
+10 -2
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
}
]
}
+7
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 />
}
]
}
@@ -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
@@ -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
+279
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
+4 -3
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 }}>