Merge pull request #44 from amhsirak/develop

feat: programmatic access
This commit is contained in:
Karishma Shukla
2024-09-28 22:42:05 +05:30
committed by GitHub
11 changed files with 352 additions and 162 deletions

View File

@@ -1,5 +0,0 @@
const genAPIKey = () => {
return [...Array(30)]
.map((e) => ((Math.random() * 36) | 0).toString(36))
.join('');
};

105
server/src/api/record.ts Normal file
View File

@@ -0,0 +1,105 @@
import { readFile, readFiles } from "../workflow-management/storage";
import { Router, Request, Response } from 'express';
import { requireAPIKey } from "../middlewares/api";
export const router = Router();
const formatRecording = (recordingData: any) => {
const recordingMeta = recordingData.recording_meta;
const workflow = recordingData.recording.workflow || [];
const firstWorkflowStep = workflow[0]?.where?.url || '';
const inputParameters = [
{
type: "string",
name: "originUrl",
label: "Origin URL",
required: true,
defaultValue: firstWorkflowStep,
},
];
return {
id: recordingMeta.id,
name: recordingMeta.name,
createdAt: new Date(recordingMeta.create_date).getTime(),
inputParameters,
};
};
router.get("/api/robots", requireAPIKey, async (req: Request, res: Response) => {
try {
const fileContents = await readFiles('./../storage/recordings/');
const formattedRecordings = fileContents.map((fileContent: string) => {
const recordingData = JSON.parse(fileContent);
return formatRecording(recordingData);
});
const response = {
statusCode: 200,
messageCode: "success",
robots: {
totalCount: formattedRecordings.length,
items: formattedRecordings,
},
};
res.status(200).json(response);
} catch (error) {
console.error("Error fetching recordings:", error);
res.status(500).json({
statusCode: 500,
messageCode: "error",
message: "Failed to retrieve recordings",
});
}
});
const formatRecordingById = (recordingData: any) => {
const recordingMeta = recordingData.recording_meta;
const workflow = recordingData.recording.workflow || [];
const firstWorkflowStep = workflow[0]?.where?.url || '';
const inputParameters = [
{
type: "string",
name: "originUrl",
label: "Origin URL",
required: true,
defaultValue: firstWorkflowStep,
},
];
return {
id: recordingMeta.id,
name: recordingMeta.name,
createdAt: new Date(recordingMeta.create_date).getTime(),
inputParameters,
};
};
router.get("/api/robots/:fileName", requireAPIKey, async (req: Request, res: Response) => {
try {
const fileContent = await readFile(`./../storage/recordings/${req.params.fileName}.waw.json`);
const recordingData = JSON.parse(fileContent);
const formattedRecording = formatRecordingById(recordingData);
const response = {
statusCode: 200,
messageCode: "success",
robot: formattedRecording,
};
res.status(200).json(response);
} catch (error) {
console.error("Error fetching recording:", error);
res.status(404).json({
statusCode: 404,
messageCode: "not_found",
message: `Recording with name "${req.params.fileName}" not found.`,
});
}
});

View File

@@ -0,0 +1,15 @@
import { Request, Response } from "express";
import User from "../models/User";
export const requireAPIKey = async (req: Request, res: Response, next: any) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: "API key is missing" });
}
const user = await User.findOne({ where: { api_key: apiKey } });
if (!user) {
return res.status(403).json({ error: "Invalid API key" });
}
req.user = user;
next();
};

View File

