Merge pull request #181 from getmaxun/robot-duplication
feat: robot duplication
This commit is contained in:
@@ -16,12 +16,12 @@ export const requireSignIn = (req: UserRequest, res: Response, next: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
verify(token, secret, (err: any, user: any) => {
|
verify(token, secret, (err: any, user: any) => {
|
||||||
console.log(err)
|
if (err) {
|
||||||
|
console.log('JWT verification error:', err);
|
||||||
if (err) return res.sendStatus(403)
|
return res.sendStatus(403);
|
||||||
|
}
|
||||||
req.user = user;
|
req.user = { ...user, id: user.userId }; // Normalize to include `id`
|
||||||
|
next();
|
||||||
next()
|
});
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -145,6 +145,80 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST endpoint to duplicate a robot and update its target URL.
|
||||||
|
*/
|
||||||
|
router.post('/recordings/:id/duplicate', requireSignIn, async (req: AuthenticatedRequest, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { targetUrl } = req.body;
|
||||||
|
|
||||||
|
if (!targetUrl) {
|
||||||
|
return res.status(400).json({ error: 'The "targetUrl" field is required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalRobot = await Robot.findOne({ where: { 'recording_meta.id': id } });
|
||||||
|
|
||||||
|
if (!originalRobot) {
|
||||||
|
return res.status(404).json({ error: 'Original robot not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastWord = targetUrl.split('/').filter(Boolean).pop() || 'Unnamed';
|
||||||
|
|
||||||
|
const workflow = originalRobot.recording.workflow.map((step) => {
|
||||||
|
if (step.where?.url && step.where.url !== "about:blank") {
|
||||||
|
step.where.url = targetUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
step.what.forEach((action) => {
|
||||||
|
if (action.action === "goto" && action.args?.length) {
|
||||||
|
action.args[0] = targetUrl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return step;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentTimestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
const newRobot = await Robot.create({
|
||||||
|
id: uuid(),
|
||||||
|
userId: originalRobot.userId,
|
||||||
|
recording_meta: {
|
||||||
|
...originalRobot.recording_meta,
|
||||||
|
id: uuid(),
|
||||||
|
name: `${originalRobot.recording_meta.name} (${lastWord})`,
|
||||||
|
createdAt: currentTimestamp,
|
||||||
|
updatedAt: currentTimestamp,
|
||||||
|
},
|
||||||
|
recording: { ...originalRobot.recording, workflow },
|
||||||
|
google_sheet_email: null,
|
||||||
|
google_sheet_name: null,
|
||||||
|
google_sheet_id: null,
|
||||||
|
google_access_token: null,
|
||||||
|
google_refresh_token: null,
|
||||||
|
schedule: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log('info', `Robot with ID ${id} duplicated successfully as ${newRobot.id}.`);
|
||||||
|
|
||||||
|
return res.status(201).json({
|
||||||
|
message: 'Robot duplicated and target URL updated successfully.',
|
||||||
|
robot: newRobot,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
logger.log('error', `Error duplicating robot with ID ${req.params.id}: ${error.message}`);
|
||||||
|
return res.status(500).json({ error: error.message });
|
||||||
|
} else {
|
||||||
|
logger.log('error', `Unknown error duplicating robot with ID ${req.params.id}`);
|
||||||
|
return res.status(500).json({ error: 'An unknown error occurred.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE endpoint for deleting a recording from the storage.
|
* DELETE endpoint for deleting a recording from the storage.
|
||||||
|
|||||||
@@ -33,6 +33,22 @@ export const updateRecording = async (id: string, data: { name?: string; limit?:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const duplicateRecording = async (id: string, targetUrl: string): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${apiUrl}/storage/recordings/${id}/duplicate`, {
|
||||||
|
targetUrl,
|
||||||
|
});
|
||||||
|
if (response.status === 201) {
|
||||||
|
return response.data; // Returns the duplicated robot details
|
||||||
|
} else {
|
||||||
|
throw new Error(`Couldn't duplicate recording with id ${id}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error duplicating recording: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getStoredRuns = async (): Promise<string[] | null> => {
|
export const getStoredRuns = async (): Promise<string[] | null> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${apiUrl}/storage/runs`);
|
const response = await axios.get(`${apiUrl}/storage/runs`);
|
||||||
|
|||||||
@@ -91,9 +91,10 @@ interface RecordingsTableProps {
|
|||||||
handleIntegrateRecording: (id: string, fileName: string, params: string[]) => void;
|
handleIntegrateRecording: (id: string, fileName: string, params: string[]) => void;
|
||||||
handleSettingsRecording: (id: string, fileName: string, params: string[]) => void;
|
handleSettingsRecording: (id: string, fileName: string, params: string[]) => void;
|
||||||
handleEditRobot: (id: string, name: string, params: string[]) => void;
|
handleEditRobot: (id: string, name: string, params: string[]) => void;
|
||||||
|
handleDuplicateRobot: (id: string, name: string, params: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot }: RecordingsTableProps) => {
|
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => {
|
||||||
const [page, setPage] = React.useState(0);
|
const [page, setPage] = React.useState(0);
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||||
const [rows, setRows] = React.useState<Data[]>([]);
|
const [rows, setRows] = React.useState<Data[]>([]);
|
||||||
@@ -264,8 +265,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
handleDuplicate={() => {
|
handleDuplicate={() => {
|
||||||
notify('info', 'Duplicating recording...');
|
handleDuplicateRobot(row.id, row.name, row.params || []);
|
||||||
// Implement duplication logic here
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
164
src/components/molecules/RobotDuplicate.tsx
Normal file
164
src/components/molecules/RobotDuplicate.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { TextField, Typography, Box, Button, Chip } from "@mui/material";
|
||||||
|
import { modalStyle } from "./AddWhereCondModal";
|
||||||
|
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||||
|
import { duplicateRecording, getStoredRecording } from '../../api/storage';
|
||||||
|
import { WhereWhatPair } from 'maxun-core';
|
||||||
|
import { getUserById } from "../../api/auth";
|
||||||
|
|
||||||
|
interface RobotMeta {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
pairs: number;
|
||||||
|
updatedAt: string;
|
||||||
|
params: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RobotWorkflow {
|
||||||
|
workflow: WhereWhatPair[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduleConfig {
|
||||||
|
runEvery: number;
|
||||||
|
runEveryUnit: 'MINUTES' | 'HOURS' | 'DAYS' | 'WEEKS' | 'MONTHS';
|
||||||
|
startFrom: 'SUNDAY' | 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY';
|
||||||
|
atTimeStart?: string;
|
||||||
|
atTimeEnd?: string;
|
||||||
|
timezone: string;
|
||||||
|
lastRunAt?: Date;
|
||||||
|
nextRunAt?: Date;
|
||||||
|
cronExpression?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RobotSettings {
|
||||||
|
id: string;
|
||||||
|
userId?: number;
|
||||||
|
recording_meta: RobotMeta;
|
||||||
|
recording: RobotWorkflow;
|
||||||
|
google_sheet_email?: string | null;
|
||||||
|
google_sheet_name?: string | null;
|
||||||
|
google_sheet_id?: string | null;
|
||||||
|
google_access_token?: string | null;
|
||||||
|
google_refresh_token?: string | null;
|
||||||
|
schedule?: ScheduleConfig | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RobotSettingsProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleStart: (settings: RobotSettings) => void;
|
||||||
|
handleClose: () => void;
|
||||||
|
initialSettings?: RobotSettings | null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||||
|
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||||
|
const [targetUrl, setTargetUrl] = useState<string | undefined>('');
|
||||||
|
const { recordingId, notify } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
getRobot();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update the targetUrl when the robot data is loaded
|
||||||
|
if (robot) {
|
||||||
|
const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
|
||||||
|
const url = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
|
||||||
|
setTargetUrl(url);
|
||||||
|
}
|
||||||
|
}, [robot]);
|
||||||
|
|
||||||
|
const getRobot = async () => {
|
||||||
|
if (recordingId) {
|
||||||
|
const robot = await getStoredRecording(recordingId);
|
||||||
|
setRobot(robot);
|
||||||
|
} else {
|
||||||
|
notify('error', 'Could not find robot details. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
|
||||||
|
|
||||||
|
// // Find the `goto` action in `what` and retrieve its arguments
|
||||||
|
// const targetUrl = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
|
||||||
|
|
||||||
|
const handleTargetUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTargetUrl(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!robot || !targetUrl) {
|
||||||
|
notify('error', 'Target URL is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("handle save");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await duplicateRecording(robot.recording_meta.id, targetUrl);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
notify('success', 'Target URL updated successfully.');
|
||||||
|
handleStart(robot); // Inform parent about the updated robot
|
||||||
|
handleClose(); // Close the modal
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
notify('error', 'Failed to update the Target URL. Please try again.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notify('error', 'An error occurred while updating the Target URL.');
|
||||||
|
console.error('Error updating Target URL:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
modalStyle={modalStyle}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Typography variant="h5" style={{ marginBottom: '20px' }}>Duplicate Robot</Typography>
|
||||||
|
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{
|
||||||
|
robot && (
|
||||||
|
<>
|
||||||
|
<span>Robot duplication is useful to extract data from pages with the same structure.</span>
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
Example: If you've created a robot for <code>producthunt.com/topics/api</code>, you can duplicate it to scrape similar pages
|
||||||
|
like <code>producthunt.com/topics/database</code> without training a robot from scratch.
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
<b>⚠️ Ensure the new page has the same structure as the original page.</b>
|
||||||
|
</span>
|
||||||
|
<TextField
|
||||||
|
label="Robot Target URL"
|
||||||
|
key="Robot Target URL"
|
||||||
|
value={targetUrl}
|
||||||
|
onChange={handleTargetUrlChange}
|
||||||
|
style={{ marginBottom: '20px', marginTop: '30px' }}
|
||||||
|
/>
|
||||||
|
<Box mt={2} display="flex" justifyContent="flex-end" onClick={handleSave}>
|
||||||
|
<Button variant="contained" color="primary">
|
||||||
|
Duplicate Robot
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
</GenericModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -54,10 +54,11 @@ interface RobotSettingsProps {
|
|||||||
handleStart: (settings: RobotSettings) => void;
|
handleStart: (settings: RobotSettings) => void;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
initialSettings?: RobotSettings | null;
|
initialSettings?: RobotSettings | null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||||
|
console.log("robot edit");
|
||||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||||
const { recordingId, notify } = useGlobalInfoStore();
|
const { recordingId, notify } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ScheduleSettings, ScheduleSettingsModal } from "../molecules/ScheduleSe
|
|||||||
import { IntegrationSettings, IntegrationSettingsModal } from "../molecules/IntegrationSettings";
|
import { IntegrationSettings, IntegrationSettingsModal } from "../molecules/IntegrationSettings";
|
||||||
import { RobotSettings, RobotSettingsModal } from "../molecules/RobotSettings";
|
import { RobotSettings, RobotSettingsModal } from "../molecules/RobotSettings";
|
||||||
import { RobotEditModal } from '../molecules/RobotEdit';
|
import { RobotEditModal } from '../molecules/RobotEdit';
|
||||||
|
import { RobotDuplicationModal } from '../molecules/RobotDuplicate';
|
||||||
|
|
||||||
interface RecordingsProps {
|
interface RecordingsProps {
|
||||||
handleEditRecording: (id: string, fileName: string) => void;
|
handleEditRecording: (id: string, fileName: string) => void;
|
||||||
@@ -20,11 +21,13 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
|||||||
const [integrateSettingsAreOpen, setIntegrateSettingsAreOpen] = useState(false);
|
const [integrateSettingsAreOpen, setIntegrateSettingsAreOpen] = useState(false);
|
||||||
const [robotSettingsAreOpen, setRobotSettingsAreOpen] = useState(false);
|
const [robotSettingsAreOpen, setRobotSettingsAreOpen] = useState(false);
|
||||||
const [robotEditAreOpen, setRobotEditAreOpen] = useState(false);
|
const [robotEditAreOpen, setRobotEditAreOpen] = useState(false);
|
||||||
|
const [robotDuplicateAreOpen, setRobotDuplicateAreOpen] = useState(false);
|
||||||
const [params, setParams] = useState<string[]>([]);
|
const [params, setParams] = useState<string[]>([]);
|
||||||
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
|
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
|
||||||
const handleIntegrateRecording = (id: string, settings: IntegrationSettings) => {};
|
const handleIntegrateRecording = (id: string, settings: IntegrationSettings) => {};
|
||||||
const handleSettingsRecording = (id: string, settings: RobotSettings) => {};
|
const handleSettingsRecording = (id: string, settings: RobotSettings) => {};
|
||||||
const handleEditRobot = (id: string, settings: RobotSettings) => {};
|
const handleEditRobot = (id: string, settings: RobotSettings) => {};
|
||||||
|
const handleDuplicateRobot = (id: string, settings: RobotSettings) => {};
|
||||||
|
|
||||||
const handleSettingsAndIntegrate = (id: string, name: string, params: string[]) => {
|
const handleSettingsAndIntegrate = (id: string, name: string, params: string[]) => {
|
||||||
if (params.length === 0) {
|
if (params.length === 0) {
|
||||||
@@ -91,6 +94,19 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDuplicateRobotOption = (id: string, name: string, params: string[]) => {
|
||||||
|
if (params.length === 0) {
|
||||||
|
setRobotDuplicateAreOpen(true);
|
||||||
|
setRecordingInfo(id, name);
|
||||||
|
setSelectedRecordingId(id);
|
||||||
|
} else {
|
||||||
|
setParams(params);
|
||||||
|
setRobotDuplicateAreOpen(true);
|
||||||
|
setRecordingInfo(id, name);
|
||||||
|
setSelectedRecordingId(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setParams([]);
|
setParams([]);
|
||||||
setRunSettingsAreOpen(false);
|
setRunSettingsAreOpen(false);
|
||||||
@@ -126,6 +142,13 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
|||||||
setSelectedRecordingId('');
|
setSelectedRecordingId('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRobotDuplicateClose = () => {
|
||||||
|
setParams([]);
|
||||||
|
setRobotDuplicateAreOpen(false);
|
||||||
|
setRecordingInfo('', '');
|
||||||
|
setSelectedRecordingId('');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<RunSettingsModal isOpen={runSettingsAreOpen}
|
<RunSettingsModal isOpen={runSettingsAreOpen}
|
||||||
@@ -150,6 +173,10 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
|||||||
handleClose={handleRobotEditClose}
|
handleClose={handleRobotEditClose}
|
||||||
handleStart={(settings) => handleEditRobot(selectedRecordingId,settings)}
|
handleStart={(settings) => handleEditRobot(selectedRecordingId,settings)}
|
||||||
/>
|
/>
|
||||||
|
<RobotDuplicationModal isOpen={robotDuplicateAreOpen}
|
||||||
|
handleClose={handleRobotDuplicateClose}
|
||||||
|
handleStart={(settings) => handleDuplicateRobot(selectedRecordingId, settings)}
|
||||||
|
/>
|
||||||
<Grid container direction="column" sx={{ padding: '30px' }}>
|
<Grid container direction="column" sx={{ padding: '30px' }}>
|
||||||
<Grid item xs>
|
<Grid item xs>
|
||||||
<RecordingsTable
|
<RecordingsTable
|
||||||
@@ -159,6 +186,7 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
|||||||
handleIntegrateRecording={handleSettingsAndIntegrate}
|
handleIntegrateRecording={handleSettingsAndIntegrate}
|
||||||
handleSettingsRecording={handleRobotSettings}
|
handleSettingsRecording={handleRobotSettings}
|
||||||
handleEditRobot={handleEditRobotOption}
|
handleEditRobot={handleEditRobotOption}
|
||||||
|
handleDuplicateRobot={handleDuplicateRobotOption}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useReducer, createContext, useEffect } from 'react';
|
import { useReducer, createContext, useEffect } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { jwtDecode } from 'jwt-decode';
|
|
||||||
import { apiUrl } from "../apiConfig";
|
import { apiUrl } from "../apiConfig";
|
||||||
|
|
||||||
interface AuthProviderProps {
|
interface AuthProviderProps {
|
||||||
@@ -51,74 +50,28 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
axios.defaults.withCredentials = true;
|
axios.defaults.withCredentials = true;
|
||||||
|
|
||||||
const logoutUser = () => {
|
|
||||||
dispatch({ type: 'LOGOUT' });
|
|
||||||
window.localStorage.removeItem('user');
|
|
||||||
window.localStorage.removeItem('logoutTimeout');
|
|
||||||
navigate('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkTokenExpiration = (token: string) => {
|
|
||||||
if (!token) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decodedToken: any = jwtDecode(token);
|
|
||||||
const currentTime = Date.now();
|
|
||||||
const tokenExpiryTime = decodedToken.exp * 1000;
|
|
||||||
|
|
||||||
if (tokenExpiryTime > currentTime) {
|
|
||||||
// Calculate remaining time until token expires
|
|
||||||
const remainingTime = tokenExpiryTime - currentTime;
|
|
||||||
|
|
||||||
// Check if a logout timeout already exists in local storage
|
|
||||||
const existingTimeout = window.localStorage.getItem('logoutTimeout');
|
|
||||||
|
|
||||||
if (!existingTimeout) {
|
|
||||||
// Set a timeout for auto-logout
|
|
||||||
const timeoutId = setTimeout(logoutUser, remainingTime);
|
|
||||||
window.localStorage.setItem('logoutTimeout', JSON.stringify(timeoutId));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logoutUser(); // Immediately logout if token is expired
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error decoding token:", error);
|
|
||||||
logoutUser();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = window.localStorage.getItem('user');
|
const storedUser = window.localStorage.getItem('user');
|
||||||
if (storedUser) {
|
if (storedUser) {
|
||||||
const userData = JSON.parse(storedUser);
|
dispatch({ type: 'LOGIN', payload: JSON.parse(storedUser) });
|
||||||
dispatch({ type: 'LOGIN', payload: userData });
|
|
||||||
|
|
||||||
// Run expiration check only if a token exists
|
|
||||||
if (userData.token) {
|
|
||||||
checkTokenExpiration(userData.token);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Clean up timeout on component unmount
|
}, []);
|
||||||
return () => {
|
|
||||||
const timeoutId = window.localStorage.getItem('logoutTimeout');
|
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(JSON.parse(timeoutId));
|
|
||||||
window.localStorage.removeItem('logoutTimeout');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []); // Only run this effect once on mount
|
|
||||||
|
|
||||||
axios.interceptors.response.use(
|
axios.interceptors.response.use(
|
||||||
(response) => response,
|
function (response) {
|
||||||
(error) => {
|
return response;
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
const res = error.response;
|
const res = error.response;
|
||||||
if (res?.status === 401 && !res.config.__isRetryRequest) {
|
if (res.status === 401 && res.config && !res.config.__isRetryRequest) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios
|
axios
|
||||||
.get(`${apiUrl}/auth/logout`)
|
.get(`${apiUrl}/auth/logout`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('/401 error > logout');
|
console.log('/401 error > logout');
|
||||||
logoutUser();
|
dispatch({ type: 'LOGOUT' });
|
||||||
|
window.localStorage.removeItem('user');
|
||||||
|
navigate('/login');
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('AXIOS INTERCEPTORS ERROR:', err);
|
console.error('AXIOS INTERCEPTORS ERROR:', err);
|
||||||
@@ -129,7 +82,7 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ state, dispatch }}>
|
<AuthContext.Provider value={{ state, dispatch }}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ a {
|
|||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
|
color: #ff00c3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#browser-actions {
|
#browser-actions {
|
||||||
@@ -53,9 +54,6 @@ code {
|
|||||||
transform-origin: top left; /* Keep the position fixed */
|
transform-origin: top left; /* Keep the position fixed */
|
||||||
}
|
}
|
||||||
|
|
||||||
#browser {
|
|
||||||
}
|
|
||||||
|
|
||||||
#browser-window {
|
#browser-window {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user