diff --git a/server/src/api/index.ts b/server/src/api/index.ts deleted file mode 100644 index e1e4e5a4..00000000 --- a/server/src/api/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -const genAPIKey = () => { - return [...Array(30)] - .map((e) => ((Math.random() * 36) | 0).toString(36)) - .join(''); -}; \ No newline at end of file diff --git a/server/src/api/record.ts b/server/src/api/record.ts new file mode 100644 index 00000000..a7e39029 --- /dev/null +++ b/server/src/api/record.ts @@ -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.`, + }); + } +}); diff --git a/server/src/middlewares/api.ts b/server/src/middlewares/api.ts new file mode 100644 index 00000000..c79529eb --- /dev/null +++ b/server/src/middlewares/api.ts @@ -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(); +}; diff --git a/server/src/models/User.ts b/server/src/models/User.ts index d5a93a7a..40eb2198 100644 --- a/server/src/models/User.ts +++ b/server/src/models/User.ts @@ -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 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, diff --git a/server/src/routes/api.ts b/server/src/routes/api.ts new file mode 100644 index 00000000..cc417bfb --- /dev/null +++ b/server/src/routes/api.ts @@ -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 }); + } +}); diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index b1431c10..3effb0d2 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -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}` }); } -}); \ No newline at end of file +}); + +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 }); + } +}); diff --git a/server/src/utils/api.ts b/server/src/utils/api.ts new file mode 100644 index 00000000..cc1eb61f --- /dev/null +++ b/server/src/utils/api.ts @@ -0,0 +1,3 @@ +export const genAPIKey = (): string => { + return [...Array(30)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''); +}; diff --git a/server/src/workflow-management/integrations/gsheet.ts b/server/src/workflow-management/integrations/gsheet.ts index 3ce55341..11a84023 100644 --- a/server/src/workflow-management/integrations/gsheet.ts +++ b/server/src/workflow-management/integrations/gsheet.ts @@ -122,4 +122,3 @@ export const processGoogleSheetUpdates = async () => { await new Promise(resolve => setTimeout(resolve, 5000)); } }; - \ No newline at end of file diff --git a/src/components/molecules/InterpretationLog.tsx b/src/components/molecules/InterpretationLog.tsx index 3feda80c..1af5bc8c 100644 --- a/src/components/molecules/InterpretationLog.tsx +++ b/src/components/molecules/InterpretationLog.tsx @@ -25,7 +25,7 @@ interface InterpretationLogProps { setIsOpen: (isOpen: boolean) => void; } -export const InterpretationLog: React.FC = ({ isOpen, setIsOpen }) => { +export const InterpretationLog: React.FC = ({ isOpen, setIsOpen }) => { const [log, setLog] = useState(''); const [selectedOption, setSelectedOption] = useState('10'); const [customValue, setCustomValue] = useState(''); @@ -66,12 +66,12 @@ export const InterpretationLog: React.FC = ({ 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 = ({ isOpen, se // Extract columns dynamically from the first item of tableData const columns = tableData.length > 0 ? Object.keys(tableData[0]) : []; - + return (
: null} - - - - - Interpreter settings - - { - Object.keys(row.interpreterSettings).map((setting, index) => { - if (setting === 'params') { - return ( -
- - - Recording parameters - - { - Object.keys(row.interpreterSettings.params).map((param, index) => { - return ( - - {/*@ts-ignore*/} - {param}: {row.interpreterSettings.params[param].toString()} - - ) - }) - } -
- ) - } - return ( - - {/*@ts-ignore*/} - {setting}: {row.interpreterSettings[setting].toString()} - - ) - }) - } -
- - { !row || !row.serializableOutput || !row.binaryOutput - || (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0) - ? The output is empty. : null } - - {row.serializableOutput && - Object.keys(row.serializableOutput).length !== 0 && -
- - - Serializable output - { Object.keys(row.serializableOutput).map((key) => { - return ( -
- - {key}: - Download - - -
-                    {row.serializableOutput[key] ? JSON.stringify(row.serializableOutput[key], null, 2)
-                    : 'The output is empty.'}
-                  
-
-
- ) - })} -
- } - {row.binaryOutput - && Object.keys(row.binaryOutput).length !== 0 && -
- - - Binary output - { Object.keys(row.binaryOutput).map((key) => { - try { - const binaryBuffer = JSON.parse(row.binaryOutput[key].data); - const b64 = Buffer.from(binaryBuffer.data).toString('base64'); + + +
+ + {interpretationInProgress ? currentLog : row.log} + +
+
+ + {interpretationInProgress ? : null} + + + + + Interpreter settings + + { + Object.keys(row.interpreterSettings).map((setting, index) => { + if (setting === 'params') { return ( - - - {key}: - Download +
+ + + Recording parameters - {key} - + { + Object.keys(row.interpreterSettings.params).map((param, index) => { + return ( + + {/*@ts-ignore*/} + {param}: {row.interpreterSettings.params[param].toString()} + + ) + }) + } +
) - } catch (e) { - console.log(e) - return - {key}: The image failed to render - } + return ( + + {/*@ts-ignore*/} + {setting}: {row.interpreterSettings[setting].toString()} + + ) + }) + } +
+ + {!row || !row.serializableOutput || !row.binaryOutput + || (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0) + ? The output is empty. : null} + + {row.serializableOutput && + Object.keys(row.serializableOutput).length !== 0 && +
+ + + Serializable output + {Object.keys(row.serializableOutput).map((key) => { + return ( +
+ + {key}: + Download + + +
+                        {row.serializableOutput[key] ? JSON.stringify(row.serializableOutput[key], null, 2)
+                          : 'The output is empty.'}
+                      
+
+
+ ) })} -
- } -
+
+ } + {row.binaryOutput + && Object.keys(row.binaryOutput).length !== 0 && +
+ + + Binary output + {Object.keys(row.binaryOutput).map((key) => { + try { + const binaryBuffer = JSON.parse(row.binaryOutput[key].data); + const b64 = Buffer.from(binaryBuffer.data).toString('base64'); + return ( + + + {key}: + Download + + {key} + + ) + } catch (e) { + console.log(e) + return + {key}: The image failed to render + + } + })} +
+ } +
); diff --git a/src/context/auth.tsx b/src/context/auth.tsx index cb5c01df..2f0dcd5d 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -21,7 +21,7 @@ const initialState = { const AuthContext = createContext<{ state: InitialStateType; - dispatch: any; + dispatch: React.Dispatch; }>({ 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 ( - {children} + + {children} + ); }; -export { AuthContext, AuthProvider }; +export { AuthContext, AuthProvider }; \ No newline at end of file