@@ -5,6 +5,7 @@ interface UserAttributes {
id: number;
email: string;
password: string;
api_key?: string | null;
}
// Optional fields for creating a new user
@@ -14,6 +15,7 @@ class User extends Model<UserAttributes, UserCreationAttributes> implements User
public id!: number;
public email!: string;
public password!: string;
public api_key!: string | null;
}
User.init(
@@ -35,6 +37,10 @@ User.init(
type: DataTypes.STRING,
allowNull: false,
},
api_key: {
type: DataTypes.STRING,
allowNull: true,
},
},
{
sequelize,

40
server/src/routes/api.ts Normal file
View File

@@ -0,0 +1,40 @@
import { Router, Request, Response } from 'express';
import { genAPIKey } from '../utils/api';
import User from '../models/User';
export const router = Router();
interface AuthenticatedRequest extends Request {
user?: { id: string };
}
router.get('/generate-api-key', async (req: AuthenticatedRequest, res) => {
try {
if (!req.user) {
return res.status(401).json({ ok: false, error: 'Unauthorized' });
}
const user = await User.findByPk(req.user.id, {
attributes: { exclude: ['password'] },
});
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
if (user.api_key) {
return res.status(400).json({ message: 'API key already exists' });
}
const apiKey = genAPIKey();
user.api_key = apiKey;
await user.save();
return res.status(200).json({
message: 'API key generated successfully',
api_key: apiKey,
});
} catch (error) {
return res.status(500).json({ message: 'Error generating API key', error });
}
});

View File

@@ -3,6 +3,7 @@ import User from '../models/User';
import jwt from 'jsonwebtoken';
import { hashPassword, comparePassword } from '../utils/auth';
import { requireSignIn } from '../middlewares/auth';
import { genAPIKey } from '../utils/api';
export const router = Router();
interface AuthenticatedRequest extends Request {
@@ -88,4 +89,35 @@ router.get('/current-user', requireSignIn, async (req: AuthenticatedRequest, res
console.error('Error in current-user route:', error);
return res.status(500).json({ ok: false, error: `Could not fetch current user: ${error.message}` });
}
});
});
router.post('/generate-api-key', requireSignIn, async (req: AuthenticatedRequest, res) => {
try {
if (!req.user) {
return res.status(401).json({ ok: false, error: 'Unauthorized' });
}
const user = await User.findByPk(req.user.id, {
attributes: { exclude: ['password'] },
});
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
if (user.api_key) {
return res.status(400).json({ message: 'API key already exists' });
}
const apiKey = genAPIKey();
user.api_key = apiKey;
await user.save();
return res.status(200).json({
message: 'API key generated successfully',
api_key: apiKey,
});
} catch (error) {
return res.status(500).json({ message: 'Error generating API key', error });
}
});

3
server/src/utils/api.ts Normal file
View File

@@ -0,0 +1,3 @@
export const genAPIKey = (): string => {
return [...Array(30)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
};

View File

@@ -122,4 +122,3 @@ export const processGoogleSheetUpdates = async () => {
await new Promise(resolve => setTimeout(resolve, 5000));
}
};

View File

