diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index afea8e476..c0cb67c46 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -15,6 +15,23 @@ import { arrayToObject } from './utils/utils'; import Concurrency from './utils/concurrency'; import Preprocessor from './preprocessor'; import log, { Level } from './utils/logger'; +import { RegexableString } from './types/workflow'; + +interface Cookie { + name: string; + path: string; + value: string; + domain: string; + secure: boolean; + expires: number; + httpOnly: boolean; + sameSite: "Strict" | "Lax" | "None"; +} + +interface CookieData { + cookies: Cookie[]; + lastUpdated: number; +} /** * Extending the Window interface for custom scraping functions. @@ -68,8 +85,16 @@ export default class Interpreter extends EventEmitter { private cumulativeResults: Record[] = []; - constructor(workflow: WorkflowFile, options?: Partial) { + private cookies: CookieData = { + cookies: [], + lastUpdated: 0 + }; + + private loginSuccessful: boolean = false; + + constructor(workflow: WorkflowFile, options?: Partial, cookies?: CookieData) { super(); + this.cookies = cookies || { cookies: [], lastUpdated: 0 }; this.workflow = workflow.workflow; this.initializedWorkflow = null; this.options = { @@ -109,6 +134,45 @@ export default class Interpreter extends EventEmitter { }) } + private checkCookieExpiry(cookie: Cookie): boolean { + if (cookie.expires === -1) { + return true; + } + + const currentTimestamp = Math.floor(Date.now() / 1000); + + const expiryTimestamp = cookie.expires > 1e10 ? + Math.floor(cookie.expires / 1000) : + cookie.expires; + + return expiryTimestamp > currentTimestamp; + } + + private filterValidCookies(): void { + if (!this.cookies?.cookies) return; + + const originalCookies = [...this.cookies.cookies]; + this.cookies.cookies = this.cookies.cookies.filter(cookie => this.checkCookieExpiry(cookie)); + + const removedCount = originalCookies.length - this.cookies.cookies.length; + if (removedCount > 0) { + this.log(`Filtered out ${removedCount} expired cookies`, Level.LOG); + } + } + + private async applyStoredCookies(page: Page): Promise { + if (!this.cookies?.cookies || this.cookies.cookies.length === 0) return false; + + try { + await page.context().addCookies(this.cookies.cookies); + + return true; + } catch (error) { + this.log(`Failed to apply cookies: ${error}`, Level.ERROR); + return false; + } + } + private async applyAdBlocker(page: Page): Promise { if (this.blocker) { await this.blocker.enableBlockingInPage(page); @@ -121,6 +185,42 @@ export default class Interpreter extends EventEmitter { } } + private isLoginUrl(url: string): boolean { + const loginKeywords = ['login', 'signin', 'sign-in', 'auth']; + const lowercaseUrl = url.toLowerCase(); + return loginKeywords.some(keyword => lowercaseUrl.includes(keyword)); + } + + private getUrlString(url: RegexableString | undefined): string { + if (!url) return ''; + + if (typeof url === 'string') return url; + + if ('$regex' in url) { + let normalUrl = url['$regex']; + return normalUrl + .replace(/^\^/, '') + .replace(/\$$/, '') + .replace(/\\([?])/g, '$1'); + } + + return ''; +} + + private findFirstPostLoginAction(workflow: Workflow): number { + for (let i = workflow.length - 1; i >= 0; i--) { + const action = workflow[i]; + if (action.where.url && action.where.url !== "about:blank") { + const urlString = this.getUrlString(action.where.url); + if (!this.isLoginUrl(urlString)) { + return i; + } + } + } + return -1; + } + + // private getSelectors(workflow: Workflow, actionId: number): string[] { // const selectors: string[] = []; @@ -217,14 +317,33 @@ export default class Interpreter extends EventEmitter { const action = workflowCopy[workflowCopy.length - 1]; - // console.log("Next action:", action) - let url: any = page.url(); if (action && action.where.url !== url && action.where.url !== "about:blank") { url = action.where.url; } + if (this.loginSuccessful) { + const pageCookies = await page.context().cookies([page.url()]); + + this.cookies.cookies = pageCookies.map(cookie => ({ + name: cookie.name, + path: cookie.path || '/', + value: cookie.value, + domain: cookie.domain, + secure: cookie.secure || false, + expires: cookie.expires || Math.floor(Date.now() / 1000) + 86400, + httpOnly: cookie.httpOnly || false, + sameSite: cookie.sameSite || 'Lax' + })); + + Object.assign(this.cookies, { lastUpdated: Date.now() }); + + // this.filterValidCookies(); + this.loginSuccessful = false; + this.log('Stored authentication cookies after successful login', Level.LOG); + } + return { url, cookies: (await page.context().cookies([page.url()])) @@ -654,6 +773,35 @@ export default class Interpreter extends EventEmitter { let actionId = -1 let repeatCount = 0; + // this.filterValidCookies(); + + if (this.cookies?.cookies?.length > 0) { + const cookiesApplied = await this.applyStoredCookies(p); + if (cookiesApplied) { + console.log("Cookies applied successfully."); + const postLoginActionId = this.findFirstPostLoginAction(workflowCopy); + if (postLoginActionId !== -1) { + const targetUrl = this.getUrlString(workflowCopy[postLoginActionId].where.url); + if (targetUrl) { + try { + await p.goto(targetUrl); + + await p.waitForLoadState('networkidle'); + + if (!this.isLoginUrl(targetUrl)) { + workflowCopy.splice(postLoginActionId + 1); + this.log('Successfully skipped login using stored cookies', Level.LOG); + } else { + this.log('Cookie authentication failed, proceeding with manual login', Level.LOG); + } + } catch (error) { + this.log(`Failed to navigate with stored cookies: ${error}`, Level.ERROR); + } + } + } + } + } + /** * Enables the interpreter functionality for popup windows. * User-requested concurrency should be entirely managed by the concurrency manager, @@ -679,11 +827,9 @@ export default class Interpreter extends EventEmitter { } let pageState = {}; - let getStateTest = "Hello"; try { pageState = await this.getState(p, workflowCopy, selectors); selectors = []; - console.log("Empty selectors:", selectors) } catch (e: any) { this.log('The browser has been closed.'); return; @@ -707,28 +853,30 @@ export default class Interpreter extends EventEmitter { const action = workflowCopy[actionId]; - console.log("MATCHED ACTION:", action); - console.log("MATCHED ACTION ID:", actionId); this.log(`Matched ${JSON.stringify(action?.where)}`, Level.LOG); - if (action) { // action is matched + if (action) { if (this.options.debugChannel?.activeId) { this.options.debugChannel.activeId(actionId); } repeatCount = action === lastAction ? repeatCount + 1 : 0; - console.log("REPEAT COUNT", repeatCount); if (this.options.maxRepeats && repeatCount > this.options.maxRepeats) { return; } lastAction = action; try { - console.log("Carrying out:", action.what); await this.carryOutSteps(p, action.what); usedActions.push(action.id ?? 'undefined'); + const url = this.getUrlString(action.where.url); + + if (this.isLoginUrl(url)) { + this.loginSuccessful = true; + } + workflowCopy.splice(actionId, 1); console.log(`Action with ID ${action.id} removed from the workflow copy.`); @@ -764,7 +912,7 @@ export default class Interpreter extends EventEmitter { * @param {ParamType} params Workflow specific, set of parameters * for the `{$param: nameofparam}` fields. */ - public async run(page: Page, params?: ParamType): Promise { + public async run(page: Page, params?: ParamType): Promise { this.log('Starting the workflow.', Level.LOG); const context = page.context(); @@ -798,6 +946,8 @@ export default class Interpreter extends EventEmitter { await this.concurrency.waitForCompletion(); this.stopper = null; + + return this.cookies; } public async stop(): Promise { diff --git a/maxun-core/src/preprocessor.ts b/maxun-core/src/preprocessor.ts index 7957c06b4..81aa31d0e 100644 --- a/maxun-core/src/preprocessor.ts +++ b/maxun-core/src/preprocessor.ts @@ -168,11 +168,11 @@ export default class Preprocessor { ); } - workflowCopy = initSpecialRecurse( - workflowCopy, - '$regex', - (regex) => new RegExp(regex), - ); + // workflowCopy = initSpecialRecurse( + // workflowCopy, + // '$regex', + // (regex) => new RegExp(regex), + // ); return workflowCopy; } diff --git a/maxun-core/src/types/workflow.ts b/maxun-core/src/types/workflow.ts index f7cf180d7..8e72c1ced 100644 --- a/maxun-core/src/types/workflow.ts +++ b/maxun-core/src/types/workflow.ts @@ -11,7 +11,7 @@ export type Meta = typeof meta[number]; export type SelectorArray = string[]; -type RegexableString = string | { '$regex': string }; +export type RegexableString = string | { '$regex': string }; type BaseConditions = { 'url': RegexableString, diff --git a/server/src/api/record.ts b/server/src/api/record.ts index 05560487d..d174a52a5 100644 --- a/server/src/api/record.ts +++ b/server/src/api/record.ts @@ -580,7 +580,7 @@ async function executeRun(id: string) { const workflow = AddGeneratedFlags(recording.recording); const interpretationInfo = await browser.interpreter.InterpretRecording( - workflow, currentPage, (newPage: Page) => currentPage = newPage, plainRun.interpreterSettings + workflow, currentPage, (newPage: Page) => currentPage = newPage, plainRun.interpreterSettings, plainRun.robotMetaId ); const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); diff --git a/server/src/models/Robot.ts b/server/src/models/Robot.ts index 3b2717d64..e66ff3fec 100644 --- a/server/src/models/Robot.ts +++ b/server/src/models/Robot.ts @@ -15,6 +15,22 @@ interface RobotWorkflow { workflow: WhereWhatPair[]; } +interface StoredCookie { + name: string; + value: string; + domain: string; + path: string; + expires: number; + secure?: boolean; + sameSite?: "Strict" | "Lax" | "None"; + httpOnly?: boolean; +} + +interface CookieStorage { + cookies: StoredCookie[]; + lastUpdated: number; +} + interface RobotAttributes { id: string; userId?: number; @@ -26,6 +42,8 @@ interface RobotAttributes { google_access_token?: string | null; google_refresh_token?: string | null; schedule?: ScheduleConfig | null; + isLogin: boolean; + cookie_storage?: CookieStorage | null; } interface ScheduleConfig { @@ -54,6 +72,8 @@ class Robot extends Model implements R public google_access_token!: string | null; public google_refresh_token!: string | null; public schedule!: ScheduleConfig | null; + public isLogin!: boolean; + public cookie_storage!: CookieStorage | null; } Robot.init( @@ -99,6 +119,15 @@ Robot.init( type: DataTypes.JSONB, allowNull: true, }, + isLogin: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + cookie_storage: { + type: DataTypes.JSONB, + allowNull: true, + } }, { sequelize, diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index ddadf240e..f97d7dce9 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -20,6 +20,7 @@ import { capture } from "../utils/analytics"; import { tryCatch } from 'bullmq'; import { WorkflowFile } from 'maxun-core'; import { Page } from 'playwright'; +import StoredCookie from "../models/Robot"; chromium.use(stealthPlugin()); export const router = Router(); @@ -91,6 +92,97 @@ router.get(('/recordings/:id/runs'), requireSignIn, async (req, res) => { } }) +/** + * GET endpoint for retrieving cookies for a specific robot. + * Returns the cookie storage object containing all cookies and their metadata. + */ +router.get('/recordings/:id/cookies', async (req, res) => { + try { + const robot = await Robot.findOne({ + where: { 'recording_meta.id': req.params.id }, + attributes: ['cookie_storage'], + raw: true + }); + + if (!robot) { + return res.status(404).json({ + error: 'Robot not found' + }); + } + + return res.json({ + cookies: robot.cookie_storage + }); + } catch (error) { + logger.log('error', `Error retrieving cookies for robot ${req.params.id}: ${error}`); + return res.status(500).json({ + error: 'Failed to retrieve cookies' + }); + } +}); + +/** + * PUT endpoint for updating cookies for a specific robot. + * Expects a cookie storage object in the request body. + */ +router.put('/recordings/:id/cookies', async (req, res) => { + try { + const robot = await Robot.findOne({ + where: { 'recording_meta.id': req.params.id } + }); + + if (!robot) { + return res.status(404).json({ + error: 'Robot not found' + }); + } + + await robot.update({ + cookie_storage: req.body.cookies + }); + + return res.json({ + message: 'Cookie storage updated successfully' + }); + + } catch (error) { + logger.log('error', `Error updating cookies for robot ${req.params.id}: ${error}`); + return res.status(500).json({ + error: 'Failed to update cookies' + }); + } +}); + +/** + * GET endpoint for retrieving the isLogin status of a specific robot. + * Returns a boolean indicating if the robot is logged in. + */ +router.get('/recordings/:id/login-status', requireSignIn, async (req, res) => { + try { + const robot = await Robot.findOne({ + where: { 'recording_meta.id': req.params.id }, + attributes: ['isLogin'], + raw: true + }); + + if (!robot) { + return res.status(404).json({ + error: 'Robot not found' + }); + } + + return res.json({ + isLogin: robot.isLogin + }); + + } catch (error) { + logger.log('error', `Error retrieving login status for robot ${req.params.id}: ${error}`); + return res.status(500).json({ + error: 'Failed to retrieve login status' + }); + } +}); + function formatRunResponse(run: any) { const formattedRun = { id: run.id, @@ -253,6 +345,7 @@ router.post('/recordings/:id/duplicate', requireSignIn, async (req: Authenticate google_sheet_id: null, google_access_token: null, google_refresh_token: null, + isLogin: false, schedule: null, }); @@ -460,7 +553,7 @@ router.post('/runs/run/:id', requireSignIn, async (req: AuthenticatedRequest, re if (browser && currentPage) { const workflow = AddGeneratedFlags(recording.recording); const interpretationInfo = await browser.interpreter.InterpretRecording( - workflow, currentPage, (newPage: Page) => currentPage = newPage, plainRun.interpreterSettings); + workflow, currentPage, (newPage: Page) => currentPage = newPage, plainRun.interpreterSettings, plainRun.robotMetaId); const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput(run, interpretationInfo.binaryOutput); await destroyRemoteBrowser(plainRun.browserId); diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index c9dc3385b..4be58fad6 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -129,9 +129,9 @@ export class WorkflowGenerator { */ private registerEventHandlers = (socket: Socket) => { socket.on('save', (data) => { - const { fileName, userId } = data; + const { fileName, userId, isLogin } = data; logger.log('debug', `Saving workflow ${fileName} for user ID ${userId}`); - this.saveNewWorkflow(fileName, userId); + this.saveNewWorkflow(fileName, userId, isLogin); }); socket.on('new-recording', () => this.workflowRecord = { workflow: [], @@ -498,7 +498,7 @@ export class WorkflowGenerator { * @param fileName The name of the file. * @returns {Promise} */ - public saveNewWorkflow = async (fileName: string, userId: number) => { + public saveNewWorkflow = async (fileName: string, userId: number, isLogin: boolean) => { const recording = this.optimizeWorkflow(this.workflowRecord); try { this.recordingMeta = { @@ -513,6 +513,7 @@ export class WorkflowGenerator { userId, recording_meta: this.recordingMeta, recording: recording, + isLogin: isLogin, }); capture( 'maxun-oss-robot-created', diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts index b982b172a..96b74d378 100644 --- a/server/src/workflow-management/classes/Interpreter.ts +++ b/server/src/workflow-management/classes/Interpreter.ts @@ -4,6 +4,23 @@ import { Socket } from "socket.io"; import { Page } from "playwright"; import { InterpreterSettings } from "../../types"; import { decrypt } from "../../utils/auth"; +import { default as axios } from "axios"; + +interface Cookie { + name: string; + path: string; + value: string; + domain: string; + secure: boolean; + expires: number; + httpOnly: boolean; + sameSite: "Strict" | "Lax" | "None"; +} + +interface CookieData { + cookies: Cookie[]; + lastUpdated: number; +} /** * Decrypts any encrypted inputs in the workflow. @@ -238,6 +255,40 @@ export class WorkflowInterpreter { this.binaryData = []; } + private async fetchLoginStatus(robotId: string): Promise { + try { + const response = await axios.get(`http://localhost:8080/storage/recordings/${robotId}/login-status`); + this.debugMessages.push(`Successfully fetched login status for robot ${robotId}`); + console.log("LOGIN STATUSS: ", response.data.isLogin); + return response.data.isLogin; + } catch (error) { + this.debugMessages.push(`Failed to fetch login status for robot ${robotId}: ${error}`); + } + } + + private async fetchCookies(robotId: string): Promise { + try { + const response = await axios.get(`http://localhost:8080/storage/recordings/${robotId}/cookies`); + + this.debugMessages.push(`Successfully fetched ${response.data.cookies.length} cookies for robot ${robotId}`); + + return response.data.cookies; + } catch (error) { + this.debugMessages.push(`Failed to fetch cookies for robot ${robotId}: ${error}`); + } + } + + private async setCookies(robotId: string, cookies: CookieData): Promise { + try { + await axios.put(`http://localhost:8080/storage/recordings/${robotId}/cookies`, { + cookies + }); + this.debugMessages.push(`Successfully stored ${cookies.cookies.length} cookies for robot ${robotId}`); + } catch (error) { + this.debugMessages.push(`Failed to store cookies for robot ${robotId}: ${error}`); + } + } + /** * Interprets the recording as a run. * @param workflow The workflow to interpret. @@ -248,8 +299,17 @@ export class WorkflowInterpreter { workflow: WorkflowFile, page: Page, updatePageOnPause: (page: Page) => void, - settings: InterpreterSettings + settings: InterpreterSettings, + robotId?: string ) => { + let cookies: CookieData = { cookies: [], lastUpdated: 0 }; + + if (robotId) { + if (await this.fetchLoginStatus(robotId)) { + cookies = await this.fetchCookies(robotId); + } + } + const params = settings.params ? settings.params : null; delete settings.params; @@ -277,7 +337,7 @@ export class WorkflowInterpreter { } } - const interpreter = new Interpreter(decryptedWorkflow, options); + const interpreter = new Interpreter(decryptedWorkflow, options, cookies); this.interpreter = interpreter; interpreter.on('flag', async (page, resume) => { @@ -297,7 +357,11 @@ export class WorkflowInterpreter { } }); - const status = await interpreter.run(page, params); + const cookie = await interpreter.run(page, params); + + if (robotId && cookie) { + await this.setCookies(robotId, cookie); + } const lastArray = this.serializableData.length > 1 ? [this.serializableData[this.serializableData.length - 1]] @@ -305,7 +369,7 @@ export class WorkflowInterpreter { const result = { log: this.debugMessages, - result: status, + result: cookie, serializableOutput: lastArray.reduce((reducedObject, item, index) => { return { [`item-${index}`]: item, diff --git a/server/src/workflow-management/scheduler/index.ts b/server/src/workflow-management/scheduler/index.ts index 169b0061c..2c915df13 100644 --- a/server/src/workflow-management/scheduler/index.ts +++ b/server/src/workflow-management/scheduler/index.ts @@ -126,7 +126,7 @@ async function executeRun(id: string) { const workflow = AddGeneratedFlags(recording.recording); const interpretationInfo = await browser.interpreter.InterpretRecording( - workflow, currentPage, (newPage: Page) => currentPage = newPage, plainRun.interpreterSettings + workflow, currentPage, (newPage: Page) => currentPage = newPage, plainRun.interpreterSettings, plainRun.robotMetaId ); const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); diff --git a/src/components/molecules/RecordingsTable.tsx b/src/components/molecules/RecordingsTable.tsx index 651d3677f..dd1f600ed 100644 --- a/src/components/molecules/RecordingsTable.tsx +++ b/src/components/molecules/RecordingsTable.tsx @@ -10,7 +10,7 @@ import TableRow from '@mui/material/TableRow'; import { useEffect } from "react"; import { WorkflowFile } from "maxun-core"; import SearchIcon from '@mui/icons-material/Search'; -import { IconButton, Button, Box, Typography, TextField, MenuItem, Menu, ListItemIcon, ListItemText } from "@mui/material"; +import { IconButton, Button, Box, Typography, TextField, MenuItem, Menu, ListItemIcon, ListItemText, FormControl, FormLabel, RadioGroup, FormControlLabel, Radio } from "@mui/material"; import { Schedule, DeleteForever, Edit, PlayCircle, Settings, Power, ContentCopy, MoreHoriz } from "@mui/icons-material"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage"; @@ -82,7 +82,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl const [isModalOpen, setModalOpen] = React.useState(false); const [searchTerm, setSearchTerm] = React.useState(''); - const { notify, setRecordings, browserId, setBrowserId, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore(); + const { notify, setRecordings, browserId, setBrowserId, recordingUrl, setRecordingUrl, isLogin, setIsLogin, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore(); const navigate = useNavigate(); const handleChangePage = (event: unknown, newPage: number) => { @@ -306,14 +306,30 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl onChange={(e: any) => setRecordingUrl(e.target.value)} style={{ marginBottom: '20px', marginTop: '20px' }} /> - + + Does this site require login? + + setIsLogin(e.target.value === 'yes')} + > + } label="Yes" /> + } label="No" /> + + + +
+ +
diff --git a/src/components/molecules/SaveRecording.tsx b/src/components/molecules/SaveRecording.tsx index cfebc867b..f5fa2c4b5 100644 --- a/src/components/molecules/SaveRecording.tsx +++ b/src/components/molecules/SaveRecording.tsx @@ -21,7 +21,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => { const [recordingName, setRecordingName] = useState(fileName); const [waitingForSave, setWaitingForSave] = useState(false); - const { browserId, setBrowserId, notify, recordings } = useGlobalInfoStore(); + const { browserId, setBrowserId, notify, recordings, isLogin } = useGlobalInfoStore(); const { socket } = useSocketStore(); const { state, dispatch } = useContext(AuthContext); const { user } = state; @@ -58,7 +58,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => { // releases resources and changes the view for main page by clearing the global browserId const saveRecording = async () => { if (user) { - const payload = { fileName: recordingName, userId: user.id }; + const payload = { fileName: recordingName, userId: user.id, isLogin: isLogin }; socket?.emit('save', payload); setWaitingForSave(true); console.log(`Saving the recording as ${recordingName} for userId ${user.id}`); diff --git a/src/context/globalInfo.tsx b/src/context/globalInfo.tsx index 58589c3a1..2594735ce 100644 --- a/src/context/globalInfo.tsx +++ b/src/context/globalInfo.tsx @@ -22,6 +22,8 @@ interface GlobalInfo { setRecordingName: (recordingName: string) => void; recordingUrl: string; setRecordingUrl: (recordingUrl: string) => void; + isLogin: boolean; + setIsLogin: (isLogin: boolean) => void; currentWorkflowActionsState: { hasScrapeListAction: boolean; hasScreenshotAction: boolean; @@ -48,6 +50,7 @@ class GlobalInfoStore implements Partial { rerenderRuns = false; recordingName = ''; recordingUrl = 'https://'; + isLogin = false; currentWorkflowActionsState = { hasScrapeListAction: false, hasScreenshotAction: false, @@ -70,6 +73,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { const [recordingId, setRecordingId] = useState(globalInfoStore.recordingId); const [recordingName, setRecordingName] = useState(globalInfoStore.recordingName); const [recordingUrl, setRecordingUrl] = useState(globalInfoStore.recordingUrl); + const [isLogin, setIsLogin] = useState(globalInfoStore.isLogin); const [currentWorkflowActionsState, setCurrentWorkflowActionsState] = useState(globalInfoStore.currentWorkflowActionsState); const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => { @@ -109,6 +113,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { setRecordingName, recordingUrl, setRecordingUrl, + isLogin, + setIsLogin, currentWorkflowActionsState, setCurrentWorkflowActionsState, }}