From 3c0f7900a4e05fa19693af87645a61e572bb153e Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 17:41:59 +0530 Subject: [PATCH 01/56] fix: register type actions correctly --- .../src/workflow-management/classes/Generator.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 6e36f287..a148e53b 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -22,7 +22,7 @@ import { getBestSelectorForAction } from "../utils"; import { browserPool } from "../../server"; import { uuid } from "uuidv4"; import { capture } from "../../utils/analytics" -import { encrypt } from "../../utils/auth"; +import { decrypt, encrypt } from "../../utils/auth"; interface PersistedGeneratedData { lastUsedSelector: string; @@ -1062,14 +1062,18 @@ export class WorkflowGenerator { if (condition.args && condition.args[1]) { if (!input.selector) { input.selector = condition.args[0]; + input.type = condition.args[2] } + if (input.selector === condition.args[0]) { input.actionCounter++; - if (condition.args[1].length === 1) { - input.value = input.value + condition.args[1]; - } else if (condition.args[1] === 'Backspace') { + const decryptedKey = decrypt(condition.args[1]); + + if (decryptedKey.length === 1) { + input.value += decryptedKey; + } else if (decryptedKey === 'Backspace') { input.value = input.value.slice(0, -1); - } else if (condition.args[1] !== 'Shift') { + } else if (decryptedKey !== 'Shift') { pushTheOptimizedAction(pair, index); pair.what.splice(index + 1, 0, { action: 'waitForLoadState', @@ -1081,7 +1085,7 @@ export class WorkflowGenerator { pushTheOptimizedAction(pair, index); input = { selector: condition.args[0], - value: condition.args[1], + value: decrypt(condition.args[1]), type: condition.args[2], actionCounter: 1, }; From 80356b5600b9900aff71e839b67d13dbd34655d6 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 3 Feb 2025 17:29:47 +0530 Subject: [PATCH 02/56] feat: add cursor position on click action --- .../workflow-management/classes/Generator.ts | 176 +++++++++++++----- 1 file changed, 128 insertions(+), 48 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index a148e53b..edb19b2a 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -429,25 +429,38 @@ export class WorkflowGenerator { if ((elementInfo?.tagName === 'INPUT' || elementInfo?.tagName === 'TEXTAREA') && selector) { // Calculate the exact position within the element - const elementPos = await page.evaluate((selector) => { + const positionAndCursor = await page.evaluate((selector) => { const element = document.querySelector(selector); if (!element) return null; + + // Get element position for the relative click coordinates const rect = element.getBoundingClientRect(); + + // Get cursor position index + let cursorIndex = 0; + if ('selectionStart' in element) { + // selectionStart gives us the exact index where the cursor is + cursorIndex = (element as HTMLInputElement | HTMLTextAreaElement).selectionStart || 0; + } + return { + rect: { x: rect.left, y: rect.top + }, + cursorIndex }; }, selector); - if (elementPos) { - const relativeX = coordinates.x - elementPos.x; - const relativeY = coordinates.y - elementPos.y; + if (positionAndCursor) { + const relativeX = coordinates.x - positionAndCursor.rect.x; + const relativeY = coordinates.y - positionAndCursor.rect.y; const pair: WhereWhatPair = { where, what: [{ action: 'click', - args: [selector, { position: { x: relativeX, y: relativeY } }] + args: [selector, { position: { x: relativeX, y: relativeY } }, { cursorIndex: positionAndCursor.cursorIndex }], }] }; @@ -1027,24 +1040,24 @@ export class WorkflowGenerator { * @param workflow The workflow to be optimized. */ private optimizeWorkflow = (workflow: WorkflowFile) => { - - // replace a sequence of press actions by a single fill action + // Enhanced input state to include cursor position let input = { selector: '', value: '', type: '', actionCounter: 0, + cursorPosition: -1 // Track cursor position, -1 means end of text }; - + const pushTheOptimizedAction = (pair: WhereWhatPair, index: number) => { if (input.value.length === 1) { - // when only one press action is present, keep it and add a waitForLoadState action + // Single character - keep as is with waitForLoadState pair.what.splice(index + 1, 0, { action: 'waitForLoadState', args: ['networkidle'], - }) + }); } else { - // when more than one press action is present, add a type action + // Multiple characters - optimize to type action pair.what.splice(index - input.actionCounter, input.actionCounter, { action: 'type', args: [input.selector, encrypt(input.value), input.type], @@ -1053,55 +1066,122 @@ export class WorkflowGenerator { args: ['networkidle'], }); } - } - - + }; + for (const pair of workflow.workflow) { - pair.what.forEach((condition, index) => { - if (condition.action === 'press') { - if (condition.args && condition.args[1]) { - if (!input.selector) { - input.selector = condition.args[0]; - input.type = condition.args[2] + for (let i = 0; i < pair.what.length; i++) { + const condition = pair.what[i]; + + // Handle click actions that set cursor position + if (condition.action === 'click' && condition.args?.[1]) { + const cursorIndex = condition.args[1].cursorIndex; + + // If we have pending input, commit it before processing the click + if (input.value.length > 0) { + pushTheOptimizedAction(pair, i); + input = { + selector: '', + value: '', + type: '', + actionCounter: 0, + cursorPosition: -1 + }; + } + + // Update cursor position for next operations + input.cursorPosition = cursorIndex; + continue; + } + + // Handle text input and editing + if (condition.action === 'press' && condition.args?.[1]) { + const [selector, encryptedKey, type] = condition.args; + const key = decrypt(encryptedKey); + + // Initialize new input state if selector changes + if (!input.selector || input.selector !== selector) { + if (input.value.length > 0) { + pushTheOptimizedAction(pair, i); } - - if (input.selector === condition.args[0]) { - input.actionCounter++; - const decryptedKey = decrypt(condition.args[1]); - - if (decryptedKey.length === 1) { - input.value += decryptedKey; - } else if (decryptedKey === 'Backspace') { - input.value = input.value.slice(0, -1); - } else if (decryptedKey !== 'Shift') { - pushTheOptimizedAction(pair, index); - pair.what.splice(index + 1, 0, { - action: 'waitForLoadState', - args: ['networkidle'], - }) - input = { selector: '', value: '', type: '', actionCounter: 0 }; - } + input = { + selector, + value: '', + type: type || 'text', + actionCounter: 0, + cursorPosition: -1 + }; + } + + input.actionCounter++; + + // Handle different key types with cursor awareness + if (key.length === 1) { + // Insert character at cursor position or append if no cursor set + if (input.cursorPosition === -1) { + // No cursor position set, append to end + input.value += key; } else { - pushTheOptimizedAction(pair, index); + // Insert at cursor position + input.value = + input.value.slice(0, input.cursorPosition) + + key + + input.value.slice(input.cursorPosition); + input.cursorPosition++; + } + } else if (key === 'Backspace') { + if (input.cursorPosition > 0) { + // Delete character before cursor + input.value = + input.value.slice(0, input.cursorPosition - 1) + + input.value.slice(input.cursorPosition); + input.cursorPosition--; + } else if (input.cursorPosition === -1 && input.value.length > 0) { + // No cursor position set, delete from end + input.value = input.value.slice(0, -1); + } + } else if (key !== 'Shift') { + // Handle other special keys + if (input.value.length > 0) { + pushTheOptimizedAction(pair, i); input = { - selector: condition.args[0], - value: decrypt(condition.args[1]), - type: condition.args[2], - actionCounter: 1, + selector: '', + value: '', + type: '', + actionCounter: 0, + cursorPosition: -1 }; } } } else { - if (input.value.length !== 0) { - pushTheOptimizedAction(pair, index); - // clear the input - input = { selector: '', value: '', type: '', actionCounter: 0 }; + // Handle non-text actions + if (input.value.length > 0) { + pushTheOptimizedAction(pair, i); + input = { + selector: '', + value: '', + type: '', + actionCounter: 0, + cursorPosition: -1 + }; } } - }); + } + + // Clean up any remaining input state + if (input.value.length > 0) { + pushTheOptimizedAction(pair, pair.what.length); + input = { + selector: '', + value: '', + type: '', + actionCounter: 0, + cursorPosition: -1 + }; + } } + return workflow; - } + }; /** * Returns workflow params from the stored metadata. From 9ff7813ad96d5527a442729fa8482c8b280db367 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 3 Feb 2025 18:13:45 +0530 Subject: [PATCH 03/56] feat: modify logic to gen cursor index --- .../workflow-management/classes/Generator.ts | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index edb19b2a..5980628b 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -429,28 +429,54 @@ export class WorkflowGenerator { if ((elementInfo?.tagName === 'INPUT' || elementInfo?.tagName === 'TEXTAREA') && selector) { // Calculate the exact position within the element - const positionAndCursor = await page.evaluate((selector) => { - const element = document.querySelector(selector); - if (!element) return null; + const positionAndCursor = await page.evaluate( + ({ selector, coords }) => { + const element = document.querySelector(selector); + if (!element) return null; + + const getCursorPosition = (inputElement: HTMLInputElement | HTMLTextAreaElement, clickCoords: Coordinates) => { + const rect = inputElement.getBoundingClientRect(); + const clickX = clickCoords.x - rect.left; + + // Get the input's text content + const text = inputElement.value; + + // Create a temporary element to measure text + const measurer = document.createElement('span'); + measurer.style.font = window.getComputedStyle(inputElement).font; + measurer.style.position = 'absolute'; + measurer.style.whiteSpace = 'pre'; + measurer.style.visibility = 'hidden'; + document.body.appendChild(measurer); + + // Find the position where the click occurred + let position = 0; + for (let i = 0; i <= text.length; i++) { + measurer.textContent = text.slice(0, i); + const width = measurer.getBoundingClientRect().width; + if (width >= clickX) { + position = i; + break; + } + } + + document.body.removeChild(measurer); + return position; + }; - // Get element position for the relative click coordinates - const rect = element.getBoundingClientRect(); - - // Get cursor position index - let cursorIndex = 0; - if ('selectionStart' in element) { - // selectionStart gives us the exact index where the cursor is - cursorIndex = (element as HTMLInputElement | HTMLTextAreaElement).selectionStart || 0; - } - - return { - rect: { - x: rect.left, - y: rect.top - }, - cursorIndex - }; - }, selector); + const rect = element.getBoundingClientRect(); + const cursorIndex = getCursorPosition(element as HTMLInputElement | HTMLTextAreaElement, coords); + + return { + rect: { + x: rect.left, + y: rect.top + }, + cursorIndex + }; + }, + { selector, coords: coordinates } + ); if (positionAndCursor) { const relativeX = coordinates.x - positionAndCursor.rect.x; From 4d0fc3cfceabc61e7230926b092ae7cd2e438264 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 5 Feb 2025 22:31:55 +0530 Subject: [PATCH 04/56] feat: !variant --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 710e46e6..0fc671be 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -220,7 +220,7 @@ export const NavBar: React.FC = ({
{!isRecording ? ( <> - From 6e93fec28e9e813c78010a9d83bbe63ee6c5efa8 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 5 Feb 2025 22:35:19 +0530 Subject: [PATCH 07/56] feat: use icon button --- src/components/dashboard/NavBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 14bbb225..5b6e8338 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -220,13 +220,13 @@ export const NavBar: React.FC = ({
{!isRecording ? ( <> - + Date: Wed, 5 Feb 2025 22:36:30 +0530 Subject: [PATCH 08/56] feat: icon button styles --- src/components/dashboard/NavBar.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 5b6e8338..1a64f71a 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -221,9 +221,11 @@ export const NavBar: React.FC = ({ {!isRecording ? ( <> {t('navbar.upgrade.button')} From 1a5beae97cfe9cf6ce3e939d13a94d40b9219a6a Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 5 Feb 2025 22:38:03 +0530 Subject: [PATCH 09/56] feat: wrap text inside typography body 1 --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 1a64f71a..6bd57a77 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -227,7 +227,7 @@ export const NavBar: React.FC = ({ padding: '8px', marginRight: '10px', }}> - {t('navbar.upgrade.button')} + {t('navbar.upgrade.button')} Date: Wed, 5 Feb 2025 22:38:29 +0530 Subject: [PATCH 10/56] feat: move typographu --- src/components/dashboard/NavBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 6bd57a77..a41c6a42 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -227,7 +227,8 @@ export const NavBar: React.FC = ({ padding: '8px', marginRight: '10px', }}> - {t('navbar.upgrade.button')} + + {t('navbar.upgrade.button')} Date: Wed, 5 Feb 2025 22:38:41 +0530 Subject: [PATCH 11/56] chore: lint --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index a41c6a42..c35fc60d 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -227,7 +227,7 @@ export const NavBar: React.FC = ({ padding: '8px', marginRight: '10px', }}> - + {t('navbar.upgrade.button')} From 5ea92572d8124c7f9954e81b0fe207bfe85e520d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 5 Feb 2025 22:39:51 +0530 Subject: [PATCH 12/56] feat: increase spacing --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index c35fc60d..8a9b1e9c 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -225,7 +225,7 @@ export const NavBar: React.FC = ({ alignItems: 'center', borderRadius: '5px', padding: '8px', - marginRight: '10px', + marginRight: '20px', }}> {t('navbar.upgrade.button')} From f63ff72d4a8a1f393eab69cac86765d50bc564ae Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 7 Feb 2025 17:32:00 +0530 Subject: [PATCH 13/56] feat: add selection start property to get cursor index --- .../workflow-management/classes/Generator.ts | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 5980628b..38237312 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -435,32 +435,33 @@ export class WorkflowGenerator { if (!element) return null; const getCursorPosition = (inputElement: HTMLInputElement | HTMLTextAreaElement, clickCoords: Coordinates) => { - const rect = inputElement.getBoundingClientRect(); - const clickX = clickCoords.x - rect.left; + // const rect = inputElement.getBoundingClientRect(); + // const clickX = clickCoords.x - rect.left; - // Get the input's text content - const text = inputElement.value; + // // Get the input's text content + // const text = inputElement.value; - // Create a temporary element to measure text - const measurer = document.createElement('span'); - measurer.style.font = window.getComputedStyle(inputElement).font; - measurer.style.position = 'absolute'; - measurer.style.whiteSpace = 'pre'; - measurer.style.visibility = 'hidden'; - document.body.appendChild(measurer); + // // Create a temporary element to measure text + // const measurer = document.createElement('span'); + // measurer.style.font = window.getComputedStyle(inputElement).font; + // measurer.style.position = 'absolute'; + // measurer.style.whiteSpace = 'pre'; + // measurer.style.visibility = 'hidden'; + // document.body.appendChild(measurer); - // Find the position where the click occurred - let position = 0; - for (let i = 0; i <= text.length; i++) { - measurer.textContent = text.slice(0, i); - const width = measurer.getBoundingClientRect().width; - if (width >= clickX) { - position = i; - break; - } - } + // // Find the position where the click occurred + // let position = 0; + // for (let i = 0; i <= text.length; i++) { + // measurer.textContent = text.slice(0, i); + // const width = measurer.getBoundingClientRect().width; + // if (width >= clickX) { + // position = i; + // break; + // } + // } - document.body.removeChild(measurer); + // document.body.removeChild(measurer); + const position = inputElement.selectionStart || 0; return position; }; @@ -740,6 +741,23 @@ export class WorkflowGenerator { * @returns {Promise} */ public saveNewWorkflow = async (fileName: string, userId: number, isLogin: boolean) => { + for (const pair of this.workflowRecord.workflow) { + for (let i = 0; i < pair.what.length; i++) { + const condition = pair.what[i]; + + if (condition.action === 'press' && condition.args) { + const [selector, encryptedKey, type] = condition.args; + const key = decrypt(encryptedKey); + + console.log(`Selector: ${selector}, Key: ${key}`); + } + + if (condition.action === 'click' && condition.args) { + console.log("Click args: ", condition.args); + } + } + } + const recording = this.optimizeWorkflow(this.workflowRecord); try { this.recordingMeta = { From 2199149827625be50a20c63428e37dc26db26fcc Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 7 Feb 2025 17:33:12 +0530 Subject: [PATCH 14/56] feat: rm previous cursor logic --- .../workflow-management/classes/Generator.ts | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 38237312..ff36bed9 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -435,32 +435,6 @@ export class WorkflowGenerator { if (!element) return null; const getCursorPosition = (inputElement: HTMLInputElement | HTMLTextAreaElement, clickCoords: Coordinates) => { - // const rect = inputElement.getBoundingClientRect(); - // const clickX = clickCoords.x - rect.left; - - // // Get the input's text content - // const text = inputElement.value; - - // // Create a temporary element to measure text - // const measurer = document.createElement('span'); - // measurer.style.font = window.getComputedStyle(inputElement).font; - // measurer.style.position = 'absolute'; - // measurer.style.whiteSpace = 'pre'; - // measurer.style.visibility = 'hidden'; - // document.body.appendChild(measurer); - - // // Find the position where the click occurred - // let position = 0; - // for (let i = 0; i <= text.length; i++) { - // measurer.textContent = text.slice(0, i); - // const width = measurer.getBoundingClientRect().width; - // if (width >= clickX) { - // position = i; - // break; - // } - // } - - // document.body.removeChild(measurer); const position = inputElement.selectionStart || 0; return position; }; From 0abb78648009aff14d76134b6e90e712f29ea24f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 8 Feb 2025 15:38:39 +0530 Subject: [PATCH 15/56] feat: show icon as per key visibility --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 9d54fe5c..3a085576 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -137,7 +137,7 @@ const ApiKeyManager = () => { setShowKey(!showKey)}> - + {showKey ? : } From ebcd117694b03b76fdff2cd681efd84e40cda42d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 8 Feb 2025 15:44:36 +0530 Subject: [PATCH 16/56] feat: use visibility off icon --- src/components/api/ApiKey.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 3a085576..9a1a492a 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -14,7 +14,7 @@ import { Tooltip, Paper, } from '@mui/material'; -import { ContentCopy, Visibility, Delete } from '@mui/icons-material'; +import { ContentCopy, Visibility, VisibilityOff, Delete } from '@mui/icons-material'; import styled from 'styled-components'; import axios from 'axios'; import { useGlobalInfoStore } from '../../context/globalInfo'; @@ -137,7 +137,7 @@ const ApiKeyManager = () => { setShowKey(!showKey)}> - {showKey ? : } + {showKey ? : } From 32b25837e4ce25e31224fb1a2bd923381d5e9da7 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 8 Feb 2025 15:51:45 +0530 Subject: [PATCH 17/56] fix: use correct icons based on showKey state --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 9a1a492a..f84f7a55 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -137,7 +137,7 @@ const ApiKeyManager = () => { setShowKey(!showKey)}> - {showKey ? : } + {showKey ? : } From fcb75f412fafd767f3b07872d84c4233b3ea9d12 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 14:28:50 +0530 Subject: [PATCH 18/56] feat: notify based on error status --- src/components/api/ApiKey.tsx | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 9d54fe5c..e92bd368 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -43,8 +43,33 @@ const ApiKeyManager = () => { try { const { data } = await axios.get(`${apiUrl}/auth/api-key`); setApiKey(data.api_key); + notify('success', t('apikey.notifications.success.fetch')); } catch (error: any) { - notify('error', t('apikey.notifications.fetch_error', { error: error.message })); + const status = error.response?.status; + let errorKey = 'unknown'; + + switch (status) { + case 401: + errorKey = 'unauthorized'; + break; + case 404: + errorKey = 'not_found'; + break; + case 500: + errorKey = 'server'; + break; + default: + if (error.message?.includes('Network Error')) { + errorKey = 'network'; + } + } + + notify( + 'error', + t(`apikey.notifications.errors.fetch.${errorKey}`, { + error: error.response?.data?.message || error.message + }) + ); } finally { setLoading(false); } From 744778a4cecdfd77977bc0397aa7575a9b0eeb1d Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 14:37:58 +0530 Subject: [PATCH 19/56] feat: return code for better error handling --- server/src/routes/auth.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index b7d884a4..39efbede 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -261,7 +261,11 @@ router.get( const authenticatedReq = req as AuthenticatedRequest; try { if (!authenticatedReq.user) { - return res.status(401).json({ ok: false, error: "Unauthorized" }); + return res.status(401).json({ + ok: false, + error: "Unauthorized", + code: "unauthorized" + }); } const user = await User.findByPk(authenticatedReq.user.id, { @@ -270,15 +274,25 @@ router.get( }); if (!user) { - return res.status(404).json({ message: "User not found" }); + return res.status(404).json({ + ok: false, + error: "User not found", + code: "not_found" + }); } return res.status(200).json({ + ok: true, message: "API key fetched successfully", api_key: user.api_key || null, }); } catch (error) { - return res.status(500).json({ message: "Error fetching API key", error }); + console.error('API Key fetch error:', error); + return res.status(500).json({ + ok: false, + error: "Error fetching API key", + code: "server", + }); } } ); From 85132c3d509e25d2b7a619432f6fe84ec0825a4a Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 15:05:49 +0530 Subject: [PATCH 20/56] feat: add translation for api key notifications --- public/locales/de.json | 41 +++++++++++++++++++++++++++++++++++------ public/locales/en.json | 41 +++++++++++++++++++++++++++++++++++------ public/locales/es.json | 41 +++++++++++++++++++++++++++++++++++------ public/locales/ja.json | 41 +++++++++++++++++++++++++++++++++++------ public/locales/zh.json | 41 +++++++++++++++++++++++++++++++++++------ 5 files changed, 175 insertions(+), 30 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index a4c02b1d..aa25f0ce 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -139,12 +139,41 @@ "no_key_message": "Sie haben noch keinen API-Schlüssel generiert.", "generate_button": "API-Schlüssel generieren", "notifications": { - "fetch_error": "API-Schlüssel konnte nicht abgerufen werden - ${error}", - "generate_success": "API-Schlüssel erfolgreich generiert", - "generate_error": "API-Schlüssel konnte nicht generiert werden - ${error}", - "delete_success": "API-Schlüssel erfolgreich gelöscht", - "delete_error": "API-Schlüssel konnte nicht gelöscht werden - ${error}", - "copy_success": "API-Schlüssel erfolgreich kopiert" + "errors": { + "fetch": { + "network": "Netzwerkfehler beim Abrufen des API-Schlüssels: ${error}", + "unauthorized": "Sie müssen angemeldet sein, um auf den API-Schlüssel zuzugreifen", + "not_found": "API-Schlüssel für Ihr Konto wurde nicht gefunden", + "server": "Serverfehler beim Abrufen des API-Schlüssels. Bitte versuchen Sie es später erneut", + "unknown": "Unbekannter Fehler beim Abrufen des API-Schlüssels: ${error}" + }, + "generate": { + "network": "Netzwerkfehler bei der Generierung des API-Schlüssels: ${error}", + "unauthorized": "Sie müssen angemeldet sein, um einen API-Schlüssel zu generieren", + "key_exists": "Sie haben bereits einen API-Schlüssel. Bitte löschen Sie zuerst den vorhandenen", + "not_found": "Benutzerkonto nicht gefunden", + "server": "Serverfehler bei der Generierung des API-Schlüssels. Bitte versuchen Sie es später erneut", + "unknown": "Unbekannter Fehler bei der Generierung des API-Schlüssels: ${error}" + }, + "delete": { + "network": "Netzwerkfehler beim Löschen des API-Schlüssels: ${error}", + "unauthorized": "Sie müssen angemeldet sein, um den API-Schlüssel zu löschen", + "not_found": "Benutzerkonto nicht gefunden", + "key_not_found": "Kein API-Schlüssel zum Löschen gefunden", + "server": "Serverfehler beim Löschen des API-Schlüssels. Bitte versuchen Sie es später erneut", + "unknown": "Unbekannter Fehler beim Löschen des API-Schlüssels: ${error}" + }, + "copy": { + "failed": "Fehler beim Kopieren des API-Schlüssels in die Zwischenablage", + "no_key": "Kein API-Schlüssel zum Kopieren verfügbar" + } + }, + "success": { + "fetch": "API-Schlüssel erfolgreich abgerufen", + "generate": "Neuer API-Schlüssel erfolgreich generiert", + "delete": "API-Schlüssel erfolgreich gelöscht", + "copy": "API-Schlüssel in die Zwischenablage kopiert" + } } }, "action_description": { diff --git a/public/locales/en.json b/public/locales/en.json index f072dcc9..0cf1cf35 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -140,12 +140,41 @@ "no_key_message": "You haven't generated an API key yet.", "generate_button": "Generate API Key", "notifications": { - "fetch_error": "Failed to fetch API Key - ${error}", - "generate_success": "Generated API Key successfully", - "generate_error": "Failed to generate API Key - ${error}", - "delete_success": "API Key deleted successfully", - "delete_error": "Failed to delete API Key - ${error}", - "copy_success": "Copied API Key successfully" + "errors": { + "fetch": { + "network": "Network error while fetching API key: ${error}", + "unauthorized": "You must be logged in to access API key", + "not_found": "Unable to find API key for your account", + "server": "Server error while fetching API key. Please try again later", + "unknown": "Unknown error occurred while fetching API key: ${error}" + }, + "generate": { + "network": "Network error while generating API key: ${error}", + "unauthorized": "You must be logged in to generate an API key", + "key_exists": "You already have an API key. Please delete the existing one first", + "not_found": "User account not found", + "server": "Server error while generating API key. Please try again later", + "unknown": "Unknown error occurred while generating API key: ${error}" + }, + "delete": { + "network": "Network error while deleting API key: ${error}", + "unauthorized": "You must be logged in to delete API key", + "not_found": "User account not found", + "key_not_found": "No API key found to delete", + "server": "Server error while deleting API key. Please try again later", + "unknown": "Unknown error occurred while deleting API key: ${error}" + }, + "copy": { + "failed": "Failed to copy API key to clipboard", + "no_key": "No API key available to copy" + } + }, + "success": { + "fetch": "API key retrieved successfully", + "generate": "New API key generated successfully", + "delete": "API key deleted successfully", + "copy": "API key copied to clipboard" + } } }, "action_description": { diff --git a/public/locales/es.json b/public/locales/es.json index 09f6d703..fb0c4e54 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -140,12 +140,41 @@ "no_key_message": "Aún no has generado una clave API.", "generate_button": "Generar Clave API", "notifications": { - "fetch_error": "Error al obtener la clave API - ${error}", - "generate_success": "Clave API generada con éxito", - "generate_error": "Error al generar la clave API - ${error}", - "delete_success": "Clave API eliminada con éxito", - "delete_error": "Error al eliminar la clave API - ${error}", - "copy_success": "Clave API copiada con éxito" + "errors": { + "fetch": { + "network": "Error de red al obtener la clave API: ${error}", + "unauthorized": "Debes iniciar sesión para acceder a la clave API", + "not_found": "No se pudo encontrar la clave API para tu cuenta", + "server": "Error del servidor al obtener la clave API. Por favor, inténtalo más tarde", + "unknown": "Error desconocido al obtener la clave API: ${error}" + }, + "generate": { + "network": "Error de red al generar la clave API: ${error}", + "unauthorized": "Debes iniciar sesión para generar una clave API", + "key_exists": "Ya tienes una clave API. Por favor, elimina la existente primero", + "not_found": "Cuenta de usuario no encontrada", + "server": "Error del servidor al generar la clave API. Por favor, inténtalo más tarde", + "unknown": "Error desconocido al generar la clave API: ${error}" + }, + "delete": { + "network": "Error de red al eliminar la clave API: ${error}", + "unauthorized": "Debes iniciar sesión para eliminar la clave API", + "not_found": "Cuenta de usuario no encontrada", + "key_not_found": "No se encontró ninguna clave API para eliminar", + "server": "Error del servidor al eliminar la clave API. Por favor, inténtalo más tarde", + "unknown": "Error desconocido al eliminar la clave API: ${error}" + }, + "copy": { + "failed": "Error al copiar la clave API al portapapeles", + "no_key": "No hay clave API disponible para copiar" + } + }, + "success": { + "fetch": "Clave API obtenida exitosamente", + "generate": "Nueva clave API generada exitosamente", + "delete": "Clave API eliminada exitosamente", + "copy": "Clave API copiada al portapapeles" + } } }, "action_description": { diff --git a/public/locales/ja.json b/public/locales/ja.json index d2fe42ea..9f852836 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -140,12 +140,41 @@ "no_key_message": "APIキーはまだ生成されていません。", "generate_button": "APIキーを生成", "notifications": { - "fetch_error": "APIキーの取得に失敗しました - ${error}", - "generate_success": "APIキーが正常に生成されました", - "generate_error": "APIキーの生成に失敗しました - ${error}", - "delete_success": "APIキーが正常に削除されました", - "delete_error": "APIキーの削除に失敗しました - ${error}", - "copy_success": "APIキーがコピーされました" + "errors": { + "fetch": { + "network": "APIキーの取得中にネットワークエラーが発生しました:${error}", + "unauthorized": "APIキーにアクセスするにはログインが必要です", + "not_found": "アカウントのAPIキーが見つかりません", + "server": "APIキーの取得中にサーバーエラーが発生しました。後でもう一度お試しください", + "unknown": "APIキーの取得中に不明なエラーが発生しました:${error}" + }, + "generate": { + "network": "APIキーの生成中にネットワークエラーが発生しました:${error}", + "unauthorized": "APIキーを生成するにはログインが必要です", + "key_exists": "APIキーが既に存在します。既存のキーを先に削除してください", + "not_found": "ユーザーアカウントが見つかりません", + "server": "APIキーの生成中にサーバーエラーが発生しました。後でもう一度お試しください", + "unknown": "APIキーの生成中に不明なエラーが発生しました:${error}" + }, + "delete": { + "network": "APIキーの削除中にネットワークエラーが発生しました:${error}", + "unauthorized": "APIキーを削除するにはログインが必要です", + "not_found": "ユーザーアカウントが見つかりません", + "key_not_found": "削除するAPIキーが見つかりません", + "server": "APIキーの削除中にサーバーエラーが発生しました。後でもう一度お試しください", + "unknown": "APIキーの削除中に不明なエラーが発生しました:${error}" + }, + "copy": { + "failed": "APIキーのクリップボードへのコピーに失敗しました", + "no_key": "コピーできるAPIキーがありません" + } + }, + "success": { + "fetch": "APIキーの取得に成功しました", + "generate": "新しいAPIキーの生成に成功しました", + "delete": "APIキーの削除に成功しました", + "copy": "APIキーをクリップボードにコピーしました" + } } }, "action_description": { diff --git a/public/locales/zh.json b/public/locales/zh.json index e7c58660..80fec31e 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -140,12 +140,41 @@ "no_key_message": "您还未生成API密钥。", "generate_button": "生成API密钥", "notifications": { - "fetch_error": "获取API密钥失败 - ${error}", - "generate_success": "API密钥生成成功", - "generate_error": "生成API密钥失败 - ${error}", - "delete_success": "API密钥删除成功", - "delete_error": "删除API密钥失败 - ${error}", - "copy_success": "API密钥复制成功" + "errors": { + "fetch": { + "network": "获取API密钥时发生网络错误:${error}", + "unauthorized": "您必须登录才能访问API密钥", + "not_found": "找不到您账户的API密钥", + "server": "获取API密钥时发生服务器错误。请稍后重试", + "unknown": "获取API密钥时发生未知错误:${error}" + }, + "generate": { + "network": "生成API密钥时发生网络错误:${error}", + "unauthorized": "您必须登录才能生成API密钥", + "key_exists": "您已经有一个API密钥。请先删除现有的密钥", + "not_found": "找不到用户账户", + "server": "生成API密钥时发生服务器错误。请稍后重试", + "unknown": "生成API密钥时发生未知错误:${error}" + }, + "delete": { + "network": "删除API密钥时发生网络错误:${error}", + "unauthorized": "您必须登录才能删除API密钥", + "not_found": "找不到用户账户", + "key_not_found": "找不到要删除的API密钥", + "server": "删除API密钥时发生服务器错误。请稍后重试", + "unknown": "删除API密钥时发生未知错误:${error}" + }, + "copy": { + "failed": "复制API密钥到剪贴板失败", + "no_key": "没有可复制的API密钥" + } + }, + "success": { + "fetch": "成功获取API密钥", + "generate": "成功生成新的API密钥", + "delete": "成功删除API密钥", + "copy": "已将API密钥复制到剪贴板" + } } }, "action_description": { From ed5e96b476c8787aecdc1565c95d9bc2045dc7e9 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 15:06:21 +0530 Subject: [PATCH 21/56] feat: better notifications for api key page --- src/components/api/ApiKey.tsx | 81 +++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index e92bd368..5afc6da1 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -83,11 +83,36 @@ const ApiKeyManager = () => { setLoading(true); try { const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`); - setApiKey(data.api_key); - - notify('success', t('apikey.notifications.generate_success')); + if (data.ok && data.api_key) { + setApiKey(data.api_key); + notify('success', t('apikey.notifications.success.generate')); + } } catch (error: any) { - notify('error', t('apikey.notifications.generate_error', { error: error.message })); + const status = error.response?.status; + let errorKey = 'unknown'; + + switch (status) { + case 401: + errorKey = 'unauthorized'; + break; + case 403: + errorKey = 'limit_reached'; + break; + case 500: + errorKey = 'server'; + break; + default: + if (error.message?.includes('Network Error')) { + errorKey = 'network'; + } + } + + notify( + 'error', + t(`apikey.notifications.errors.generate.${errorKey}`, { + error: error.response?.data?.message || error.message + }) + ); } finally { setLoading(false); } @@ -96,22 +121,54 @@ const ApiKeyManager = () => { const deleteApiKey = async () => { setLoading(true); try { - await axios.delete(`${apiUrl}/auth/delete-api-key`); - setApiKey(null); - notify('success', t('apikey.notifications.delete_success')); + const response = await axios.delete(`${apiUrl}/auth/delete-api-key`); + if (response.data.ok) { + setApiKey(null); + notify('success', t('apikey.notifications.success.delete')); + } } catch (error: any) { - notify('error', t('apikey.notifications.delete_error', { error: error.message })); + const status = error.response?.status; + let errorKey = 'unknown'; + + switch (status) { + case 401: + errorKey = 'unauthorized'; + break; + case 404: + errorKey = 'not_found'; + break; + case 500: + errorKey = 'server'; + break; + default: + if (error.message?.includes('Network Error')) { + errorKey = 'network'; + } + } + + notify( + 'error', + t(`apikey.notifications.errors.delete.${errorKey}`, { + error: error.response?.data?.message || error.message + }) + ); } finally { setLoading(false); } }; - const copyToClipboard = () => { - if (apiKey) { - navigator.clipboard.writeText(apiKey); + const copyToClipboard = async () => { + if (!apiKey) return; + + try { + await navigator.clipboard.writeText(apiKey); setCopySuccess(true); + notify('success', t('apikey.notifications.success.copy')); + + // Reset copy success state after 2 seconds setTimeout(() => setCopySuccess(false), 2000); - notify('info', t('apikey.notifications.copy_success')); + } catch (error) { + notify('error', t('apikey.notifications.errors.copy.failed')); } }; From d800158f9980f8369c40a2d328d16f78d1e51a26 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 15:06:47 +0530 Subject: [PATCH 22/56] feat: add error codes for api key routes --- server/src/routes/auth.ts | 98 +++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 39efbede..0980b400 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -220,36 +220,56 @@ router.post( const authenticatedReq = req as AuthenticatedRequest; try { if (!authenticatedReq.user) { - return res.status(401).json({ ok: false, error: "Unauthorized" }); + return res.status(401).json({ + ok: false, + message: "Unauthorized", + code: "unauthorized" + }); } + const user = await User.findByPk(authenticatedReq.user.id, { attributes: { exclude: ["password"] }, }); if (!user) { - return res.status(404).json({ message: "User not found" }); + return res.status(404).json({ + ok: false, + message: "User not found", + code: "not_found" + }); } if (user.api_key) { - return res.status(400).json({ message: "API key already exists" }); + return res.status(400).json({ + ok: false, + message: "API key already exists", + code: "key_exists" + }); } - const apiKey = genAPIKey(); + const apiKey = genAPIKey(); await user.update({ api_key: apiKey }); + // Capture analytics event capture("maxun-oss-api-key-created", { user_id: user.id, created_at: new Date().toISOString(), }); return res.status(200).json({ + ok: true, message: "API key generated successfully", - api_key: apiKey, + api_key: apiKey }); + } catch (error) { - return res - .status(500) - .json({ message: "Error generating API key", error }); + console.error('API Key generation error:', error); + return res.status(500).json({ + ok: false, + message: "Error generating API key", + code: "server", + error: process.env.NODE_ENV === 'development' ? error : undefined + }); } } ); @@ -263,7 +283,7 @@ router.get( if (!authenticatedReq.user) { return res.status(401).json({ ok: false, - error: "Unauthorized", + message: "Unauthorized", code: "unauthorized" }); } @@ -276,7 +296,7 @@ router.get( if (!user) { return res.status(404).json({ ok: false, - error: "User not found", + message: "User not found", code: "not_found" }); } @@ -284,14 +304,16 @@ router.get( return res.status(200).json({ ok: true, message: "API key fetched successfully", - api_key: user.api_key || null, + api_key: user.api_key || null }); + } catch (error) { console.error('API Key fetch error:', error); return res.status(500).json({ ok: false, - error: "Error fetching API key", + message: "Error fetching API key", code: "server", + error: process.env.NODE_ENV === 'development' ? error : undefined }); } } @@ -302,33 +324,59 @@ router.delete( requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; - if (!authenticatedReq.user) { - return res.status(401).send({ error: "Unauthorized" }); - } - try { - const user = await User.findByPk(authenticatedReq.user.id, { raw: true }); + if (!authenticatedReq.user) { + return res.status(401).json({ + ok: false, + message: "Unauthorized", + code: "unauthorized" + }); + } + + const user = await User.findByPk(authenticatedReq.user.id, { + raw: true, + attributes: ["id", "api_key"] + }); if (!user) { - return res.status(404).json({ message: "User not found" }); + return res.status(404).json({ + ok: false, + message: "User not found", + code: "not_found" + }); } if (!user.api_key) { - return res.status(404).json({ message: "API Key not found" }); + return res.status(404).json({ + ok: false, + message: "API Key not found", + code: "key_not_found" + }); } - await User.update({ api_key: null }, { where: { id: authenticatedReq.user.id } }); + await User.update( + { api_key: null }, + { where: { id: authenticatedReq.user.id } } + ); capture("maxun-oss-api-key-deleted", { user_id: user.id, deleted_at: new Date().toISOString(), }); - return res.status(200).json({ message: "API Key deleted successfully" }); - } catch (error: any) { - return res - .status(500) - .json({ message: "Error deleting API key", error: error.message }); + return res.status(200).json({ + ok: true, + message: "API Key deleted successfully" + }); + + } catch (error) { + console.error('API Key deletion error:', error); + return res.status(500).json({ + ok: false, + message: "Error deleting API key", + code: "server", + error: process.env.NODE_ENV === 'development' ? error : undefined + }); } } ); From 14a391e0bf9ba8d5f3a2ca4d5e427f73e64c667d Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Feb 2025 19:14:33 +0530 Subject: [PATCH 23/56] feat: check limit while processing workflow --- server/src/routes/storage.ts | 56 ++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index e159d8cb..399920df 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -25,35 +25,43 @@ chromium.use(stealthPlugin()); export const router = Router(); -export const decryptWorkflowActions = async (workflow: any[],): Promise => { - // Create a deep copy to avoid mutating the original workflow - const processedWorkflow = JSON.parse(JSON.stringify(workflow)); +export const processWorkflowActions = async (workflow: any[], checkLimit: boolean = false): Promise => { + const processedWorkflow = JSON.parse(JSON.stringify(workflow)); - // Process each step in the workflow - for (const step of processedWorkflow) { - if (!step.what) continue; - - // Process each action in the step - for (const action of step.what) { - // Only process type and press actions - if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) { - // The second argument contains the encrypted value - const encryptedValue = action.args[1]; - if (typeof encryptedValue === 'string') { - try { - // Decrypt the value and update the args array - action.args[1] = await decrypt(encryptedValue); - } catch (error) { - console.error('Failed to decrypt value:', error); - // Keep the encrypted value if decryption fails + processedWorkflow.workflow.forEach((pair: any) => { + pair.what.forEach((action: any) => { + // Handle limit validation for scrapeList action + if (action.action === 'scrapeList' && checkLimit && Array.isArray(action.args) && action.args.length > 0) { + const scrapeConfig = action.args[0]; + if (scrapeConfig && typeof scrapeConfig === 'object' && 'limit' in scrapeConfig) { + if (typeof scrapeConfig.limit === 'number' && scrapeConfig.limit > 5) { + scrapeConfig.limit = 5; } } } - } - } + + // Handle decryption for type and press actions + if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) { + try { + const encryptedValue = action.args[1]; + if (typeof encryptedValue === 'string') { + const decryptedValue = decrypt(encryptedValue); + action.args[1] = decryptedValue; + } else { + logger.log('error', 'Encrypted value is not a string'); + action.args[1] = ''; + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.log('error', `Failed to decrypt input value: ${errorMessage}`); + action.args[1] = ''; + } + } + }); + }); return processedWorkflow; -}; +} /** * Logs information about recordings API. @@ -88,7 +96,7 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => { ); if (data?.recording?.workflow) { - data.recording.workflow = await decryptWorkflowActions( + data.recording.workflow = await processWorkflowActions( data.recording.workflow, ); } From 206d760dd13c2561441326b9a7902e9237bdc209 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Feb 2025 22:00:51 +0530 Subject: [PATCH 24/56] feat: accurately get cursor index value for input field --- .../workflow-management/classes/Generator.ts | 94 ++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index ff36bed9..bd2233ba 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -431,27 +431,95 @@ export class WorkflowGenerator { // Calculate the exact position within the element const positionAndCursor = await page.evaluate( ({ selector, coords }) => { - const element = document.querySelector(selector); - if (!element) return null; + const getCursorPosition = (element: any, clickX: any) => { + // Get the input's text content + const text = element.value; - const getCursorPosition = (inputElement: HTMLInputElement | HTMLTextAreaElement, clickCoords: Coordinates) => { - const position = inputElement.selectionStart || 0; - return position; - }; + // Create a temporary hidden div to measure text + const mirror = document.createElement('div'); + + // Copy ALL relevant styles that could affect text measurement + const style = window.getComputedStyle(element); + mirror.style.cssText = ` + font: ${style.font}; + line-height: ${style.lineHeight}; + padding: ${style.padding}; + border: ${style.border}; + box-sizing: ${style.boxSizing}; + white-space: ${style.whiteSpace}; + overflow-wrap: ${style.overflowWrap}; + position: absolute; + top: -9999px; + left: -9999px; + width: ${element.offsetWidth}px; + `; + + document.body.appendChild(mirror); + + // Get the element's padding and border widths + const paddingLeft = parseFloat(style.paddingLeft); + const borderLeft = parseFloat(style.borderLeftWidth); + + // Adjust clickX to account for padding and border + const adjustedClickX = clickX - (paddingLeft + borderLeft); + + let bestIndex = 0; + let bestDiff = Infinity; + + // Try each possible cursor position + for (let i = 0; i <= text.length; i++) { + // Create a span for the text before cursor + const textBeforeCursor = text.substring(0, i); + const span = document.createElement('span'); + span.textContent = textBeforeCursor; + mirror.innerHTML = ''; + mirror.appendChild(span); + + // Get the x-position where this character would end + const textWidth = span.getBoundingClientRect().width; + + // Calculate distance from adjusted click to this position + const diff = Math.abs(adjustedClickX - textWidth); + + // If this position is closer to the click, update bestIndex + if (diff < bestDiff) { + bestIndex = i; + bestDiff = diff; + } + } + + // Clean up + document.body.removeChild(mirror); + + // Add debug logging + console.log({ + text, + clickX, + adjustedClickX, + bestIndex, + value: text.substring(0, bestIndex), + nextChar: text[bestIndex] || 'EOL' + }); + + return bestIndex; + }; + + const element = document.querySelector(selector) as HTMLInputElement | HTMLTextAreaElement; + if (!element) return null; const rect = element.getBoundingClientRect(); - const cursorIndex = getCursorPosition(element as HTMLInputElement | HTMLTextAreaElement, coords); + const relativeX = coords.x - rect.left; return { - rect: { - x: rect.left, - y: rect.top - }, - cursorIndex + rect: { + x: rect.left, + y: rect.top + }, + cursorIndex: getCursorPosition(element, relativeX) }; }, { selector, coords: coordinates } - ); + ); if (positionAndCursor) { const relativeX = coordinates.x - positionAndCursor.rect.x; From 945f1200c4d02a168e6996e794f74d65a5d04783 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Feb 2025 22:37:40 +0530 Subject: [PATCH 25/56] feat: optimize press actions to type actions --- .../workflow-management/classes/Generator.ts | 201 ++++++++---------- 1 file changed, 86 insertions(+), 115 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index bd2233ba..ff556359 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -42,6 +42,13 @@ interface MetaData { isLogin?: boolean; } +interface InputState { + selector: string; + value: string; + type: string; + cursorPosition: number; +} + /** * Workflow generator is used to transform the user's interactions into an automatically * generated correct workflows, using the ability of internal state persistence and @@ -1126,56 +1133,33 @@ export class WorkflowGenerator { * @param workflow The workflow to be optimized. */ private optimizeWorkflow = (workflow: WorkflowFile) => { - // Enhanced input state to include cursor position - let input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 // Track cursor position, -1 means end of text - }; - - const pushTheOptimizedAction = (pair: WhereWhatPair, index: number) => { - if (input.value.length === 1) { - // Single character - keep as is with waitForLoadState - pair.what.splice(index + 1, 0, { - action: 'waitForLoadState', - args: ['networkidle'], - }); - } else { - // Multiple characters - optimize to type action - pair.what.splice(index - input.actionCounter, input.actionCounter, { - action: 'type', - args: [input.selector, encrypt(input.value), input.type], - }, { - action: 'waitForLoadState', - args: ['networkidle'], - }); - } - }; + // Track state for each input field + const inputStates = new Map(); + // First pass: Process all actions and build final states for (const pair of workflow.workflow) { - for (let i = 0; i < pair.what.length; i++) { - const condition = pair.what[i]; + let currentIndex = 0; + + while (currentIndex < pair.what.length) { + const condition = pair.what[currentIndex]; - // Handle click actions that set cursor position - if (condition.action === 'click' && condition.args?.[1]) { - const cursorIndex = condition.args[1].cursorIndex; + // Handle click actions with cursor positioning + if (condition.action === 'click' && condition.args?.[2]?.cursorIndex !== undefined) { + const selector = condition.args[0]; + const cursorIndex = condition.args[2].cursorIndex; - // If we have pending input, commit it before processing the click - if (input.value.length > 0) { - pushTheOptimizedAction(pair, i); - input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 - }; - } + let state = inputStates.get(selector) || { + selector, + value: '', + type: 'text', + cursorPosition: -1 + }; - // Update cursor position for next operations - input.cursorPosition = cursorIndex; + state.cursorPosition = cursorIndex; + inputStates.set(selector, state); + + // Remove the click action + pair.what.splice(currentIndex, 1); continue; } @@ -1183,86 +1167,73 @@ export class WorkflowGenerator { if (condition.action === 'press' && condition.args?.[1]) { const [selector, encryptedKey, type] = condition.args; const key = decrypt(encryptedKey); + + let state = inputStates.get(selector) || { + selector, + value: '', + type: type || 'text', + cursorPosition: -1 + }; - // Initialize new input state if selector changes - if (!input.selector || input.selector !== selector) { - if (input.value.length > 0) { - pushTheOptimizedAction(pair, i); - } - input = { - selector, - value: '', - type: type || 'text', - actionCounter: 0, - cursorPosition: -1 - }; - } - - input.actionCounter++; - - // Handle different key types with cursor awareness if (key.length === 1) { - // Insert character at cursor position or append if no cursor set - if (input.cursorPosition === -1) { - // No cursor position set, append to end - input.value += key; + if (state.cursorPosition === -1) { + state.value += key; } else { - // Insert at cursor position - input.value = - input.value.slice(0, input.cursorPosition) + + state.value = + state.value.slice(0, state.cursorPosition) + key + - input.value.slice(input.cursorPosition); - input.cursorPosition++; + state.value.slice(state.cursorPosition); + state.cursorPosition++; } } else if (key === 'Backspace') { - if (input.cursorPosition > 0) { - // Delete character before cursor - input.value = - input.value.slice(0, input.cursorPosition - 1) + - input.value.slice(input.cursorPosition); - input.cursorPosition--; - } else if (input.cursorPosition === -1 && input.value.length > 0) { - // No cursor position set, delete from end - input.value = input.value.slice(0, -1); + if (state.cursorPosition > 0) { + state.value = + state.value.slice(0, state.cursorPosition - 1) + + state.value.slice(state.cursorPosition); + state.cursorPosition--; + } else if (state.cursorPosition === -1 && state.value.length > 0) { + state.value = state.value.slice(0, -1); } - } else if (key !== 'Shift') { - // Handle other special keys - if (input.value.length > 0) { - pushTheOptimizedAction(pair, i); - input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 - }; + } else if (key === 'Delete') { + if (state.cursorPosition >= 0 && state.cursorPosition < state.value.length) { + state.value = + state.value.slice(0, state.cursorPosition) + + state.value.slice(state.cursorPosition + 1); + } else if (state.cursorPosition === -1 && state.value.length > 0) { + // If no cursor position set, delete at the end + state.value = state.value.slice(0, -1); } } - } else { - // Handle non-text actions - if (input.value.length > 0) { - pushTheOptimizedAction(pair, i); - input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 - }; - } - } - } - // Clean up any remaining input state - if (input.value.length > 0) { - pushTheOptimizedAction(pair, pair.what.length); - input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 - }; + inputStates.set(selector, state); + + // Remove the press action + pair.what.splice(currentIndex, 1); + continue; + } + + currentIndex++; + } + } + + // Second pass: Add one type action per selector in the last pair where that selector was used + for (const [selector, state] of inputStates.entries()) { + if (state.value) { + // Find the last pair that used this selector + for (let i = workflow.workflow.length - 1; i >= 0; i--) { + const pair = workflow.workflow[i]; + + // Add type action to the end of the pair + pair.what.push({ + action: 'type', + args: [selector, encrypt(state.value), state.type] + }, { + action: 'waitForLoadState', + args: ['networkidle'] + }); + + break; // Stop after adding to the first pair we find + } } } From afe7c2fced6ae75f2d52792b37ebee4af41508fc Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Feb 2025 23:03:49 +0530 Subject: [PATCH 26/56] feat: maintain input type for fields --- .../workflow-management/classes/Generator.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index ff556359..fc732e60 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -1168,12 +1168,18 @@ export class WorkflowGenerator { const [selector, encryptedKey, type] = condition.args; const key = decrypt(encryptedKey); - let state = inputStates.get(selector) || { - selector, - value: '', - type: type || 'text', - cursorPosition: -1 - }; + let state = inputStates.get(selector); + if (!state) { + state = { + selector, + value: '', + type: type || 'text', // Use the type from the press action + cursorPosition: -1 + }; + } else { + // Update type from the press action if it exists + state.type = type || state.type; + } if (key.length === 1) { if (state.cursorPosition === -1) { From 816afa3a813cd63dfaaaf9e916cff4cad8ab7462 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Feb 2025 23:26:56 +0530 Subject: [PATCH 27/56] fix: login title en --- public/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en.json b/public/locales/en.json index f072dcc9..919af547 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -50,7 +50,7 @@ "new":"Create Robot", "modal":{ "title":"Enter the URL", - "login_title": "Does this website require login?", + "login_title": "Does this website require logging in?", "label":"URL", "button":"Start Recording" }, From 10ea965cef123a9cdd8332ecbd255b89c4934864 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Feb 2025 23:30:39 +0530 Subject: [PATCH 28/56] fix: login title german --- public/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/de.json b/public/locales/de.json index a4c02b1d..1ff41fba 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -50,7 +50,7 @@ "new": "Roboter erstellen", "modal": { "title": "Geben Sie die URL ein", - "login_title": "Erfordert diese Seite eine Anmeldung?", + "login_title": "Ist für diese Website eine Anmeldung erforderlich?", "label": "URL", "button": "Aufnahme starten" }, From 3316cf2ee4c9515f6a44c285f745df3d3459fda7 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Feb 2025 23:32:09 +0530 Subject: [PATCH 29/56] fix: login title spanish --- public/locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/es.json b/public/locales/es.json index 09f6d703..cd79d2cd 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -50,7 +50,7 @@ "new": "Crear Robot", "modal": { "title": "Ingresa la URL", - "login_title": "¿Este sitio requiere inicio de sesión?", + "login_title": "¿Este sitio web requiere iniciar sesión?", "label": "URL", "button": "Comenzar grabación" }, From a4b1a6fdb144c07381b805a3fa27ba6cc7ea0e05 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 13 Feb 2025 04:21:08 +0530 Subject: [PATCH 30/56] chore: cleanup --- server/src/routes/auth.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index b7d884a4..47eced63 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -17,7 +17,6 @@ router.post("/register", async (req, res) => { try { const { email, password } = req.body; - // Validation checks with translation codes if (!email) { return res.status(400).json({ error: "VALIDATION_ERROR", @@ -32,7 +31,6 @@ router.post("/register", async (req, res) => { }); } - // Check if user exists let userExist = await User.findOne({ raw: true, where: { email } }); if (userExist) { return res.status(400).json({ @@ -43,7 +41,6 @@ router.post("/register", async (req, res) => { const hashedPassword = await hashPassword(password); - // Create user let user: any; try { user = await User.create({ email, password: hashedPassword }); @@ -55,7 +52,6 @@ router.post("/register", async (req, res) => { }); } - // Check JWT secret if (!process.env.JWT_SECRET) { console.log("JWT_SECRET is not defined in the environment"); return res.status(500).json({ @@ -64,7 +60,6 @@ router.post("/register", async (req, res) => { }); } - // Success path const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET as string); user.password = undefined as unknown as string; res.cookie("token", token, { @@ -124,7 +119,6 @@ router.post("/login", async (req, res) => { const token = jwt.sign({ id: user?.id }, process.env.JWT_SECRET as string); - // return user and token to client, exclude hashed password if (user) { user.password = undefined as unknown as string; } From 6a989ad0e431804aa42d5d9e1bc25f44890820c4 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 11:14:38 +0530 Subject: [PATCH 31/56] feat: extract type action creds --- src/components/robot/RobotEdit.tsx | 64 ++++++------------------------ 1 file changed, 12 insertions(+), 52 deletions(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index f1f79b77..12fd32bd 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -118,6 +118,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin useEffect(() => { if (robot?.recording?.workflow) { const extractedCredentials = extractInitialCredentials(robot.recording.workflow); + console.log(extractedCredentials); setCredentials(extractedCredentials); setCredentialGroups(groupCredentialsByType(extractedCredentials)); } @@ -126,74 +127,32 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const extractInitialCredentials = (workflow: any[]): Credentials => { const credentials: Credentials = {}; - // Helper function to check if a character is printable - const isPrintableCharacter = (char: string): boolean => { - return char.length === 1 && !!char.match(/^[\x20-\x7E]$/); - }; - // Process each step in the workflow workflow.forEach(step => { if (!step.what) return; - // Keep track of the current input field being processed - let currentSelector = ''; - let currentValue = ''; - let currentType = ''; - // Process actions in sequence to maintain correct text state step.what.forEach((action: any) => { if ( - (action.action === 'type' || action.action === 'press') && + action.action === 'type' && action.args?.length >= 2 && typeof action.args[1] === 'string' ) { const selector: string = action.args[0]; - const character: string = action.args[1]; - const inputType: string = action.args[2] || ''; + const value: string = action.args[1]; + const type: string = action.args[2]; - // Detect `input[type="password"]` - if (!currentType && inputType.toLowerCase() === 'password') { - currentType = 'password'; + if (!credentials[selector]) { + credentials[selector] = { + value: '', + type: '' + }; } - // If we're dealing with a new selector, store the previous one - if (currentSelector && selector !== currentSelector) { - if (!credentials[currentSelector]) { - credentials[currentSelector] = { - value: currentValue, - type: currentType - }; - } else { - credentials[currentSelector].value = currentValue; - } - } - - // Update current tracking variables - if (selector !== currentSelector) { - currentSelector = selector; - currentValue = credentials[selector]?.value || ''; - currentType = inputType || credentials[selector]?.type || ''; - } - - // Handle different types of key actions - if (character === 'Backspace') { - // Remove the last character when backspace is pressed - currentValue = currentValue.slice(0, -1); - } else if (isPrintableCharacter(character)) { - // Add the character to the current value - currentValue += character; - } - // Note: We ignore other special keys like 'Shift', 'Enter', etc. + credentials[selector].value = value; + credentials[selector].type = type; } }); - - // Store the final state of the last processed selector - if (currentSelector) { - credentials[currentSelector] = { - value: currentValue, - type: currentType - }; - } }); return credentials; @@ -224,6 +183,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const getRobot = async () => { if (recordingId) { const robot = await getStoredRecording(recordingId); + console.log("ROBOT:", robot); setRobot(robot); } else { notify('error', t('robot_edit.notifications.update_failed')); From 4c21e2794a4e87e8b6e91dd97f827064c62ca526 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 11:15:51 +0530 Subject: [PATCH 32/56] feat: console log cleanup --- src/components/robot/RobotEdit.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index 12fd32bd..b4b271bc 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -183,7 +183,6 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const getRobot = async () => { if (recordingId) { const robot = await getStoredRecording(recordingId); - console.log("ROBOT:", robot); setRobot(robot); } else { notify('error', t('robot_edit.notifications.update_failed')); From b7ce40c8af0b8f7f942b02b4c05f6c214b9a08aa Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 11:58:02 +0530 Subject: [PATCH 33/56] feat: update workflow type actions, update robot --- server/src/routes/storage.ts | 69 +++++++++++++++--------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 01f1ca6b..aa63798f 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -167,51 +167,26 @@ function updateTypeActionsInWorkflow(workflow: any[], credentials: Credentials) return workflow.map(step => { if (!step.what) return step; - const indicesToRemove = new Set(); - step.what.forEach((action: any, index: number) => { - if (!action.action || !action.args?.[0]) return; - - if ((action.action === 'type' || action.action === 'press') && credentials[action.args[0]]) { - indicesToRemove.add(index); - - if (step.what[index + 1]?.action === 'waitForLoadState') { - indicesToRemove.add(index + 1); - } - } - }); - - const filteredWhat = step.what.filter((_: any, index: number) => !indicesToRemove.has(index)); - - Object.entries(credentials).forEach(([selector, credentialInfo]) => { - const clickIndex = filteredWhat.findIndex((action: any) => - action.action === 'click' && action.args?.[0] === selector - ); - - if (clickIndex !== -1) { - const chars = credentialInfo.value.split(''); + step.what = step.what.map((action: any) => { + if (action.action === 'type' && action.args?.length >= 2) { + const selector = action.args[0]; - chars.forEach((char, i) => { - filteredWhat.splice(clickIndex + 1 + (i * 2), 0, { - action: 'type', + if (credentials[selector]) { + return { + ...action, args: [ selector, - encrypt(char), - credentialInfo.type + encrypt(credentials[selector].value), + credentials[selector].type ] - }); - - filteredWhat.splice(clickIndex + 2 + (i * 2), 0, { - action: 'waitForLoadState', - args: ['networkidle'] - }); - }); + }; + } } + + return action; }); - return { - ...step, - what: filteredWhat - }; + return step; }); } @@ -281,9 +256,23 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r } } - robot.set('recording', { ...robot.recording, workflow }); + const updates: any = { + recording: { + ...robot.recording, + workflow + } + }; - await robot.save(); + if (name) { + updates.recording_meta = { + ...robot.recording_meta, + name + }; + } + + await Robot.update(updates, { + where: { 'recording_meta.id': id } + }); const updatedRobot = await Robot.findOne({ where: { 'recording_meta.id': id } }); From 81b917b3976545c3cafe4143eb76ac8b29db4a1a Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 12:05:02 +0530 Subject: [PATCH 34/56] feat: console log cleanup --- src/components/robot/RobotEdit.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index b4b271bc..86fc5974 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -118,7 +118,6 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin useEffect(() => { if (robot?.recording?.workflow) { const extractedCredentials = extractInitialCredentials(robot.recording.workflow); - console.log(extractedCredentials); setCredentials(extractedCredentials); setCredentialGroups(groupCredentialsByType(extractedCredentials)); } From 8369714a152d8cb989669cd1f8fd00de3fe53cd5 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 12:08:33 +0530 Subject: [PATCH 35/56] feat: comments cleanup --- .../workflow-management/classes/Generator.ts | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index fc732e60..a5d92d4e 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -435,17 +435,13 @@ export class WorkflowGenerator { } if ((elementInfo?.tagName === 'INPUT' || elementInfo?.tagName === 'TEXTAREA') && selector) { - // Calculate the exact position within the element const positionAndCursor = await page.evaluate( ({ selector, coords }) => { const getCursorPosition = (element: any, clickX: any) => { - // Get the input's text content const text = element.value; - // Create a temporary hidden div to measure text const mirror = document.createElement('div'); - // Copy ALL relevant styles that could affect text measurement const style = window.getComputedStyle(element); mirror.style.cssText = ` font: ${style.font}; @@ -463,51 +459,33 @@ export class WorkflowGenerator { document.body.appendChild(mirror); - // Get the element's padding and border widths const paddingLeft = parseFloat(style.paddingLeft); const borderLeft = parseFloat(style.borderLeftWidth); - // Adjust clickX to account for padding and border const adjustedClickX = clickX - (paddingLeft + borderLeft); let bestIndex = 0; let bestDiff = Infinity; - // Try each possible cursor position for (let i = 0; i <= text.length; i++) { - // Create a span for the text before cursor const textBeforeCursor = text.substring(0, i); const span = document.createElement('span'); span.textContent = textBeforeCursor; mirror.innerHTML = ''; mirror.appendChild(span); - // Get the x-position where this character would end const textWidth = span.getBoundingClientRect().width; - // Calculate distance from adjusted click to this position const diff = Math.abs(adjustedClickX - textWidth); - // If this position is closer to the click, update bestIndex if (diff < bestDiff) { bestIndex = i; bestDiff = diff; } } - // Clean up document.body.removeChild(mirror); - // Add debug logging - console.log({ - text, - clickX, - adjustedClickX, - bestIndex, - value: text.substring(0, bestIndex), - nextChar: text[bestIndex] || 'EOL' - }); - return bestIndex; }; @@ -1133,17 +1111,14 @@ export class WorkflowGenerator { * @param workflow The workflow to be optimized. */ private optimizeWorkflow = (workflow: WorkflowFile) => { - // Track state for each input field const inputStates = new Map(); - // First pass: Process all actions and build final states for (const pair of workflow.workflow) { let currentIndex = 0; while (currentIndex < pair.what.length) { const condition = pair.what[currentIndex]; - // Handle click actions with cursor positioning if (condition.action === 'click' && condition.args?.[2]?.cursorIndex !== undefined) { const selector = condition.args[0]; const cursorIndex = condition.args[2].cursorIndex; @@ -1158,12 +1133,10 @@ export class WorkflowGenerator { state.cursorPosition = cursorIndex; inputStates.set(selector, state); - // Remove the click action pair.what.splice(currentIndex, 1); continue; } - // Handle text input and editing if (condition.action === 'press' && condition.args?.[1]) { const [selector, encryptedKey, type] = condition.args; const key = decrypt(encryptedKey); @@ -1173,11 +1146,10 @@ export class WorkflowGenerator { state = { selector, value: '', - type: type || 'text', // Use the type from the press action + type: type || 'text', cursorPosition: -1 }; } else { - // Update type from the press action if it exists state.type = type || state.type; } @@ -1206,14 +1178,12 @@ export class WorkflowGenerator { state.value.slice(0, state.cursorPosition) + state.value.slice(state.cursorPosition + 1); } else if (state.cursorPosition === -1 && state.value.length > 0) { - // If no cursor position set, delete at the end state.value = state.value.slice(0, -1); } } inputStates.set(selector, state); - // Remove the press action pair.what.splice(currentIndex, 1); continue; } @@ -1222,14 +1192,11 @@ export class WorkflowGenerator { } } - // Second pass: Add one type action per selector in the last pair where that selector was used for (const [selector, state] of inputStates.entries()) { if (state.value) { - // Find the last pair that used this selector for (let i = workflow.workflow.length - 1; i >= 0; i--) { const pair = workflow.workflow[i]; - // Add type action to the end of the pair pair.what.push({ action: 'type', args: [selector, encrypt(state.value), state.type] @@ -1238,7 +1205,7 @@ export class WorkflowGenerator { args: ['networkidle'] }); - break; // Stop after adding to the first pair we find + break; } } } From 5207d7da6f90b0d67130825dafedc0fec6c3764f Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 12:11:04 +0530 Subject: [PATCH 36/56] feat: default to text if type not specified --- src/components/robot/RobotEdit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index 86fc5974..1f76c725 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -139,7 +139,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin ) { const selector: string = action.args[0]; const value: string = action.args[1]; - const type: string = action.args[2]; + const type: string = action.args[2] || 'text'; if (!credentials[selector]) { credentials[selector] = { From cd574194b0e4a5ad7b0667262abb57e007bf5151 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 12:23:58 +0530 Subject: [PATCH 37/56] feat: console log cleanup --- .../workflow-management/classes/Generator.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index a5d92d4e..28b8b361 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -768,23 +768,6 @@ export class WorkflowGenerator { * @returns {Promise} */ public saveNewWorkflow = async (fileName: string, userId: number, isLogin: boolean) => { - for (const pair of this.workflowRecord.workflow) { - for (let i = 0; i < pair.what.length; i++) { - const condition = pair.what[i]; - - if (condition.action === 'press' && condition.args) { - const [selector, encryptedKey, type] = condition.args; - const key = decrypt(encryptedKey); - - console.log(`Selector: ${selector}, Key: ${key}`); - } - - if (condition.action === 'click' && condition.args) { - console.log("Click args: ", condition.args); - } - } - } - const recording = this.optimizeWorkflow(this.workflowRecord); try { this.recordingMeta = { From c6f5479358bd7d47192761ec66105312967133a3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 15:50:13 +0530 Subject: [PATCH 38/56] feat: add support for full and partial word type actions --- src/components/robot/RobotEdit.tsx | 120 +++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 23 deletions(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index 1f76c725..6e8868de 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -123,39 +123,113 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin } }, [robot]); - const extractInitialCredentials = (workflow: any[]): Credentials => { + function extractInitialCredentials(workflow: any[]): Credentials { const credentials: Credentials = {}; - - // Process each step in the workflow + + const isPrintableCharacter = (char: string): boolean => { + return char.length === 1 && !!char.match(/^[\x20-\x7E]$/); + }; + workflow.forEach(step => { if (!step.what) return; - - // Process actions in sequence to maintain correct text state - step.what.forEach((action: any) => { - if ( - action.action === 'type' && - action.args?.length >= 2 && - typeof action.args[1] === 'string' - ) { - const selector: string = action.args[0]; - const value: string = action.args[1]; - const type: string = action.args[2] || 'text'; - + + let currentSelector = ''; + let currentValue = ''; + let currentType = ''; + let i = 0; + + while (i < step.what.length) { + const action = step.what[i]; + + if (!action.action || !action.args?.[0]) { + i++; + continue; + } + + const selector = action.args[0]; + + // Handle full word type actions first + if (action.action === 'type' && + action.args?.length >= 2 && + typeof action.args[1] === 'string' && + action.args[1].length > 1) { + if (!credentials[selector]) { credentials[selector] = { - value: '', - type: '' + value: action.args[1], + type: action.args[2] || 'text' }; } - - credentials[selector].value = value; - credentials[selector].type = type; + i++; + continue; } - }); + + // Handle character-by-character sequences (both type and press) + if ((action.action === 'type' || action.action === 'press') && + action.args?.length >= 2 && + typeof action.args[1] === 'string') { + + if (selector !== currentSelector) { + if (currentSelector && currentValue) { + credentials[currentSelector] = { + value: currentValue, + type: currentType || 'text' + }; + } + currentSelector = selector; + currentValue = credentials[selector]?.value || ''; + currentType = action.args[2] || credentials[selector]?.type || 'text'; + } + + const character = action.args[1]; + + if (isPrintableCharacter(character)) { + currentValue += character; + } else if (character === 'Backspace') { + currentValue = currentValue.slice(0, -1); + } + + if (!currentType && action.args[2]?.toLowerCase() === 'password') { + currentType = 'password'; + } + + let j = i + 1; + while (j < step.what.length) { + const nextAction = step.what[j]; + if (!nextAction.action || !nextAction.args?.[0] || + nextAction.args[0] !== selector || + (nextAction.action !== 'type' && nextAction.action !== 'press')) { + break; + } + if (nextAction.args[1] === 'Backspace') { + currentValue = currentValue.slice(0, -1); + } else if (isPrintableCharacter(nextAction.args[1])) { + currentValue += nextAction.args[1]; + } + j++; + } + + credentials[currentSelector] = { + value: currentValue, + type: currentType + }; + + i = j; + } else { + i++; + } + } + + if (currentSelector && currentValue) { + credentials[currentSelector] = { + value: currentValue, + type: currentType || 'text' + }; + } }); - + return credentials; - }; + } const groupCredentialsByType = (credentials: Credentials): GroupedCredentials => { return Object.entries(credentials).reduce((acc: GroupedCredentials, [selector, info]) => { From ff21f5355b41d4c2f8c0c21b16db499f93f22926 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 16:13:42 +0530 Subject: [PATCH 39/56] feat: fallback to update type actions --- server/src/routes/storage.ts | 99 +++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 18 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index aa63798f..7e5b3638 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -163,30 +163,93 @@ interface Credentials { [key: string]: CredentialInfo; } -function updateTypeActionsInWorkflow(workflow: any[], credentials: Credentials) { +function handleWorkflowActions(workflow: any[], credentials: Credentials) { + // Process one step at a time to avoid memory issues return workflow.map(step => { if (!step.what) return step; - step.what = step.what.map((action: any) => { - if (action.action === 'type' && action.args?.length >= 2) { - const selector = action.args[0]; + // Use a more memory-efficient approach + const newWhat: any[] = []; + const processedSelectors = new Set(); + + for (let i = 0; i < step.what.length; i++) { + const action = step.what[i]; + + // Skip invalid actions + if (!action?.action || !action?.args?.[0]) { + newWhat.push(action); + continue; + } + + const selector = action.args[0]; + const credential = credentials[selector]; + + // Handle non-credential actions + if (!credential) { + newWhat.push(action); + continue; + } + + // Handle credential-related actions + if (action.action === 'click') { + newWhat.push(action); - if (credentials[selector]) { - return { - ...action, - args: [ - selector, - encrypt(credentials[selector].value), - credentials[selector].type - ] - }; + // If we haven't processed this selector and there's a following type/press action + if (!processedSelectors.has(selector) && + i + 1 < step.what.length && + (step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press')) { + + // Add type action + newWhat.push({ + action: 'type', + args: [selector, encrypt(credential.value), credential.type] + }); + + // Add single waitForLoadState + newWhat.push({ + action: 'waitForLoadState', + args: ['networkidle'] + }); + + processedSelectors.add(selector); + + // Skip subsequent type/press/waitForLoadState actions for this selector + while (i + 1 < step.what.length && + (step.what[i + 1].action === 'type' || + step.what[i + 1].action === 'press' || + step.what[i + 1].action === 'waitForLoadState')) { + i++; + } + } + } else if ((action.action === 'type' || action.action === 'press') && + !processedSelectors.has(selector)) { + // Handle standalone type/press action + newWhat.push({ + action: 'type', + args: [selector, encrypt(credential.value), credential.type] + }); + + newWhat.push({ + action: 'waitForLoadState', + args: ['networkidle'] + }); + + processedSelectors.add(selector); + + // Skip subsequent type/press/waitForLoadState actions for this selector + while (i + 1 < step.what.length && + (step.what[i + 1].action === 'type' || + step.what[i + 1].action === 'press' || + step.what[i + 1].action === 'waitForLoadState')) { + i++; } } - - return action; - }); + } - return step; + return { + ...step, + what: newWhat + }; }); } @@ -218,7 +281,7 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r let workflow = [...robot.recording.workflow]; // Create a copy of the workflow if (credentials) { - workflow = updateTypeActionsInWorkflow(workflow, credentials); + workflow = handleWorkflowActions(workflow, credentials); } // Update the limit From 99696aaba2aabef1421a362930c1afd704427eb9 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 16:15:27 +0530 Subject: [PATCH 40/56] feat: comment cleanup --- server/src/routes/storage.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 7e5b3638..ea1fce38 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -164,18 +164,15 @@ interface Credentials { } function handleWorkflowActions(workflow: any[], credentials: Credentials) { - // Process one step at a time to avoid memory issues return workflow.map(step => { if (!step.what) return step; - // Use a more memory-efficient approach const newWhat: any[] = []; const processedSelectors = new Set(); for (let i = 0; i < step.what.length; i++) { const action = step.what[i]; - // Skip invalid actions if (!action?.action || !action?.args?.[0]) { newWhat.push(action); continue; @@ -184,28 +181,23 @@ function handleWorkflowActions(workflow: any[], credentials: Credentials) { const selector = action.args[0]; const credential = credentials[selector]; - // Handle non-credential actions if (!credential) { newWhat.push(action); continue; } - // Handle credential-related actions if (action.action === 'click') { newWhat.push(action); - // If we haven't processed this selector and there's a following type/press action if (!processedSelectors.has(selector) && i + 1 < step.what.length && (step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press')) { - // Add type action newWhat.push({ action: 'type', args: [selector, encrypt(credential.value), credential.type] }); - // Add single waitForLoadState newWhat.push({ action: 'waitForLoadState', args: ['networkidle'] @@ -213,7 +205,6 @@ function handleWorkflowActions(workflow: any[], credentials: Credentials) { processedSelectors.add(selector); - // Skip subsequent type/press/waitForLoadState actions for this selector while (i + 1 < step.what.length && (step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press' || @@ -223,7 +214,6 @@ function handleWorkflowActions(workflow: any[], credentials: Credentials) { } } else if ((action.action === 'type' || action.action === 'press') && !processedSelectors.has(selector)) { - // Handle standalone type/press action newWhat.push({ action: 'type', args: [selector, encrypt(credential.value), credential.type] From a90ee270f2edf22ce56c6cbf5e831e98510a52ba Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 14 Feb 2025 23:35:44 +0530 Subject: [PATCH 41/56] feat: create an account --- public/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en.json b/public/locales/en.json index 919af547..883892a6 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -20,7 +20,7 @@ } }, "register": { - "title": "Register Account", + "title": "Create An Account", "email": "Enter Work Email", "password": "Password", "button": "Register", From 4d058eb8292c93ac0d41e12faee180249cb5961c Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 15 Feb 2025 12:17:04 +0530 Subject: [PATCH 42/56] feat: add translation for logout notifs --- public/locales/de.json | 13 +++++++++++++ public/locales/en.json | 13 +++++++++++++ public/locales/es.json | 13 +++++++++++++ public/locales/ja.json | 13 +++++++++++++ public/locales/zh.json | 13 +++++++++++++ 5 files changed, 65 insertions(+) diff --git a/public/locales/de.json b/public/locales/de.json index 1ff41fba..7314b677 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -487,6 +487,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "Erfolgreich abgemeldet" + }, + "errors": { + "logout": { + "unauthorized": "Sie sind nicht berechtigt, diese Aktion durchzuführen", + "server": "Serverfehler beim Abmelden", + "network": "Netzwerkfehler beim Abmelden", + "unknown": "Ein unerwarteter Fehler ist beim Abmelden aufgetreten" + } + } + }, "upgrade": { "button": "Upgrade", "modal": { diff --git a/public/locales/en.json b/public/locales/en.json index 919af547..cfaf9ef4 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -498,6 +498,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "Logged out successfully" + }, + "errors": { + "logout": { + "unauthorized": "You are not authorized to perform this action", + "server": "Server error occurred during logout", + "network": "Network error while logging out", + "unknown": "An unexpected error occurred during logout" + } + } + }, "upgrade": { "button": "Upgrade", "modal": { diff --git a/public/locales/es.json b/public/locales/es.json index cd79d2cd..ca4edc5d 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -488,6 +488,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "Sesión cerrada exitosamente" + }, + "errors": { + "logout": { + "unauthorized": "No estás autorizado para realizar esta acción", + "server": "Error del servidor durante el cierre de sesión", + "network": "Error de red al cerrar sesión", + "unknown": "Ocurrió un error inesperado al cerrar sesión" + } + } + }, "upgrade": { "button": "Actualizar", "modal": { diff --git a/public/locales/ja.json b/public/locales/ja.json index d2fe42ea..a4a66143 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -488,6 +488,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "ログアウトに成功しました" + }, + "errors": { + "logout": { + "unauthorized": "この操作を実行する権限がありません", + "server": "ログアウト中にサーバーエラーが発生しました", + "network": "ログアウト中にネットワークエラーが発生しました", + "unknown": "ログアウト中に予期せぬエラーが発生しました" + } + } + }, "upgrade": { "button": "アップグレード", "modal": { diff --git a/public/locales/zh.json b/public/locales/zh.json index e7c58660..c92ed186 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -488,6 +488,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "退出登录成功" + }, + "errors": { + "logout": { + "unauthorized": "您没有执行此操作的权限", + "server": "退出登录时发生服务器错误", + "network": "退出登录时发生网络错误", + "unknown": "退出登录时发生未知错误" + } + } + }, "upgrade": { "button": "升级", "modal": { From f3f81d6c5298646410fee366a2a8248ec9290d56 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 15 Feb 2025 12:18:17 +0530 Subject: [PATCH 43/56] feat: better logout error messages --- src/components/dashboard/NavBar.tsx | 38 +++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 8a9b1e9c..2456d013 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -108,11 +108,39 @@ export const NavBar: React.FC = ({ }; const logout = async () => { - dispatch({ type: "LOGOUT" }); - window.localStorage.removeItem("user"); - const { data } = await axios.get(`${apiUrl}/auth/logout`); - notify("success", data.message); - navigate("/login"); + try { + const { data } = await axios.get(`${apiUrl}/auth/logout`); + if (data.ok) { + dispatch({ type: "LOGOUT" }); + window.localStorage.removeItem("user"); + notify('success', t('navbar.notifications.success.logout')); + navigate("/login"); + } + } catch (error: any) { + const status = error.response?.status; + let errorKey = 'unknown'; + + switch (status) { + case 401: + errorKey = 'unauthorized'; + break; + case 500: + errorKey = 'server'; + break; + default: + if (error.message?.includes('Network Error')) { + errorKey = 'network'; + } + } + + notify( + 'error', + t(`navbar.notifications.errors.logout.${errorKey}`, { + error: error.response?.data?.message || error.message + }) + ); + navigate("/login"); + } }; const goToMainMenu = async () => { From 18787317da278458536edf8ab98944ebddcfbcee Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 15 Feb 2025 12:18:54 +0530 Subject: [PATCH 44/56] feat: add code for error messages --- server/src/routes/auth.ts | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 47eced63..f18ee8c1 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -140,14 +140,38 @@ router.post("/login", async (req, res) => { } }); -router.get("/logout", async (req, res) => { - try { - res.clearCookie("token"); - return res.json({ message: "Logout successful" }); - } catch (error: any) { - res.status(500).send(`Could not logout user - ${error.message}`); +router.get( + "/logout", + requireSignIn, + async (req: Request, res) => { + const authenticatedReq = req as AuthenticatedRequest; + try { + if (!authenticatedReq.user) { + return res.status(401).json({ + ok: false, + message: "Unauthorized", + code: "unauthorized" + }); + } + + res.clearCookie("token"); + + return res.status(200).json({ + ok: true, + message: "Logged out successfully", + code: "success" + }); + } catch (error) { + console.error('Logout error:', error); + return res.status(500).json({ + ok: false, + message: "Error during logout", + code: "server", + error: process.env.NODE_ENV === 'development' ? error : undefined + }); + } } -}); +); router.get( "/current-user", From a5c100f1744919387846a38894550204805d2db7 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 13:18:03 +0530 Subject: [PATCH 45/56] fix: add error handling for optimize --- server/src/workflow-management/selector.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 3f44a01e..20a6a529 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1092,12 +1092,16 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { newPath.splice(i, 1); const newPathKey = selector(newPath); if (scope.visited.has(newPathKey)) { - return; + continue; } - if (unique(newPath) && same(newPath, input)) { - yield newPath; - scope.visited.set(newPathKey, true); - yield* optimize(newPath, input, scope); + try { + if (unique(newPath) && same(newPath, input)) { + yield newPath; + scope.visited.set(newPathKey, true); + yield* optimize(newPath, input, scope); + } + } catch (e: any) { + continue; } } } From cbe8d4fee89504f1db81114eda690b230958371b Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 19:04:43 +0530 Subject: [PATCH 46/56] fix: add selector check for state --- maxun-core/src/interpret.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index 7fc4d17a..88047128 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -286,6 +286,12 @@ export default class Interpreter extends EventEmitter { ? arrayToObject(superset[key]) : superset[key]; + if (key === 'selectors' && Array.isArray(value) && Array.isArray(superset[key])) { + return value.some(selector => + (superset[key] as any[]).includes(selector) + ); + } + // Every `subset` key must exist in the `superset` and // have the same value (strict equality), or subset[key] <= superset[key] return parsedSuperset[key] From 2033df72f6a8616a9880a9451a3907342d430f45 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 21 Feb 2025 15:52:38 +0530 Subject: [PATCH 47/56] feat: set cursor to pointer --- src/components/dashboard/NavBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 8a9b1e9c..7408ec79 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -205,6 +205,7 @@ export const NavBar: React.FC = ({
{t('navbar.project_name')}
From dfecc521f4ec7622574e5e1be3343af9bb7ef5be Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 21 Feb 2025 15:52:55 +0530 Subject: [PATCH 48/56] fix: format --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 7408ec79..f6c7102c 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -205,7 +205,7 @@ export const NavBar: React.FC = ({
{t('navbar.project_name')}
From 87f9b4d07c0cb09d3e0eb474e774dc8fe63233eb Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 21 Feb 2025 15:53:37 +0530 Subject: [PATCH 49/56] feat: onclick navigate to / --- src/components/dashboard/NavBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index f6c7102c..a437ba61 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -206,7 +206,8 @@ export const NavBar: React.FC = ({ display: 'flex', justifyContent: 'flex-start', cursor: 'pointer' - }}> + }} + onClick={() => navigate('/')}>
{t('navbar.project_name')}
Date: Fri, 21 Feb 2025 15:53:45 +0530 Subject: [PATCH 50/56] fix: format --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index a437ba61..8f951646 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -207,7 +207,7 @@ export const NavBar: React.FC = ({ justifyContent: 'flex-start', cursor: 'pointer' }} - onClick={() => navigate('/')}> + onClick={() => navigate('/')}>
{t('navbar.project_name')}
Date: Sat, 22 Feb 2025 14:41:57 +0530 Subject: [PATCH 51/56] fix: rm auth check for logout --- server/src/routes/auth.ts | 47 +++++++++++++-------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index f18ee8c1..9396a5d4 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -140,22 +140,9 @@ router.post("/login", async (req, res) => { } }); -router.get( - "/logout", - requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; +router.get("/logout", async (req, res) => { try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); - } - res.clearCookie("token"); - return res.status(200).json({ ok: true, message: "Logged out successfully", @@ -176,13 +163,12 @@ router.get( router.get( "/current-user", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password"] }, }); if (!user) { @@ -205,7 +191,7 @@ router.get( router.get( "/user/:id", requireSignIn, - async (req: Request, res) => { + async (req: AuthenticatedRequest, res) => { try { const { id } = req.params; if (!id) { @@ -234,13 +220,12 @@ router.get( router.post( "/generate-api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password"] }, }); @@ -275,14 +260,13 @@ router.post( router.get( "/api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { raw: true, attributes: ["api_key"], }); @@ -304,14 +288,13 @@ router.get( router.delete( "/delete-api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; - if (!authenticatedReq.user) { + async (req: AuthenticatedRequest, res) => { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } try { - const user = await User.findByPk(authenticatedReq.user.id, { raw: true }); + const user = await User.findByPk(req.user.id, { raw: true }); if (!user) { return res.status(404).json({ message: "User not found" }); @@ -321,7 +304,7 @@ router.delete( return res.status(404).json({ message: "API Key not found" }); } - await User.update({ api_key: null }, { where: { id: authenticatedReq.user.id } }); + await User.update({ api_key: null }, { where: { id: req.user.id } }); capture("maxun-oss-api-key-deleted", { user_id: user.id, From dbe99bafb8930b724e82d8f233751aacfb17bb8f Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 22 Feb 2025 14:49:05 +0530 Subject: [PATCH 52/56] feat: revert to auth request --- server/src/routes/auth.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 9396a5d4..67eb13dc 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -350,8 +350,7 @@ router.get("/google", (req, res) => { router.get( "/google/callback", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { code, state } = req.query; try { if (!state) { @@ -377,12 +376,12 @@ router.get( return res.status(400).json({ message: "Email not found" }); } - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } // Get the currently authenticated user (from `requireSignIn`) - let user = await User.findOne({ where: { id: authenticatedReq.user.id } }); + let user = await User.findOne({ where: { id: req.user.id } }); if (!user) { return res.status(400).json({ message: "User not found" }); @@ -460,13 +459,12 @@ router.get( router.post( "/gsheets/data", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { spreadsheetId, robotId } = req.body; - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { raw: true }); + const user = await User.findByPk(req.user.id, { raw: true }); if (!user) { return res.status(400).json({ message: "User not found" }); @@ -578,14 +576,13 @@ router.post("/gsheets/update", requireSignIn, async (req, res) => { router.post( "/gsheets/remove", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { robotId } = req.body; if (!robotId) { return res.status(400).json({ message: "Robot ID is required" }); } - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } @@ -607,7 +604,7 @@ router.post( }); capture("maxun-oss-google-sheet-integration-removed", { - user_id: authenticatedReq.user.id, + user_id: req.user.id, robot_id: robotId, deleted_at: new Date().toISOString(), }); From daa889e73499e526d621d695dd84d1012e928d0b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 22 Feb 2025 21:22:19 +0530 Subject: [PATCH 53/56] feat: revert to auth request --- server/src/routes/auth.ts | 176 ++++++++++---------------------------- 1 file changed, 47 insertions(+), 129 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index f77205b4..728cc42c 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -140,22 +140,9 @@ router.post("/login", async (req, res) => { } }); -router.get( - "/logout", - requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; +router.get("/logout", async (req, res) => { try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); - } - res.clearCookie("token"); - return res.status(200).json({ ok: true, message: "Logged out successfully", @@ -176,13 +163,12 @@ router.get( router.get( "/current-user", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password"] }, }); if (!user) { @@ -205,7 +191,7 @@ router.get( router.get( "/user/:id", requireSignIn, - async (req: Request, res) => { + async (req: AuthenticatedRequest, res) => { try { const { id } = req.params; if (!id) { @@ -234,60 +220,39 @@ router.get( router.post( "/generate-api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); + if (!req.user) { + return res.status(401).json({ ok: false, error: "Unauthorized" }); } - - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password"] }, }); if (!user) { - return res.status(404).json({ - ok: false, - message: "User not found", - code: "not_found" - }); + return res.status(404).json({ message: "User not found" }); } if (user.api_key) { - return res.status(400).json({ - ok: false, - message: "API key already exists", - code: "key_exists" - }); + return res.status(400).json({ message: "API key already exists" }); } - const apiKey = genAPIKey(); + await user.update({ api_key: apiKey }); - // Capture analytics event capture("maxun-oss-api-key-created", { user_id: user.id, created_at: new Date().toISOString(), }); return res.status(200).json({ - ok: true, message: "API key generated successfully", - api_key: apiKey + api_key: apiKey, }); - } catch (error) { - console.error('API Key generation error:', error); - return res.status(500).json({ - ok: false, - message: "Error generating API key", - code: "server", - error: process.env.NODE_ENV === 'development' ? error : undefined - }); + return res + .status(500) + .json({ message: "Error generating API key", error }); } } ); @@ -295,44 +260,27 @@ router.post( router.get( "/api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); + if (!req.user) { + return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { raw: true, attributes: ["api_key"], }); if (!user) { - return res.status(404).json({ - ok: false, - message: "User not found", - code: "not_found" - }); + return res.status(404).json({ message: "User not found" }); } return res.status(200).json({ - ok: true, message: "API key fetched successfully", - api_key: user.api_key || null + api_key: user.api_key || null, }); - } catch (error) { - console.error('API Key fetch error:', error); - return res.status(500).json({ - ok: false, - message: "Error fetching API key", - code: "server", - error: process.env.NODE_ENV === 'development' ? error : undefined - }); + return res.status(500).json({ message: "Error fetching API key", error }); } } ); @@ -340,61 +288,34 @@ router.get( router.delete( "/delete-api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; - try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); - } + async (req: AuthenticatedRequest, res) => { + if (!req.user) { + return res.status(401).send({ error: "Unauthorized" }); + } - const user = await User.findByPk(authenticatedReq.user.id, { - raw: true, - attributes: ["id", "api_key"] - }); + try { + const user = await User.findByPk(req.user.id, { raw: true }); if (!user) { - return res.status(404).json({ - ok: false, - message: "User not found", - code: "not_found" - }); + return res.status(404).json({ message: "User not found" }); } if (!user.api_key) { - return res.status(404).json({ - ok: false, - message: "API Key not found", - code: "key_not_found" - }); + return res.status(404).json({ message: "API Key not found" }); } - await User.update( - { api_key: null }, - { where: { id: authenticatedReq.user.id } } - ); + await User.update({ api_key: null }, { where: { id: req.user.id } }); capture("maxun-oss-api-key-deleted", { user_id: user.id, deleted_at: new Date().toISOString(), }); - return res.status(200).json({ - ok: true, - message: "API Key deleted successfully" - }); - - } catch (error) { - console.error('API Key deletion error:', error); - return res.status(500).json({ - ok: false, - message: "Error deleting API key", - code: "server", - error: process.env.NODE_ENV === 'development' ? error : undefined - }); + return res.status(200).json({ message: "API Key deleted successfully" }); + } catch (error: any) { + return res + .status(500) + .json({ message: "Error deleting API key", error: error.message }); } } ); @@ -429,8 +350,7 @@ router.get("/google", (req, res) => { router.get( "/google/callback", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { code, state } = req.query; try { if (!state) { @@ -456,12 +376,12 @@ router.get( return res.status(400).json({ message: "Email not found" }); } - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } // Get the currently authenticated user (from `requireSignIn`) - let user = await User.findOne({ where: { id: authenticatedReq.user.id } }); + let user = await User.findOne({ where: { id: req.user.id } }); if (!user) { return res.status(400).json({ message: "User not found" }); @@ -539,13 +459,12 @@ router.get( router.post( "/gsheets/data", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { spreadsheetId, robotId } = req.body; - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { raw: true }); + const user = await User.findByPk(req.user.id, { raw: true }); if (!user) { return res.status(400).json({ message: "User not found" }); @@ -657,14 +576,13 @@ router.post("/gsheets/update", requireSignIn, async (req, res) => { router.post( "/gsheets/remove", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { robotId } = req.body; if (!robotId) { return res.status(400).json({ message: "Robot ID is required" }); } - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } @@ -686,7 +604,7 @@ router.post( }); capture("maxun-oss-google-sheet-integration-removed", { - user_id: authenticatedReq.user.id, + user_id: req.user.id, robot_id: robotId, deleted_at: new Date().toISOString(), }); @@ -700,4 +618,4 @@ router.post( }); } } -); +); \ No newline at end of file From 18fc3de9f4cd5e7bddbd9652528be585ae2ed921 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 22 Feb 2025 21:24:32 +0530 Subject: [PATCH 54/56] chore: publish 0.0.12 --- maxun-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxun-core/package.json b/maxun-core/package.json index d0211414..2f04d42a 100644 --- a/maxun-core/package.json +++ b/maxun-core/package.json @@ -1,6 +1,6 @@ { "name": "maxun-core", - "version": "0.0.11", + "version": "0.0.12", "description": "Core package for Maxun, responsible for data extraction", "main": "build/index.js", "typings": "build/index.d.ts", From 09a35da30f8eae656d0b050ca59d4d9239cca178 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 22 Feb 2025 21:27:59 +0530 Subject: [PATCH 55/56] chore: v0.0.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b238a679..99cbcd16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maxun", - "version": "0.0.9", + "version": "0.0.10", "author": "Maxun", "license": "AGPL-3.0-or-later", "dependencies": { From 04ba84d8e67e7d064b35b97e8b6ef3d696da362f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 22 Feb 2025 21:29:01 +0530 Subject: [PATCH 56/56] chore: upgrade core to 0.0.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99cbcd16..0580d2de 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "lodash": "^4.17.21", "loglevel": "^1.8.0", "loglevel-plugin-remote": "^0.6.8", - "maxun-core": "^0.0.11", + "maxun-core": "^0.0.12", "minio": "^8.0.1", "moment-timezone": "^0.5.45", "node-cron": "^3.0.3",