@@ -25,7 +25,7 @@ interface InterpretationLogProps {
setIsOpen: (isOpen: boolean) => void;
}
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
const [log, setLog] = useState<string>('');
const [selectedOption, setSelectedOption] = useState<string>('10');
const [customValue, setCustomValue] = useState('');
@@ -66,12 +66,12 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
setLog((prevState) =>
prevState + '\n' + '---------- Serializable output data received ----------' + '\n'
+ JSON.stringify(data, null, 2) + '\n' + '--------------------------------------------------');
// Set table data
if (Array.isArray(data)) {
setTableData(data);
}
scrollLogToBottom();
}, [log, scrollLogToBottom]);
@@ -104,7 +104,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
// Extract columns dynamically from the first item of tableData
const columns = tableData.length > 0 ? Object.keys(tableData[0]) : [];
return (
<div>
<button

View File

@@ -7,7 +7,7 @@ import { TabPanel, TabContext } from "@mui/lab";
import SettingsIcon from '@mui/icons-material/Settings';
import ImageIcon from '@mui/icons-material/Image';
import ArticleIcon from '@mui/icons-material/Article';
import {Buffer} from 'buffer';
import { Buffer } from 'buffer';
import { useEffect } from "react";
import AssignmentIcon from '@mui/icons-material/Assignment';
@@ -19,7 +19,7 @@ interface RunContentProps {
abortRunHandler: () => void,
}
export const RunContent = ({row, currentLog, interpretationInProgress, logEndRef, abortRunHandler}: RunContentProps) => {
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
const [tab, setTab] = React.useState<string>('log');
useEffect(() => {
@@ -29,141 +29,142 @@ export const RunContent = ({row, currentLog, interpretationInProgress, logEndRef
return (
<Box sx={{ width: '100%' }}>
<TabContext value={tab}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs">
<Tab label="Log" value='log' />
<Tab label="Input" value='input' />
<Tab label="Output" value='output' />
</Tabs>
</Box>
<TabPanel value='log'>
<Box sx={{ margin: 1,
background: '#19171c',
overflowY: 'scroll',
overflowX: 'scroll',
width: '700px',
height: 'fit-content',
maxHeight: '450px',
}}>
<div>
<Highlight className="javascript">
{interpretationInProgress ? currentLog : row.log}
</Highlight>
<div style={{ float:"left", clear: "both" }}
ref={logEndRef}/>
</div>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs">
<Tab label="Log" value='log' />
<Tab label="Input" value='input' />
<Tab label="Output" value='output' />
</Tabs>
</Box>
{interpretationInProgress ? <Button
color="error"
onClick={abortRunHandler}
>
Stop
</Button> : null}
</TabPanel>
<TabPanel value='input' sx={{width: '700px'}}>
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
<SettingsIcon sx={{marginRight: '15px'}}/>
Interpreter settings
</Typography>
{
Object.keys(row.interpreterSettings).map((setting, index) => {
if (setting === 'params') {
return (
<div key={`settings-${setting}-${index}`}>
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}} key={`setting-${index}`}>
<AssignmentIcon sx={{marginRight: '15px'}}/>
Recording parameters
</Typography>
{
Object.keys(row.interpreterSettings.params).map((param, index) => {
return (
<Typography key={`recording-params-item-${index}`} sx={{margin: '10px'}}>
{/*@ts-ignore*/}
{param}: {row.interpreterSettings.params[param].toString()}
</Typography>
)
})
}
</div>
)
}
return (
<Typography key={`interpreter-settings-item-${index}`} sx={{margin: '10px'}}>
{/*@ts-ignore*/}
{setting}: {row.interpreterSettings[setting].toString()}
</Typography>
)
})
}
</TabPanel>
<TabPanel value='output' sx={{width: '700px'}}>
{ !row || !row.serializableOutput || !row.binaryOutput
|| (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0)
? <Typography>The output is empty.</Typography> : null }
{row.serializableOutput &&
Object.keys(row.serializableOutput).length !== 0 &&
<div>
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
<ArticleIcon sx={{marginRight: '15px'}}/>
Serializable output</Typography>
{ Object.keys(row.serializableOutput).map((key) => {
return (
<div key={`number-of-serializable-output-${key}`}>
<Typography>
{key}:
<a href={`data:application/json;utf8,${JSON.stringify(row.serializableOutput[key], null, 2)}`}
download={key} style={{margin:'10px'}}>Download</a>
</Typography>
<Box sx={{
width: 'fit-content',
background: 'rgba(0,0,0,0.06)',
maxHeight: '300px',
overflow: 'scroll',
}}>
<pre key={`serializable-output-${key}`}>
{row.serializableOutput[key] ? JSON.stringify(row.serializableOutput[key], null, 2)
: 'The output is empty.'}
</pre>
</Box>
</div>
)
})}
</div>
}
{row.binaryOutput
&& Object.keys(row.binaryOutput).length !== 0 &&
<div>
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
<ImageIcon sx={{marginRight:'15px'}}/>
Binary output</Typography>
{ Object.keys(row.binaryOutput).map((key) => {
try {
const binaryBuffer = JSON.parse(row.binaryOutput[key].data);
const b64 = Buffer.from(binaryBuffer.data).toString('base64');
<TabPanel value='log'>
<Box sx={{
margin: 1,
background: '#19171c',
overflowY: 'scroll',
overflowX: 'scroll',
width: '700px',
height: 'fit-content',
maxHeight: '450px',
}}>
<div>
<Highlight className="javascript">
{interpretationInProgress ? currentLog : row.log}
</Highlight>
<div style={{ float: "left", clear: "both" }}
ref={logEndRef} />
</div>
</Box>
{interpretationInProgress ? <Button
color="error"
onClick={abortRunHandler}
>
Stop
</Button> : null}
</TabPanel>
<TabPanel value='input' sx={{ width: '700px' }}>
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
<SettingsIcon sx={{ marginRight: '15px' }} />
Interpreter settings
</Typography>
{
Object.keys(row.interpreterSettings).map((setting, index) => {
if (setting === 'params') {
return (
<Box key={`number-of-binary-output-${key}`} sx={{
width: 'max-content',
}}>
<Typography key={`binary-output-key-${key}`}>
{key}:
<a href={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
download={key} style={{margin:'10px'}}>Download</a>
<div key={`settings-${setting}-${index}`}>
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }} key={`setting-${index}`}>
<AssignmentIcon sx={{ marginRight: '15px' }} />
Recording parameters
</Typography>
<img key={`image-${key}`} src={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
alt={key} height='auto' width='700px'/>
</Box>
{
Object.keys(row.interpreterSettings.params).map((param, index) => {
return (
<Typography key={`recording-params-item-${index}`} sx={{ margin: '10px' }}>
{/*@ts-ignore*/}
{param}: {row.interpreterSettings.params[param].toString()}
</Typography>
)
})
}
</div>
)
} catch (e) {
console.log(e)
return <Typography key={`number-of-binary-output-${key}`}>
{key}: The image failed to render
</Typography>
}
return (
<Typography key={`interpreter-settings-item-${index}`} sx={{ margin: '10px' }}>
{/*@ts-ignore*/}
{setting}: {row.interpreterSettings[setting].toString()}
</Typography>
)
})
}
</TabPanel>
<TabPanel value='output' sx={{ width: '700px' }}>
{!row || !row.serializableOutput || !row.binaryOutput
|| (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0)
? <Typography>The output is empty.</Typography> : null}
{row.serializableOutput &&
Object.keys(row.serializableOutput).length !== 0 &&
<div>
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
<ArticleIcon sx={{ marginRight: '15px' }} />
Serializable output</Typography>
{Object.keys(row.serializableOutput).map((key) => {
return (
<div key={`number-of-serializable-output-${key}`}>
<Typography>
{key}:
<a href={`data:application/json;utf8,${JSON.stringify(row.serializableOutput[key], null, 2)}`}
download={key} style={{ margin: '10px' }}>Download</a>
</Typography>
<Box sx={{
width: 'fit-content',
background: 'rgba(0,0,0,0.06)',
maxHeight: '300px',
overflow: 'scroll',
}}>
<pre key={`serializable-output-${key}`}>
{row.serializableOutput[key] ? JSON.stringify(row.serializableOutput[key], null, 2)
: 'The output is empty.'}
</pre>
</Box>
</div>
)
})}
</div>
}
</TabPanel>
</div>
}
{row.binaryOutput
&& Object.keys(row.binaryOutput).length !== 0 &&
<div>
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
<ImageIcon sx={{ marginRight: '15px' }} />
Binary output</Typography>
{Object.keys(row.binaryOutput).map((key) => {
try {
const binaryBuffer = JSON.parse(row.binaryOutput[key].data);
const b64 = Buffer.from(binaryBuffer.data).toString('base64');
return (
<Box key={`number-of-binary-output-${key}`} sx={{
width: 'max-content',
}}>
<Typography key={`binary-output-key-${key}`}>
{key}:
<a href={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
download={key} style={{ margin: '10px' }}>Download</a>
</Typography>
<img key={`image-${key}`} src={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
alt={key} height='auto' width='700px' />
</Box>
)
} catch (e) {
console.log(e)
return <Typography key={`number-of-binary-output-${key}`}>
{key}: The image failed to render
</Typography>
}
})}
</div>
}
</TabPanel>
</TabContext>
</Box>
);

View File

@@ -21,7 +21,7 @@ const initialState = {
const AuthContext = createContext<{
state: InitialStateType;
dispatch: any;
dispatch: React.Dispatch<ActionType>;
}>({
state: initialState,
dispatch: () => null,
@@ -46,38 +46,34 @@ const reducer = (state: InitialStateType, action: ActionType) => {
const AuthProvider = ({ children }: AuthProviderProps) => {
const [state, dispatch] = useReducer(reducer, initialState);
const navigate = useNavigate();
axios.defaults.withCredentials = true;
// get user info from local storage
useEffect(() => {
dispatch({
type: 'LOGIN',
payload: JSON.parse(window.localStorage.getItem('user') || 'null'),
});
const storedUser = window.localStorage.getItem('user');
if (storedUser) {
dispatch({ type: 'LOGIN', payload: JSON.parse(storedUser) });
}
}, []);
axios.interceptors.response.use(
function (response) {
// any status code that lies within the range of 2XX causes this function to trigger
return response;
},
function (error) {
// any status codes that fall outside the range of 2XX cause this function to trigger
let res = error.response;
const res = error.response;
if (res.status === 401 && res.config && !res.config.__isRetryRequest) {
return new Promise((resolve, reject) => {
axios
.get('http://localhost:8080/auth/logout')
.then((data) => {
.then(() => {
console.log('/401 error > logout');
dispatch({ type: 'LOGOUT' });
window.localStorage.removeItem('user');
navigate('/login'); // Replace router.push with navigate
navigate('/login');
})
.catch((err) => {
console.log('AXIOS INTERCEPTORS ERROR:', err);
console.error('AXIOS INTERCEPTORS ERROR:', err);
reject(error);
});
});
@@ -86,16 +82,12 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
}
);
// csrf - include tokens in the axios header every time a request is made
useEffect(() => {
const getCsrfToken = async () => {
try {
const { data } = await axios.get('http://localhost:8080/csrf-token');
console.log('CSRF Token Response:', data);
if (data && data.csrfToken) {
if (data.csrfToken) {
(axios.defaults.headers as any)['X-CSRF-TOKEN'] = data.csrfToken;
} else {
console.error('CSRF token not found in the response');
}
} catch (error) {
console.error('Error fetching CSRF token:', error);
@@ -105,8 +97,10 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
}, []);
return (
<AuthContext.Provider value={{ state, dispatch }}>{children}</AuthContext.Provider>
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
};
export { AuthContext, AuthProvider };
export { AuthContext, AuthProvider };