diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..b675b2763
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,11 @@
+node_modules
+npm-debug.log
+dist
+.git
+.gitignore
+.env
+.md
+.vscode
+coverage
+docker-compose.yml
+Dockerfile
\ No newline at end of file
diff --git a/ENVEXAMPLE b/ENVEXAMPLE
index d17b1fc5d..a387785f6 100644
--- a/ENVEXAMPLE
+++ b/ENVEXAMPLE
@@ -1,12 +1,12 @@
# App Setup
NODE_ENV=production # Set to 'development' or 'production' as required
-JWT_SECRET=your_jwt_secret_key # Replace with a secure JWT secret key
+JWT_SECRET=a9Z$kLq7^f03GzNw!bP9dH4xV6sT2yXl3O8vR@uYq3 # Replace with a secure JWT secret key
DB_NAME=maxun # Your PostgreSQL database name
DB_USER=postgres # PostgreSQL username
DB_PASSWORD=postgres # PostgreSQL password
DB_HOST=postgres # Host for PostgreSQL in Docker
DB_PORT=5432 # Port for PostgreSQL (default: 5432)
-ENCRYPTION_KEY=your_encryption_key # Key for encrypting sensitive data (passwords and proxies)
+ENCRYPTION_KEY=f4d5e6a7b8c9d0e1f23456789abcdef01234567890abcdef123456789abcdef0 # Key for encrypting sensitive data (passwords and proxies)
MINIO_ENDPOINT=minio # MinIO endpoint in Docker
MINIO_PORT=9000 # Port for MinIO (default: 9000)
MINIO_ACCESS_KEY=minio_access_key # MinIO access key
diff --git a/README.md b/README.md
index 602cb05ca..dd5624220 100644
--- a/README.md
+++ b/README.md
@@ -20,12 +20,17 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web
Twitter |
Join Maxun Cloud |
Watch Tutorials
+
+
+

+> Note: We are in early stages of development and do not support self hosting yet. You can run Maxun locally.
+
# Local Setup
### Docker Compose
```
@@ -49,6 +54,16 @@ npm install
cd maxun-core
npm install
+# get back to the root directory
+cd ..
+
+# make sure playwright is properly initialized
+npx playwright install
+npx playwright install-deps
+
+# get back to the root directory
+cd ..
+
# start frontend and backend together
npm run start
```
@@ -61,8 +76,8 @@ You can access the frontend at http://localhost:5173/ and backend at http://loca
| Variable | Mandatory | Description | If Not Set |
|-----------------------|-----------|----------------------------------------------------------------------------------------------|--------------------------------------------------------------|
-| `BACKEND_URL` | Yes | URL to run backend on. | Backend won't start. If not sure, set to http://localhost:8080 |
-| `VITE_BACKEND_URL` | Yes | URL to run backend on. | Backend won't start. If not sure, set to http://localhost:8080 |
+| `BACKEND_URL` | Yes | URL to run backend on. | Default value: http://localhost:8080 |
+| `VITE_BACKEND_URL` | Yes | URL used by frontend to connect to backend | Default value: http://localhost:8080 |
| `JWT_SECRET` | Yes | Secret key used to sign and verify JSON Web Tokens (JWTs) for authentication. | JWT authentication will not work. |
| `DB_NAME` | Yes | Name of the Postgres database to connect to. | Database connection will fail. |
| `DB_USER` | Yes | Username for Postgres database authentication. | Database connection will fail. |
diff --git a/docker-compose.yml b/docker-compose.yml
index 1aeefd266..abd4565e3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -32,16 +32,18 @@ services:
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
- command: server /data
+ command: server /data --console-address :9001
ports:
- - "9000:9000"
+ - "9000:9000" # API port
+ - "9001:9001" # WebUI port
volumes:
- minio_data:/data
backend:
- build:
- context: .
- dockerfile: server/Dockerfile
+ #build:
+ #context: .
+ #dockerfile: server/Dockerfile
+ image: getmaxun/maxun-backend:v0.0.1
ports:
- "8080:8080"
env_file: .env
@@ -56,6 +58,7 @@ services:
- seccomp=unconfined # This might help with browser sandbox issues
# Increase shared memory size for Chromium
shm_size: '2gb'
+ mem_limit: 2g # Set a 2GB memory limit
depends_on:
- postgres
- redis
@@ -66,9 +69,10 @@ services:
- /var/run/dbus:/var/run/dbus
frontend:
- build:
- context: .
- dockerfile: Dockerfile
+ #build:
+ #context: .
+ #dockerfile: Dockerfile
+ image: getmaxun/maxun-frontend:v0.0.1
ports:
- "5173:5173"
env_file: .env
diff --git a/esbuild.config.js b/esbuild.config.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/package.json b/package.json
index fd9664585..8c914f558 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "maxun",
- "version": "0.0.1",
+ "version": "0.0.2",
"author": "Maxun",
"license": "AGPL-3.0-or-later",
"dependencies": {
@@ -39,6 +39,7 @@
"ioredis": "^5.4.1",
"joi": "^17.6.0",
"jsonwebtoken": "^9.0.2",
+ "jwt-decode": "^4.0.0",
"loglevel": "^1.8.0",
"loglevel-plugin-remote": "^0.6.8",
"maxun-core": "^0.0.3",
@@ -46,10 +47,11 @@
"moment-timezone": "^0.5.45",
"node-cron": "^3.0.3",
"pg": "^8.13.0",
- "playwright": "^1.20.1",
+ "playwright": "^1.48.2",
"playwright-extra": "^4.3.6",
"posthog-node": "^4.2.1",
"prismjs": "^1.28.0",
+ "puppeteer-extra-plugin-recaptcha": "^3.6.8",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
@@ -89,6 +91,7 @@
"devDependencies": {
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.13",
+ "@types/js-cookie": "^3.0.6",
"@types/loglevel": "^1.6.3",
"@types/node": "22.7.9",
"@types/node-cron": "^3.0.11",
@@ -102,6 +105,7 @@
"ajv": "^8.8.2",
"concurrently": "^7.0.0",
"cross-env": "^7.0.3",
+ "js-cookie": "^3.0.5",
"nodemon": "^2.0.15",
"ts-node": "^10.4.0",
"vite": "^5.4.10"
diff --git a/server/Dockerfile b/server/Dockerfile
index a99042693..ae26e8ebc 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -18,33 +18,6 @@ RUN npm install
# Install Playwright browsers and dependencies
RUN npx playwright install --with-deps chromium
-# Install xvfb for display support
-#RUN apt-get update && apt-get install -y xvfb
-
-# RUN apt-get update && apt-get install -y \
-# libgbm-dev \
-# libxkbcommon-x11-0 \
-# libxcomposite1 \
-# libxdamage1 \
-# libxrandr2 \
-# libxshmfence1 \
-# libxtst6 \
-# libnss3 \
-# libatk1.0-0 \
-# libatk-bridge2.0-0 \
-# libdrm2 \
-# libxcb1 \
-# libxkbcommon0 \
-# fonts-noto-color-emoji \
-# fonts-unifont \
-# libpango-1.0-0 \
-# libcairo2 \
-# libasound2 \
-# libglib2.0-0 \
-# libdbus-1-3 \
-# && rm -rf /var/lib/apt/lists/*
-
-# Create and set permissions for chrome directories
# Create the Chromium data directory with necessary permissions
RUN mkdir -p /tmp/chromium-data-dir && \
chmod -R 777 /tmp/chromium-data-dir
diff --git a/server/src/api/record.ts b/server/src/api/record.ts
index b55f06bc2..fef0be9af 100644
--- a/server/src/api/record.ts
+++ b/server/src/api/record.ts
@@ -1,6 +1,7 @@
import { readFile, readFiles } from "../workflow-management/storage";
import { Router, Request, Response } from 'express';
-import { chromium } from "playwright";
+import { chromium } from "playwright-extra";
+import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { requireAPIKey } from "../middlewares/api";
import Robot from "../models/Robot";
import Run from "../models/Run";
@@ -14,6 +15,7 @@ import { io, Socket } from "socket.io-client";
import { BinaryOutputService } from "../storage/mino";
import { AuthenticatedRequest } from "../routes/record"
import {capture} from "../utils/analytics";
+chromium.use(stealthPlugin());
const formatRecording = (recordingData: any) => {
const recordingMeta = recordingData.recording_meta;
@@ -289,7 +291,7 @@ router.get("/robots/:id", requireAPIKey, async (req: Request, res: Response) =>
* type: string
* example: "Failed to retrieve runs"
*/
-router.get("/robots/:id/runs", requireAPIKey, async (req: Request, res: Response) => {
+router.get("/robots/:id/runs",requireAPIKey, async (req: Request, res: Response) => {
try {
const runs = await Run.findAll({
where: {
@@ -321,6 +323,7 @@ router.get("/robots/:id/runs", requireAPIKey, async (req: Request, res: Response
}
);
+
function formatRunResponse(run: any) {
const formattedRun = {
id: run.id,
@@ -494,7 +497,7 @@ async function createWorkflowAndStoreMetadata(id: string, userId: string) {
} catch (e) {
const { message } = e as Error;
logger.log('info', `Error while scheduling a run with id: ${id}`);
- console.log(message);
+ console.log(`Error scheduling run:`, message);
return {
success: false,
error: message,
@@ -766,7 +769,6 @@ router.post("/robots/:id/runs", requireAPIKey, async (req: AuthenticatedRequest,
return res.status(401).json({ ok: false, error: 'Unauthorized' });
}
const runId = await handleRunRecording(req.params.id, req.user.dataValues.id);
- console.log(`Result`, runId);
if (!runId) {
throw new Error('Run ID is undefined');
diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts
index cbda39425..07ea8780b 100644
--- a/server/src/browser-management/classes/RemoteBrowser.ts
+++ b/server/src/browser-management/classes/RemoteBrowser.ts
@@ -3,9 +3,10 @@ import {
Browser,
CDPSession,
BrowserContext,
- chromium,
} from 'playwright';
import { Socket } from "socket.io";
+import { chromium } from 'playwright-extra';
+import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { PlaywrightBlocker } from '@cliqz/adblocker-playwright';
import fetch from 'cross-fetch';
@@ -14,7 +15,7 @@ import { InterpreterSettings, RemoteBrowserOptions } from "../../types";
import { WorkflowGenerator } from "../../workflow-management/classes/Generator";
import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter";
import { getDecryptedProxyConfig } from '../../routes/proxy';
-
+chromium.use(stealthPlugin());
/**
@@ -163,9 +164,7 @@ export class RemoteBrowser {
contextOptions.userAgent = browserUserAgent;
this.context = await this.browser.newContext(contextOptions);
- console.log(`Context from initialize: ${JSON.stringify(this.context)}`)
this.currentPage = await this.context.newPage();
- console.log(`CPage from initialize: ${JSON.stringify(this.currentPage)}`)
// await this.currentPage.setExtraHTTPHeaders({
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
// });
@@ -307,6 +306,7 @@ export class RemoteBrowser {
const workflow = this.generator.AddGeneratedFlags(this.generator.getWorkflowFile());
await this.initializeNewPage();
if (this.currentPage) {
+ this.currentPage.setViewportSize({ height: 400, width: 900 });
const params = this.generator.getParams();
if (params) {
this.interpreterSettings.params = params.reduce((acc, param) => {
diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts
index 2e43a4c5c..d6902b3f3 100644
--- a/server/src/browser-management/inputHandlers.ts
+++ b/server/src/browser-management/inputHandlers.ts
@@ -271,7 +271,6 @@ const handleChangeUrl = async (generator: WorkflowGenerator, page: Page, url: st
try {
await page.goto(url);
logger.log('debug', `Went to ${url}`);
- console.log(`Went to ${url}`)
} catch (e) {
const { message } = e as Error;
logger.log('error', message);
diff --git a/server/src/middlewares/auth.ts b/server/src/middlewares/auth.ts
index 75540ad55..74770c3a1 100644
--- a/server/src/middlewares/auth.ts
+++ b/server/src/middlewares/auth.ts
@@ -16,12 +16,16 @@ export const requireSignIn = (req: UserRequest, res: Response, next: any) => {
}
verify(token, secret, (err: any, user: any) => {
- console.log(err)
-
- if (err) return res.sendStatus(403)
-
+ if (err) {
+ console.log('JWT verification error:', err);
+ return res.sendStatus(403);
+ }
+ // Normalize payload key
+ if (user.userId && !user.id) {
+ user.id = user.userId;
+ delete user.userId; // temporary: del the old key for clarity
+ }
req.user = user;
-
- next()
- })
+ next();
+ });
};
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
index 6766a3565..692add99e 100644
--- a/server/src/routes/auth.ts
+++ b/server/src/routes/auth.ts
@@ -1,480 +1,557 @@
-import { Router, Request, Response } from 'express';
-import User from '../models/User';
-import Robot from '../models/Robot';
-import jwt from 'jsonwebtoken';
-import { hashPassword, comparePassword } from '../utils/auth';
-import { requireSignIn } from '../middlewares/auth';
-import { genAPIKey } from '../utils/api';
-import { google } from 'googleapis';
-import { capture } from "../utils/analytics"
+import { Router, Request, Response } from "express";
+import User from "../models/User";
+import Robot from "../models/Robot";
+import jwt from "jsonwebtoken";
+import { hashPassword, comparePassword } from "../utils/auth";
+import { requireSignIn } from "../middlewares/auth";
+import { genAPIKey } from "../utils/api";
+import { google } from "googleapis";
+import { capture } from "../utils/analytics";
export const router = Router();
interface AuthenticatedRequest extends Request {
- user?: { id: string };
+ user?: { id: string };
}
-router.post('/register', async (req, res) => {
- console.log('Received request at /auth/register');
- console.log('Received body:', req.body);
- try {
- const { email, password } = req.body
-
- if (!email) return res.status(400).send('Email is required')
- if (!password || password.length < 6) return res.status(400).send('Password is required and must be at least 6 characters')
-
- let userExist = await User.findOne({ raw: true, where: { email } });
- if (userExist) return res.status(400).send('User already exists')
-
- const hashedPassword = await hashPassword(password)
-
- let user: any;
-
- try {
- user = await User.create({ email, password: hashedPassword });
- } catch (
- error: any
- ) {
- console.log(`Could not create user - ${error}`)
- return res.status(500).send(`Could not create user - ${error.message}`)
- }
-
- if (!process.env.JWT_SECRET) {
- console.log('JWT_SECRET is not defined in the environment');
- return res.status(500).send('Internal Server Error');
- }
-
- const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET as string, { expiresIn: '12h' });
- user.password = undefined as unknown as string
- res.cookie('token', token, {
- httpOnly: true
- })
- capture(
- 'maxun-oss-user-registered',
- {
- email: user.email,
- userId: user.id,
- registeredAt: new Date().toISOString()
- }
- )
- console.log(`User registered - ${user.email}`)
- res.json(user)
- } catch (error: any) {
- console.log(`Could not register user - ${error}`)
- res.status(500).send(`Could not register user - ${error.message}`)
- }
-})
+router.post("/register", async (req, res) => {
+ try {
+ const { email, password } = req.body;
-router.post('/login', async (req, res) => {
- try {
- const { email, password } = req.body;
- if (!email || !password) return res.status(400).send('Email and password are required')
- if (password.length < 6) return res.status(400).send('Password must be at least 6 characters')
-
- let user = await User.findOne({ raw: true, where: { email } });
- if (!user) return res.status(400).send('User does not exist');
-
- const match = await comparePassword(password, user.password)
- if (!match) return res.status(400).send('Invalid email or password')
-
- const token = jwt.sign({ id: user?.id }, process.env.JWT_SECRET as string, { expiresIn: '12h' });
-
- // return user and token to client, exclude hashed password
- if (user) {
- user.password = undefined as unknown as string;
- }
- res.cookie('token', token, {
- httpOnly: true
- })
- res.json(user)
- } catch (error: any) {
- res.status(400).send(`Could not login user - ${error.message}`)
- console.log(`Could not login user - ${error}`)
- }
-})
+ if (!email) return res.status(400).send("Email is required");
+ if (!password || password.length < 6)
+ return res
+ .status(400)
+ .send("Password is required and must be at least 6 characters");
+
+ let userExist = await User.findOne({ raw: true, where: { email } });
+ if (userExist) return res.status(400).send("User already exists");
+
+ const hashedPassword = await hashPassword(password);
+
+ let user: any;
-router.get('/logout', async (req, res) => {
try {
- res.clearCookie('token')
- return res.json({ message: 'Logout successful' })
+ user = await User.create({ email, password: hashedPassword });
} catch (error: any) {
- res.status(500).send(`Could not logout user - ${error.message}`)
+ console.log(`Could not create user - ${error}`);
+ return res.status(500).send(`Could not create user - ${error.message}`);
}
-})
-router.get('/current-user', requireSignIn, async (req: AuthenticatedRequest, res) => {
- try {
- if (!req.user) {
- return res.status(401).json({ ok: false, error: 'Unauthorized' });
- }
- const user = await User.findByPk(req.user.id, {
- attributes: { exclude: ['password'] },
- });
- if (!user) {
- return res.status(404).json({ ok: false, error: 'User not found' });
- } else {
- return res.status(200).json({ ok: true, user: user });
- }
- } catch (error: any) {
- console.error('Error in current-user route:', error);
- return res.status(500).json({ ok: false, error: `Could not fetch current user: ${error.message}` });
+ if (!process.env.JWT_SECRET) {
+ console.log("JWT_SECRET is not defined in the environment");
+ return res.status(500).send("Internal Server Error");
}
+
+ const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET as string);
+ user.password = undefined as unknown as string;
+ res.cookie("token", token, {
+ httpOnly: true,
+ });
+ capture("maxun-oss-user-registered", {
+ email: user.email,
+ userId: user.id,
+ registeredAt: new Date().toISOString(),
+ });
+ console.log(`User registered - ${user.email}`);
+ res.json(user);
+ } catch (error: any) {
+ console.log(`Could not register user - ${error}`);
+ res.status(500).send(`Could not register user - ${error.message}`);
+ }
});
-router.get('/user/:id', requireSignIn, async (req: AuthenticatedRequest, res) => {
- try {
- const { id } = req.params;
- if (!id) {
- return res.status(400).json({ message: 'User ID is required' });
- }
+router.post("/login", async (req, res) => {
+ try {
+ const { email, password } = req.body;
+ if (!email || !password)
+ return res.status(400).send("Email and password are required");
+ if (password.length < 6)
+ return res.status(400).send("Password must be at least 6 characters");
- const user = await User.findByPk(id, {
- attributes: { exclude: ['password'] },
- });
+ let user = await User.findOne({ raw: true, where: { email } });
+ if (!user) return res.status(400).send("User does not exist");
- if (!user) {
- return res.status(404).json({ message: 'User not found' });
- }
+ const match = await comparePassword(password, user.password);
+ if (!match) return res.status(400).send("Invalid email or password");
- return res.status(200).json({ message: 'User fetched successfully', user });
- } catch (error: any) {
- return res.status(500).json({ message: 'Error fetching user', error: error.message });
+ 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;
}
+ res.cookie("token", token, {
+ httpOnly: true,
+ });
+ capture("maxun-oss-user-login", {
+ email: user.email,
+ userId: user.id,
+ loggedInAt: new Date().toISOString(),
+ });
+ res.json(user);
+ } catch (error: any) {
+ res.status(400).send(`Could not login user - ${error.message}`);
+ console.log(`Could not login user - ${error}`);
+ }
+});
+
+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.post('/generate-api-key', requireSignIn, async (req: AuthenticatedRequest, res) => {
+router.get(
+ "/current-user",
+ requireSignIn,
+ async (req: AuthenticatedRequest, res) => {
try {
- if (!req.user) {
- return res.status(401).json({ ok: false, error: 'Unauthorized' });
- }
- const user = await User.findByPk(req.user.id, {
- attributes: { exclude: ['password'] },
+ if (!req.user) {
+ return res.status(401).json({ ok: false, error: "Unauthorized" });
+ }
+ const user = await User.findByPk(req.user.id, {
+ attributes: { exclude: ["password"] },
+ });
+ if (!user) {
+ return res.status(404).json({ ok: false, error: "User not found" });
+ } else {
+ return res.status(200).json({ ok: true, user: user });
+ }
+ } catch (error: any) {
+ console.error("Error in current-user route:", error);
+ return res
+ .status(500)
+ .json({
+ ok: false,
+ error: `Could not fetch current user: ${error.message}`,
});
+ }
+ }
+);
- if (!user) {
- return res.status(404).json({ message: 'User not found' });
- }
-
- if (user.api_key) {
- return res.status(400).json({ message: 'API key already exists' });
- }
- const apiKey = genAPIKey();
-
- await user.update({ api_key: apiKey });
-
- capture(
- 'maxun-oss-api-key-created',
- {
- user_id: user.id,
- created_at: new Date().toISOString()
- }
- )
+router.get(
+ "/user/:id",
+ requireSignIn,
+ async (req: AuthenticatedRequest, res) => {
+ try {
+ const { id } = req.params;
+ if (!id) {
+ return res.status(400).json({ message: "User ID is required" });
+ }
+
+ const user = await User.findByPk(id, {
+ attributes: { exclude: ["password"] },
+ });
+
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ return res
+ .status(200)
+ .json({ message: "User fetched successfully", user });
+ } catch (error: any) {
+ return res
+ .status(500)
+ .json({ message: "Error fetching user", error: error.message });
+ }
+ }
+);
- return res.status(200).json({
- message: 'API key generated successfully',
- api_key: apiKey,
- });
+router.post(
+ "/generate-api-key",
+ requireSignIn,
+ async (req: AuthenticatedRequest, res) => {
+ try {
+ if (!req.user) {
+ return res.status(401).json({ ok: false, error: "Unauthorized" });
+ }
+ const user = await User.findByPk(req.user.id, {
+ attributes: { exclude: ["password"] },
+ });
+
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ if (user.api_key) {
+ return res.status(400).json({ message: "API key already exists" });
+ }
+ const apiKey = genAPIKey();
+
+ await user.update({ api_key: apiKey });
+
+ capture("maxun-oss-api-key-created", {
+ user_id: user.id,
+ created_at: new Date().toISOString(),
+ });
+
+ return res.status(200).json({
+ message: "API key generated successfully",
+ api_key: apiKey,
+ });
} catch (error) {
- return res.status(500).json({ message: 'Error generating API key', error });
+ return res
+ .status(500)
+ .json({ message: "Error generating API key", error });
}
-});
+ }
+);
-router.get('/api-key', requireSignIn, async (req: AuthenticatedRequest, res) => {
+router.get(
+ "/api-key",
+ requireSignIn,
+ async (req: AuthenticatedRequest, res) => {
try {
- if (!req.user) {
- return res.status(401).json({ ok: false, error: 'Unauthorized' });
- }
-
- const user = await User.findByPk(req.user.id, {
- raw: true,
- attributes: ['api_key'],
- });
-
- if (!user) {
- return res.status(404).json({ message: 'User not found' });
- }
-
- return res.status(200).json({
- message: 'API key fetched successfully',
- api_key: user.api_key || null,
- });
+ if (!req.user) {
+ return res.status(401).json({ ok: false, error: "Unauthorized" });
+ }
+
+ const user = await User.findByPk(req.user.id, {
+ raw: true,
+ attributes: ["api_key"],
+ });
+
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ return res.status(200).json({
+ message: "API key fetched successfully",
+ api_key: user.api_key || null,
+ });
} catch (error) {
- return res.status(500).json({ message: 'Error fetching API key', error });
+ return res.status(500).json({ message: "Error fetching API key", error });
}
-});
-
-router.delete('/delete-api-key', requireSignIn, async (req: AuthenticatedRequest, res) => {
+ }
+);
+router.delete(
+ "/delete-api-key",
+ requireSignIn,
+ async (req: AuthenticatedRequest, res) => {
if (!req.user) {
- return res.status(401).send({ error: 'Unauthorized' });
+ return res.status(401).send({ error: "Unauthorized" });
}
try {
- const user = await User.findByPk(req.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' });
- }
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
- if (!user.api_key) {
- return res.status(404).json({ message: 'API Key not found' });
- }
+ if (!user.api_key) {
+ return res.status(404).json({ message: "API Key not found" });
+ }
- await User.update({ api_key: null }, { where: { id: req.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()
- }
- )
+ 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' });
+ 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(500)
+ .json({ message: "Error deleting API key", error: error.message });
}
-});
+ }
+);
const oauth2Client = new google.auth.OAuth2(
- process.env.GOOGLE_CLIENT_ID,
- process.env.GOOGLE_CLIENT_SECRET,
- process.env.GOOGLE_REDIRECT_URI
+ process.env.GOOGLE_CLIENT_ID,
+ process.env.GOOGLE_CLIENT_SECRET,
+ process.env.GOOGLE_REDIRECT_URI
);
// Step 1: Redirect to Google for authentication
-router.get('/google', (req, res) => {
- const { robotId } = req.query;
- if (!robotId) {
- return res.status(400).json({ message: 'Robot ID is required' });
- }
- const scopes = [
- 'https://www.googleapis.com/auth/spreadsheets',
- 'https://www.googleapis.com/auth/userinfo.email',
- 'https://www.googleapis.com/auth/drive.readonly',
- ];
- const url = oauth2Client.generateAuthUrl({
- access_type: 'offline',
- prompt: 'consent', // Ensures you get a refresh token on first login
- scope: scopes,
- state: robotId.toString(),
- });
- res.redirect(url);
+router.get("/google", (req, res) => {
+ const { robotId } = req.query;
+ if (!robotId) {
+ return res.status(400).json({ message: "Robot ID is required" });
+ }
+ const scopes = [
+ "https://www.googleapis.com/auth/spreadsheets",
+ "https://www.googleapis.com/auth/userinfo.email",
+ "https://www.googleapis.com/auth/drive.readonly",
+ ];
+ const url = oauth2Client.generateAuthUrl({
+ access_type: "offline",
+ prompt: "consent", // Ensures you get a refresh token on first login
+ scope: scopes,
+ state: robotId.toString(),
+ });
+ res.redirect(url);
});
// Step 2: Handle Google OAuth callback
-router.get('/google/callback', requireSignIn, async (req: AuthenticatedRequest, res) => {
+router.get(
+ "/google/callback",
+ requireSignIn,
+ async (req: AuthenticatedRequest, res) => {
const { code, state } = req.query;
try {
- if (!state) {
- return res.status(400).json({ message: 'Robot ID is required' });
- }
-
- const robotId = state
-
- // Get access and refresh tokens
- if (typeof code !== 'string') {
- return res.status(400).json({ message: 'Invalid code' });
- }
- const { tokens } = await oauth2Client.getToken(code);
- oauth2Client.setCredentials(tokens);
-
- // Get user profile from Google
- const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client });
- const { data: { email } } = await oauth2.userinfo.get();
-
- if (!email) {
- return res.status(400).json({ message: 'Email not found' });
- }
-
- if (!req.user) {
- return res.status(401).send({ error: 'Unauthorized' });
- }
-
- // Get the currently authenticated user (from `requireSignIn`)
- let user = await User.findOne({ where: { id: req.user.id } });
-
- if (!user) {
- return res.status(400).json({ message: 'User not found' });
- }
-
- let robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
-
- if (!robot) {
- return res.status(400).json({ message: 'Robot not found' });
- }
-
- robot = await robot.update({
- google_sheet_email: email,
- google_access_token: tokens.access_token,
- google_refresh_token: tokens.refresh_token,
- });
- capture(
- 'maxun-oss-google-sheet-integration-created',
- {
- user_id: user.id,
- robot_id: robot.recording_meta.id,
- created_at: new Date().toISOString()
- }
- )
-
- // List user's Google Sheets from their Google Drive
- const drive = google.drive({ version: 'v3', auth: oauth2Client });
- const response = await drive.files.list({
- q: "mimeType='application/vnd.google-apps.spreadsheet'", // List only Google Sheets files
- fields: 'files(id, name)', // Retrieve the ID and name of each file
- });
-
- const files = response.data.files || [];
- if (files.length === 0) {
- return res.status(404).json({ message: 'No spreadsheets found.' });
- }
-
- // Generate JWT token for session
- const jwtToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as string, { expiresIn: '12h' });
- res.cookie('token', jwtToken, { httpOnly: true });
-
- res.json({
- message: 'Google authentication successful',
- google_sheet_email: robot.google_sheet_email,
- jwtToken,
- files
- });
+ if (!state) {
+ return res.status(400).json({ message: "Robot ID is required" });
+ }
+
+ const robotId = state;
+
+ // Get access and refresh tokens
+ if (typeof code !== "string") {
+ return res.status(400).json({ message: "Invalid code" });
+ }
+ const { tokens } = await oauth2Client.getToken(code);
+ oauth2Client.setCredentials(tokens);
+
+ // Get user profile from Google
+ const oauth2 = google.oauth2({ version: "v2", auth: oauth2Client });
+ const {
+ data: { email },
+ } = await oauth2.userinfo.get();
+
+ if (!email) {
+ return res.status(400).json({ message: "Email not found" });
+ }
+
+ if (!req.user) {
+ return res.status(401).send({ error: "Unauthorized" });
+ }
+
+ // Get the currently authenticated user (from `requireSignIn`)
+ let user = await User.findOne({ where: { id: req.user.id } });
+
+ if (!user) {
+ return res.status(400).json({ message: "User not found" });
+ }
+
+ let robot = await Robot.findOne({
+ where: { "recording_meta.id": robotId },
+ });
+
+ if (!robot) {
+ return res.status(400).json({ message: "Robot not found" });
+ }
+
+ robot = await robot.update({
+ google_sheet_email: email,
+ google_access_token: tokens.access_token,
+ google_refresh_token: tokens.refresh_token,
+ });
+ capture("maxun-oss-google-sheet-integration-created", {
+ user_id: user.id,
+ robot_id: robot.recording_meta.id,
+ created_at: new Date().toISOString(),
+ });
+
+ // List user's Google Sheets from their Google Drive
+ const drive = google.drive({ version: "v3", auth: oauth2Client });
+ const response = await drive.files.list({
+ q: "mimeType='application/vnd.google-apps.spreadsheet'", // List only Google Sheets files
+ fields: "files(id, name)", // Retrieve the ID and name of each file
+ });
+
+ const files = response.data.files || [];
+ if (files.length === 0) {
+ return res.status(404).json({ message: "No spreadsheets found." });
+ }
+
+ // Generate JWT token for session
+ const jwtToken = jwt.sign(
+ { id: user.id },
+ process.env.JWT_SECRET as string
+ );
+ res.cookie("token", jwtToken, { httpOnly: true });
+
+ // res.json({
+ // message: 'Google authentication successful',
+ // google_sheet_email: robot.google_sheet_email,
+ // jwtToken,
+ // files
+ // });
+
+ res.cookie("robot_auth_status", "success", {
+ httpOnly: false,
+ maxAge: 60000,
+ }); // 1-minute expiration
+ res.cookie("robot_auth_message", "Robot successfully authenticated", {
+ httpOnly: false,
+ maxAge: 60000,
+ });
+ res.redirect(`http://localhost:5173`);
} catch (error: any) {
- res.status(500).json({ message: `Google OAuth error: ${error.message}` });
+ res.status(500).json({ message: `Google OAuth error: ${error.message}` });
}
-});
+ }
+);
// Step 3: Get data from Google Sheets
-router.post('/gsheets/data', requireSignIn, async (req: AuthenticatedRequest, res) => {
+router.post(
+ "/gsheets/data",
+ requireSignIn,
+ async (req: AuthenticatedRequest, res) => {
const { spreadsheetId, robotId } = req.body;
if (!req.user) {
- return res.status(401).send({ error: 'Unauthorized' });
+ return res.status(401).send({ error: "Unauthorized" });
}
const user = await User.findByPk(req.user.id, { raw: true });
if (!user) {
- return res.status(400).json({ message: 'User not found' });
+ return res.status(400).json({ message: "User not found" });
}
- const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId }, raw: true });
+ const robot = await Robot.findOne({
+ where: { "recording_meta.id": robotId },
+ raw: true,
+ });
if (!robot) {
- return res.status(400).json({ message: 'Robot not found' });
+ return res.status(400).json({ message: "Robot not found" });
}
// Set Google OAuth credentials
oauth2Client.setCredentials({
- access_token: robot.google_access_token,
- refresh_token: robot.google_refresh_token,
+ access_token: robot.google_access_token,
+ refresh_token: robot.google_refresh_token,
});
- const sheets = google.sheets({ version: 'v4', auth: oauth2Client });
+ const sheets = google.sheets({ version: "v4", auth: oauth2Client });
try {
- // Fetch data from the spreadsheet (you can let the user choose a specific range too)
- const sheetData = await sheets.spreadsheets.values.get({
- spreadsheetId,
- range: 'Sheet1!A1:D5', // Default range, could be dynamic based on user input
- });
- res.json(sheetData.data);
+ // Fetch data from the spreadsheet (you can let the user choose a specific range too)
+ const sheetData = await sheets.spreadsheets.values.get({
+ spreadsheetId,
+ range: "Sheet1!A1:D5", // Default range, could be dynamic based on user input
+ });
+ res.json(sheetData.data);
} catch (error: any) {
- res.status(500).json({ message: `Error accessing Google Sheets: ${error.message}` });
+ res
+ .status(500)
+ .json({ message: `Error accessing Google Sheets: ${error.message}` });
}
-});
+ }
+);
// Step 4: Get user's Google Sheets files (new route)
-router.get('/gsheets/files', requireSignIn, async (req, res) => {
- try {
- const robotId = req.query.robotId;
- const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId }, raw: true });
-
- if (!robot) {
- return res.status(400).json({ message: 'Robot not found' });
- }
+router.get("/gsheets/files", requireSignIn, async (req, res) => {
+ try {
+ const robotId = req.query.robotId;
+ const robot = await Robot.findOne({
+ where: { "recording_meta.id": robotId },
+ raw: true,
+ });
- oauth2Client.setCredentials({
- access_token: robot.google_access_token,
- refresh_token: robot.google_refresh_token,
- });
+ if (!robot) {
+ return res.status(400).json({ message: "Robot not found" });
+ }
- // List user's Google Sheets files from their Google Drive
- const drive = google.drive({ version: 'v3', auth: oauth2Client });
- const response = await drive.files.list({
- q: "mimeType='application/vnd.google-apps.spreadsheet'",
- fields: 'files(id, name)',
- });
+ oauth2Client.setCredentials({
+ access_token: robot.google_access_token,
+ refresh_token: robot.google_refresh_token,
+ });
- const files = response.data.files || [];
- if (files.length === 0) {
- return res.status(404).json({ message: 'No spreadsheets found.' });
- }
+ // List user's Google Sheets files from their Google Drive
+ const drive = google.drive({ version: "v3", auth: oauth2Client });
+ const response = await drive.files.list({
+ q: "mimeType='application/vnd.google-apps.spreadsheet'",
+ fields: "files(id, name)",
+ });
- res.json(files);
- } catch (error: any) {
- console.log('Error fetching Google Sheets files:', error);
- res.status(500).json({ message: `Error retrieving Google Sheets files: ${error.message}` });
+ const files = response.data.files || [];
+ if (files.length === 0) {
+ return res.status(404).json({ message: "No spreadsheets found." });
}
+
+ res.json(files);
+ } catch (error: any) {
+ console.log("Error fetching Google Sheets files:", error);
+ res
+ .status(500)
+ .json({
+ message: `Error retrieving Google Sheets files: ${error.message}`,
+ });
+ }
});
// Step 5: Update robot's google_sheet_id when a Google Sheet is selected
-router.post('/gsheets/update', requireSignIn, async (req, res) => {
- const { spreadsheetId, spreadsheetName, robotId } = req.body;
+router.post("/gsheets/update", requireSignIn, async (req, res) => {
+ const { spreadsheetId, spreadsheetName, robotId } = req.body;
+
+ if (!spreadsheetId || !robotId) {
+ return res
+ .status(400)
+ .json({ message: "Spreadsheet ID and Robot ID are required" });
+ }
+
+ try {
+ let robot = await Robot.findOne({
+ where: { "recording_meta.id": robotId },
+ });
- if (!spreadsheetId || !robotId) {
- return res.status(400).json({ message: 'Spreadsheet ID and Robot ID are required' });
+ if (!robot) {
+ return res.status(404).json({ message: "Robot not found" });
}
- try {
- let robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
-
- if (!robot) {
- return res.status(404).json({ message: 'Robot not found' });
- }
-
- await robot.update({ google_sheet_id: spreadsheetId, google_sheet_name: spreadsheetName });
+ await robot.update({
+ google_sheet_id: spreadsheetId,
+ google_sheet_name: spreadsheetName,
+ });
- res.json({ message: 'Robot updated with selected Google Sheet ID' });
- } catch (error: any) {
- res.status(500).json({ message: `Error updating robot: ${error.message}` });
- }
+ res.json({ message: "Robot updated with selected Google Sheet ID" });
+ } catch (error: any) {
+ res.status(500).json({ message: `Error updating robot: ${error.message}` });
+ }
});
-router.post('/gsheets/remove', requireSignIn, async (req: AuthenticatedRequest, res) => {
+router.post(
+ "/gsheets/remove",
+ requireSignIn,
+ async (req: AuthenticatedRequest, res) => {
const { robotId } = req.body;
if (!robotId) {
- return res.status(400).json({ message: 'Robot ID is required' });
+ return res.status(400).json({ message: "Robot ID is required" });
}
if (!req.user) {
- return res.status(401).send({ error: 'Unauthorized' });
+ return res.status(401).send({ error: "Unauthorized" });
}
try {
- let robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
-
- if (!robot) {
- return res.status(404).json({ message: 'Robot not found' });
- }
-
- await robot.update({
- google_sheet_id: null,
- google_sheet_name: null,
- google_sheet_email: null,
- google_access_token: null,
- google_refresh_token: null
- });
-
- capture(
- 'maxun-oss-google-sheet-integration-removed',
- {
- user_id: req.user.id,
- robot_id: robotId,
- deleted_at: new Date().toISOString()
- }
- )
-
- res.json({ message: 'Google Sheets integration removed successfully' });
+ let robot = await Robot.findOne({
+ where: { "recording_meta.id": robotId },
+ });
+
+ if (!robot) {
+ return res.status(404).json({ message: "Robot not found" });
+ }
+
+ await robot.update({
+ google_sheet_id: null,
+ google_sheet_name: null,
+ google_sheet_email: null,
+ google_access_token: null,
+ google_refresh_token: null,
+ });
+
+ capture("maxun-oss-google-sheet-integration-removed", {
+ user_id: req.user.id,
+ robot_id: robotId,
+ deleted_at: new Date().toISOString(),
+ });
+
+ res.json({ message: "Google Sheets integration removed successfully" });
} catch (error: any) {
- res.status(500).json({ message: `Error removing Google Sheets integration: ${error.message}` });
+ res
+ .status(500)
+ .json({
+ message: `Error removing Google Sheets integration: ${error.message}`,
+ });
}
-});
+ }
+);
diff --git a/server/src/routes/proxy.ts b/server/src/routes/proxy.ts
index 9182c44cf..d68a889e4 100644
--- a/server/src/routes/proxy.ts
+++ b/server/src/routes/proxy.ts
@@ -1,8 +1,10 @@
import { Router, Request, Response } from 'express';
-import { chromium } from "playwright";
+import { chromium } from 'playwright-extra';
+import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import User from '../models/User';
import { encrypt, decrypt } from '../utils/auth';
import { requireSignIn } from '../middlewares/auth';
+chromium.use(stealthPlugin());
export const router = Router();
@@ -74,8 +76,6 @@ router.get('/test', requireSignIn, async (req: AuthenticatedRequest, res: Respon
const decryptedProxyUsername = user.proxy_username ? decrypt(user.proxy_username) : null;
const decryptedProxyPassword = user.proxy_password ? decrypt(user.proxy_password) : null;
- console.log(`Decrypted vals: ${decryptedProxyPassword}, ${decryptedProxyUrl}, ${decryptedProxyUsername}`);
-
const proxyOptions: any = {
server: decryptedProxyUrl,
...(decryptedProxyUsername && decryptedProxyPassword && {
@@ -170,8 +170,6 @@ export const getDecryptedProxyConfig = async (userId: string) => {
const decryptedProxyUsername = user.proxy_username ? decrypt(user.proxy_username) : null;
const decryptedProxyPassword = user.proxy_password ? decrypt(user.proxy_password) : null;
- console.log(`Decrypting ${decryptedProxyUrl}, ${decryptedProxyUsername}, ${decryptedProxyPassword}`);
-
return {
proxy_url: decryptedProxyUrl,
proxy_username: decryptedProxyUsername,
diff --git a/server/src/routes/record.ts b/server/src/routes/record.ts
index 56089118f..51d3ff922 100644
--- a/server/src/routes/record.ts
+++ b/server/src/routes/record.ts
@@ -11,14 +11,14 @@ import {
stopRunningInterpretation,
getRemoteBrowserCurrentUrl, getRemoteBrowserCurrentTabs,
} from '../browser-management/controller'
-import { chromium } from 'playwright';
+import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import logger from "../logger";
import { getDecryptedProxyConfig } from './proxy';
import { requireSignIn } from '../middlewares/auth';
export const router = Router();
-// chromium.use(stealthPlugin());
+chromium.use(stealthPlugin());
export interface AuthenticatedRequest extends Request {
@@ -57,7 +57,6 @@ router.get('/start', requireSignIn, async (req: AuthenticatedRequest, res: Respo
}
const id = initializeRemoteBrowserForRecording(req.user.id);
- console.log('id start:', id);
return res.send(id);
});
@@ -70,7 +69,6 @@ router.post('/start', requireSignIn, (req: AuthenticatedRequest, res:Response) =
return res.status(401).send('User not authenticated');
}
const id = initializeRemoteBrowserForRecording(req.user.id);
- console.log('id start POST:', id);
return res.send(id);
});
diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts
index 228f60dee..f84583d0b 100644
--- a/server/src/routes/storage.ts
+++ b/server/src/routes/storage.ts
@@ -1,7 +1,8 @@
import { Router } from 'express';
import logger from "../logger";
import { createRemoteBrowserForRun, destroyRemoteBrowser } from "../browser-management/controller";
-import { chromium } from "playwright";
+import { chromium } from 'playwright-extra';
+import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { browserPool } from "../server";
import { uuid } from "uuidv4";
import moment from 'moment-timezone';
@@ -16,6 +17,8 @@ import { workflowQueue } from '../worker';
import { AuthenticatedRequest } from './record';
import { computeNextRun } from '../utils/schedule';
import { capture } from "../utils/analytics";
+import { tryCatch } from 'bullmq';
+chromium.use(stealthPlugin());
export const router = Router();
@@ -57,6 +60,217 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => {
}
})
+router.get(('/recordings/:id/runs'), requireSignIn, async (req, res) => {
+ try {
+ const runs = await Run.findAll({
+ where: {
+ robotMetaId: req.params.id
+ },
+ raw: true
+ });
+ const formattedRuns = runs.map(formatRunResponse);
+ const response = {
+ statusCode: 200,
+ messageCode: "success",
+ runs: {
+ totalCount: formattedRuns.length,
+ items: formattedRuns,
+ },
+ };
+
+ res.status(200).json(response);
+} catch (error) {
+ console.error("Error fetching runs:", error);
+ res.status(500).json({
+ statusCode: 500,
+ messageCode: "error",
+ message: "Failed to retrieve runs",
+ });
+}
+})
+
+function formatRunResponse(run: any) {
+ const formattedRun = {
+ id: run.id,
+ status: run.status,
+ name: run.name,
+ robotId: run.robotMetaId, // Renaming robotMetaId to robotId
+ startedAt: run.startedAt,
+ finishedAt: run.finishedAt,
+ runId: run.runId,
+ runByUserId: run.runByUserId,
+ runByScheduleId: run.runByScheduleId,
+ runByAPI: run.runByAPI,
+ data: {},
+ screenshot: null,
+ };
+
+ if (run.serializableOutput && run.serializableOutput['item-0']) {
+ formattedRun.data = run.serializableOutput['item-0'];
+ } else if (run.binaryOutput && run.binaryOutput['item-0']) {
+ formattedRun.screenshot = run.binaryOutput['item-0'];
+ }
+
+ return formattedRun;
+}
+
+/**
+ * PUT endpoint to update the name and limit of a robot.
+ */
+router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, res) => {
+ try {
+ const { id } = req.params;
+ const { name, limit } = req.body;
+
+ // Validate input
+ if (!name && limit === undefined) {
+ return res.status(400).json({ error: 'Either "name" or "limit" must be provided.' });
+ }
+
+ // Fetch the robot by ID
+ const robot = await Robot.findOne({ where: { 'recording_meta.id': id } });
+
+ if (!robot) {
+ return res.status(404).json({ error: 'Robot not found.' });
+ }
+
+ // Update fields if provided
+ if (name) {
+ robot.set('recording_meta', { ...robot.recording_meta, name });
+ }
+
+ // Update the limit
+ if (limit !== undefined) {
+ const workflow = [...robot.recording.workflow]; // Create a copy of the workflow
+
+ // Ensure the workflow structure is valid before updating
+ if (
+ workflow.length > 0 &&
+ workflow[0]?.what?.[0]
+ ) {
+ // Create a new workflow object with the updated limit
+ const updatedWorkflow = workflow.map((step, index) => {
+ if (index === 0) { // Assuming you want to update the first step
+ return {
+ ...step,
+ what: step.what.map((action, actionIndex) => {
+ if (actionIndex === 0) { // Assuming the first action needs updating
+ return {
+ ...action,
+ args: (action.args ?? []).map((arg, argIndex) => {
+ if (argIndex === 0) { // Assuming the first argument needs updating
+ return { ...arg, limit };
+ }
+ return arg;
+ }),
+ };
+ }
+ return action;
+ }),
+ };
+ }
+ return step;
+ });
+
+ // Replace the workflow in the recording object
+ robot.set('recording', { ...robot.recording, workflow: updatedWorkflow });
+ } else {
+ return res.status(400).json({ error: 'Invalid workflow structure for updating limit.' });
+ }
+ }
+
+ await robot.save();
+
+ const updatedRobot = await Robot.findOne({ where: { 'recording_meta.id': id } });
+
+ logger.log('info', `Robot with ID ${id} was updated successfully.`);
+
+ return res.status(200).json({ message: 'Robot updated successfully', robot });
+ } catch (error) {
+ // Safely handle the error type
+ if (error instanceof Error) {
+ logger.log('error', `Error updating robot with ID ${req.params.id}: ${error.message}`);
+ return res.status(500).json({ error: error.message });
+ } else {
+ logger.log('error', `Unknown error updating robot with ID ${req.params.id}`);
+ return res.status(500).json({ error: 'An unknown error occurred.' });
+ }
+ }
+});
+
+
+/**
+ * POST endpoint to duplicate a robot and update its target URL.
+ */
+router.post('/recordings/:id/duplicate', requireSignIn, async (req: AuthenticatedRequest, res) => {
+ try {
+ const { id } = req.params;
+ const { targetUrl } = req.body;
+
+ if (!targetUrl) {
+ return res.status(400).json({ error: 'The "targetUrl" field is required.' });
+ }
+
+ const originalRobot = await Robot.findOne({ where: { 'recording_meta.id': id } });
+
+ if (!originalRobot) {
+ return res.status(404).json({ error: 'Original robot not found.' });
+ }
+
+ const lastWord = targetUrl.split('/').filter(Boolean).pop() || 'Unnamed';
+
+ const workflow = originalRobot.recording.workflow.map((step) => {
+ if (step.where?.url && step.where.url !== "about:blank") {
+ step.where.url = targetUrl;
+ }
+
+ step.what.forEach((action) => {
+ if (action.action === "goto" && action.args?.length) {
+ action.args[0] = targetUrl;
+ }
+ });
+
+ return step;
+ });
+
+ const currentTimestamp = new Date().toISOString();
+
+ const newRobot = await Robot.create({
+ id: uuid(),
+ userId: originalRobot.userId,
+ recording_meta: {
+ ...originalRobot.recording_meta,
+ id: uuid(),
+ name: `${originalRobot.recording_meta.name} (${lastWord})`,
+ createdAt: currentTimestamp,
+ updatedAt: currentTimestamp,
+ },
+ recording: { ...originalRobot.recording, workflow },
+ google_sheet_email: null,
+ google_sheet_name: null,
+ google_sheet_id: null,
+ google_access_token: null,
+ google_refresh_token: null,
+ schedule: null,
+ });
+
+ logger.log('info', `Robot with ID ${id} duplicated successfully as ${newRobot.id}.`);
+
+ return res.status(201).json({
+ message: 'Robot duplicated and target URL updated successfully.',
+ robot: newRobot,
+ });
+ } catch (error) {
+ if (error instanceof Error) {
+ logger.log('error', `Error duplicating robot with ID ${req.params.id}: ${error.message}`);
+ return res.status(500).json({ error: error.message });
+ } else {
+ logger.log('error', `Unknown error duplicating robot with ID ${req.params.id}`);
+ return res.status(500).json({ error: 'An unknown error occurred.' });
+ }
+ }
+});
+
/**
* DELETE endpoint for deleting a recording from the storage.
*/
diff --git a/server/src/storage/mino.ts b/server/src/storage/mino.ts
index 04efaf8c6..0f29c3cdf 100644
--- a/server/src/storage/mino.ts
+++ b/server/src/storage/mino.ts
@@ -12,47 +12,48 @@ const minioClient = new Client({
minioClient.bucketExists('maxun-test')
.then((exists) => {
if (exists) {
- console.log('MinIO was connected successfully.');
+ console.log('MinIO connected successfully.');
} else {
- console.log('Bucket does not exist, but MinIO was connected.');
+ console.log('MinIO connected successfully.');
}
})
.catch((err) => {
console.error('Error connecting to MinIO:', err);
})
-async function createBucketWithPolicy(bucketName: string, policy?: 'public-read' | 'private') {
+async function createBucketWithPolicy(bucketName: string, policy = 'public-read') {
try {
const bucketExists = await minioClient.bucketExists(bucketName);
if (!bucketExists) {
await minioClient.makeBucket(bucketName);
console.log(`Bucket ${bucketName} created successfully.`);
-
- if (policy === 'public-read') {
- // Define a public-read policy
- const policyJSON = {
- Version: "2012-10-17",
- Statement: [
- {
- Effect: "Allow",
- Principal: "",
- Action: ["s3:GetObject"],
- Resource: [`arn:aws:s3:::${bucketName}/*`]
- }
- ]
- };
- await minioClient.setBucketPolicy(bucketName, JSON.stringify(policyJSON));
- console.log(`Public-read policy applied to bucket ${bucketName}.`);
- }
} else {
console.log(`Bucket ${bucketName} already exists.`);
}
+
+ if (policy === 'public-read') {
+ // Apply public-read policy after confirming the bucket exists
+ const policyJSON = {
+ Version: "2012-10-17",
+ Statement: [
+ {
+ Effect: "Allow",
+ Principal: "*",
+ Action: ["s3:GetObject"],
+ Resource: [`arn:aws:s3:::${bucketName}/*`]
+ }
+ ]
+ };
+ await minioClient.setBucketPolicy(bucketName, JSON.stringify(policyJSON));
+ console.log(`Public-read policy applied to bucket ${bucketName}.`);
+ }
} catch (error) {
console.error('Error in bucket creation or policy application:', error);
}
}
+
class BinaryOutputService {
private bucketName: string;
diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts
index f8313df79..ddab4bd40 100644
--- a/server/src/utils/auth.ts
+++ b/server/src/utils/auth.ts
@@ -6,29 +6,37 @@ export const hashPassword = (password: string): Promise => {
return new Promise((resolve, reject) => {
bcrypt.genSalt(12, (err, salt) => {
if (err) {
- reject(err)
+ reject(err);
}
bcrypt.hash(password, salt, (err, hash) => {
if (err) {
- reject(err)
+ reject(err);
}
- resolve(hash)
- })
- })
- })
-}
+ resolve(hash);
+ });
+ });
+ });
+};
// password from frontend and hash from database
export const comparePassword = (password: string, hash: string): Promise => {
- return bcrypt.compare(password, hash)
-}
+ return bcrypt.compare(password, hash);
+};
export const encrypt = (text: string): string => {
const ivLength = 16;
const iv = crypto.randomBytes(ivLength);
const algorithm = 'aes-256-cbc';
- const key = Buffer.from(getEnvVariable('ENCRYPTION_KEY'), 'hex');
- const cipher = crypto.createCipheriv(algorithm, key, iv);
+
+ // Retrieve the encryption key or generate a new one if invalid or empty
+ let key = getEnvVariable('ENCRYPTION_KEY');
+ if (!key || key.length !== 64) { // aes-256-cbc requires a 256-bit key, which is 64 hex characters
+ console.warn('Invalid or missing ENCRYPTION_KEY, generating a new one.');
+ key = crypto.randomBytes(32).toString('hex'); // Generate a new 256-bit (32-byte) key
+ }
+ const keyBuffer = Buffer.from(key, 'hex');
+
+ const cipher = crypto.createCipheriv(algorithm, keyBuffer, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}`;
@@ -37,9 +45,17 @@ export const encrypt = (text: string): string => {
export const decrypt = (encryptedText: string): string => {
const [iv, encrypted] = encryptedText.split(':');
const algorithm = "aes-256-cbc";
- const key = Buffer.from(getEnvVariable('ENCRYPTION_KEY'), 'hex');
- const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(iv, 'hex'));
+
+ // Retrieve the encryption key or generate a new one if invalid or empty
+ let key = getEnvVariable('ENCRYPTION_KEY');
+ if (!key || key.length !== 64) { // aes-256-cbc requires a 256-bit key, which is 64 hex characters
+ console.warn('Invalid or missing ENCRYPTION_KEY, generating a new one.');
+ key = crypto.randomBytes(32).toString('hex'); // Generate a new 256-bit (32-byte) key
+ }
+ const keyBuffer = Buffer.from(key, 'hex');
+
+ const decipher = crypto.createDecipheriv(algorithm, keyBuffer, Buffer.from(iv, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
-};
\ No newline at end of file
+};
diff --git a/server/src/worker.ts b/server/src/worker.ts
index fd3470d49..3a82ee737 100644
--- a/server/src/worker.ts
+++ b/server/src/worker.ts
@@ -5,11 +5,6 @@ import { handleRunRecording } from "./workflow-management/scheduler";
import Robot from './models/Robot';
import { computeNextRun } from './utils/schedule';
-console.log('Environment variables:', {
- REDIS_HOST: process.env.REDIS_HOST,
- REDIS_PORT: process.env.REDIS_PORT,
-});
-
const connection = new IORedis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379,
@@ -68,7 +63,6 @@ console.log('Worker is running...');
async function jobCounts() {
const jobCounts = await workflowQueue.getJobCounts();
- console.log('Jobs:', jobCounts);
}
jobCounts();
@@ -78,8 +72,4 @@ process.on('SIGINT', () => {
process.exit();
});
-export { workflowQueue, worker };
-
-export const temp = () => {
- console.log('temp');
-}
\ No newline at end of file
+export { workflowQueue, worker };
\ No newline at end of file
diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts
index a2db820e7..7801a20ec 100644
--- a/server/src/workflow-management/classes/Generator.ts
+++ b/server/src/workflow-management/classes/Generator.ts
@@ -128,7 +128,6 @@ export class WorkflowGenerator {
*/
private registerEventHandlers = (socket: Socket) => {
socket.on('save', (data) => {
- console.log('Received data:', data);
const { fileName, userId } = data;
logger.log('debug', `Saving workflow ${fileName} for user ID ${userId}`);
this.saveNewWorkflow(fileName, userId);
@@ -560,6 +559,8 @@ export class WorkflowGenerator {
if (this.listSelector !== '') {
const childSelectors = await getChildSelectors(page, this.listSelector || '');
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childSelectors })
+ console.log(`Child Selectors: ${childSelectors}`)
+ console.log(`Parent Selector: ${this.listSelector}`)
} else {
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo });
}
diff --git a/server/src/workflow-management/scheduler/index.ts b/server/src/workflow-management/scheduler/index.ts
index a94c6d16f..082fcf2ed 100644
--- a/server/src/workflow-management/scheduler/index.ts
+++ b/server/src/workflow-management/scheduler/index.ts
@@ -1,5 +1,6 @@
import { uuid } from "uuidv4";
-import { chromium } from "playwright";
+import { chromium } from 'playwright-extra';
+import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { io, Socket } from "socket.io-client";
import { createRemoteBrowserForRun, destroyRemoteBrowser } from '../../browser-management/controller';
import logger from '../../logger';
@@ -10,6 +11,7 @@ import Run from "../../models/Run";
import { getDecryptedProxyConfig } from "../../routes/proxy";
import { BinaryOutputService } from "../../storage/mino";
import { capture } from "../../utils/analytics";
+chromium.use(stealthPlugin());
async function createWorkflowAndStoreMetadata(id: string, userId: string) {
try {
diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts
index 18b878ff5..193de8910 100644
--- a/server/src/workflow-management/selector.ts
+++ b/server/src/workflow-management/selector.ts
@@ -794,6 +794,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates
export const getChildSelectors = async (page: Page, parentSelector: string): Promise => {
try {
const childSelectors = await page.evaluate((parentSelector: string) => {
+ // Function to get a non-unique selector based on tag and class (if present)
function getNonUniqueSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase();
@@ -811,6 +812,7 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro
return selector;
}
+ // Function to generate selector path from an element to its parent
function getSelectorPath(element: HTMLElement | null): string {
if (!element || !element.parentElement) return '';
@@ -820,22 +822,33 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro
return `${parentSelector} > ${elementSelector}`;
}
- function getAllDescendantSelectors(element: HTMLElement, stopAtParent: HTMLElement | null): string[] {
+ // Function to recursively get all descendant selectors
+ function getAllDescendantSelectors(element: HTMLElement): string[] {
let selectors: string[] = [];
const children = Array.from(element.children) as HTMLElement[];
for (const child of children) {
- selectors.push(getSelectorPath(child));
- selectors = selectors.concat(getAllDescendantSelectors(child, stopAtParent));
+ const childPath = getSelectorPath(child);
+ if (childPath) {
+ selectors.push(childPath); // Add direct child path
+ selectors = selectors.concat(getAllDescendantSelectors(child)); // Recursively process descendants
+ }
}
return selectors;
}
- const parentElement = document.querySelector(parentSelector) as HTMLElement;
- if (!parentElement) return [];
+ // Find all occurrences of the parent selector in the DOM
+ const parentElements = Array.from(document.querySelectorAll(parentSelector)) as HTMLElement[];
+ const allChildSelectors = new Set(); // Use a set to ensure uniqueness
+
+ // Process each parent element and its descendants
+ parentElements.forEach((parentElement) => {
+ const descendantSelectors = getAllDescendantSelectors(parentElement);
+ descendantSelectors.forEach((selector) => allChildSelectors.add(selector)); // Add selectors to the set
+ });
- return getAllDescendantSelectors(parentElement, parentElement);
+ return Array.from(allChildSelectors); // Convert the set back to an array
}, parentSelector);
return childSelectors || [];
@@ -845,6 +858,7 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro
}
};
+
/**
* Returns the first pair from the given workflow that contains the given selector
* inside the where condition, and it is the only selector there.
diff --git a/src/api/storage.ts b/src/api/storage.ts
index 9b4b06b2d..4b2f4e80d 100644
--- a/src/api/storage.ts
+++ b/src/api/storage.ts
@@ -5,6 +5,11 @@ import { ScheduleSettings } from "../components/molecules/ScheduleSettings";
import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage";
import { apiUrl } from "../apiConfig";
+
+
+
+
+
export const getStoredRecordings = async (): Promise => {
try {
const response = await axios.get(`${apiUrl}/storage/recordings`);
@@ -19,6 +24,36 @@ export const getStoredRecordings = async (): Promise => {
}
};
+export const updateRecording = async (id: string, data: { name?: string; limit?: number }): Promise => {
+ try {
+ const response = await axios.put(`${apiUrl}/storage/recordings/${id}`, data);
+ if (response.status === 200) {
+ return true;
+ } else {
+ throw new Error(`Couldn't update recording with id ${id}`);
+ }
+ } catch (error: any) {
+ console.error(`Error updating recording: ${error.message}`);
+ return false;
+ }
+};
+
+export const duplicateRecording = async (id: string, targetUrl: string): Promise => {
+ try {
+ const response = await axios.post(`${apiUrl}/storage/recordings/${id}/duplicate`, {
+ targetUrl,
+ });
+ if (response.status === 201) {
+ return response.data; // Returns the duplicated robot details
+ } else {
+ throw new Error(`Couldn't duplicate recording with id ${id}`);
+ }
+ } catch (error: any) {
+ console.error(`Error duplicating recording: ${error.message}`);
+ return null;
+ }
+};
+
export const getStoredRuns = async (): Promise => {
try {
const response = await axios.get(`${apiUrl}/storage/runs`);
@@ -47,18 +82,49 @@ export const getStoredRecording = async (id: string) => {
}
}
+
+
+export const checkRunsForRecording = async (id: string): Promise => {
+
+
+ try {
+ const response = await axios.get(`${apiUrl}/storage/recordings/${id}/runs`);
+
+ const runs = response.data;
+ console.log(runs.runs.totalCount)
+ return runs.runs.totalCount > 0;
+ } catch (error) {
+ console.error('Error checking runs for recording:', error);
+ return false;
+ }
+};
+
+
export const deleteRecordingFromStorage = async (id: string): Promise => {
+
+ const hasRuns = await checkRunsForRecording(id);
+
+ if (hasRuns) {
+
+ return false;
+ }
try {
const response = await axios.delete(`${apiUrl}/storage/recordings/${id}`);
if (response.status === 200) {
- return response.data;
+
+ return true;
} else {
throw new Error(`Couldn't delete stored recording ${id}`);
}
} catch (error: any) {
console.log(error);
+
return false;
}
+
+
+
+
};
export const deleteRunFromStorage = async (id: string): Promise => {
@@ -93,7 +159,7 @@ export const createRunForStoredRecording = async (id: string, settings: RunSetti
try {
const response = await axios.put(
`${apiUrl}/storage/runs/${id}`,
- { ...settings });
+ { ...settings });
if (response.status === 200) {
return response.data;
} else {
diff --git a/src/components/molecules/IntegrationSettings.tsx b/src/components/molecules/IntegrationSettings.tsx
index 1150dd72c..c31605dea 100644
--- a/src/components/molecules/IntegrationSettings.tsx
+++ b/src/components/molecules/IntegrationSettings.tsx
@@ -1,227 +1,323 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect } from "react";
import { GenericModal } from "../atoms/GenericModal";
-import { MenuItem, Typography, CircularProgress, Alert, AlertTitle, Chip } from "@mui/material";
+import {
+ MenuItem,
+ Typography,
+ CircularProgress,
+ Alert,
+ AlertTitle,
+ Chip,
+} from "@mui/material";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
-import axios from 'axios';
-import { useGlobalInfoStore } from '../../context/globalInfo';
-import { getStoredRecording } from '../../api/storage';
-import { apiUrl } from '../../apiConfig.js';
+import axios from "axios";
+import { useGlobalInfoStore } from "../../context/globalInfo";
+import { getStoredRecording } from "../../api/storage";
+import { apiUrl } from "../../apiConfig.js";
+import Cookies from 'js-cookie';
interface IntegrationProps {
- isOpen: boolean;
- handleStart: (data: IntegrationSettings) => void;
- handleClose: () => void;
+ isOpen: boolean;
+ handleStart: (data: IntegrationSettings) => void;
+ handleClose: () => void;
}
export interface IntegrationSettings {
- spreadsheetId: string;
- spreadsheetName: string;
- data: string;
+ spreadsheetId: string;
+ spreadsheetName: string;
+ data: string;
}
-export const IntegrationSettingsModal = ({ isOpen, handleStart, handleClose }: IntegrationProps) => {
- const [settings, setSettings] = useState({
- spreadsheetId: '',
- spreadsheetName: '',
- data: '',
- });
+export const IntegrationSettingsModal = ({
+ isOpen,
+ handleStart,
+ handleClose,
+}: IntegrationProps) => {
+ const [settings, setSettings] = useState({
+ spreadsheetId: "",
+ spreadsheetName: "",
+ data: "",
+ });
- const [spreadsheets, setSpreadsheets] = useState<{ id: string, name: string }[]>([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
+ const [spreadsheets, setSpreadsheets] = useState<
+ { id: string; name: string }[]
+ >([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
- const { recordingId, notify } = useGlobalInfoStore();
- const [recording, setRecording] = useState(null);
+ const { recordingId, notify } = useGlobalInfoStore();
+ const [recording, setRecording] = useState(null);
- const authenticateWithGoogle = () => {
- window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`;
- };
+ const authenticateWithGoogle = () => {
+ window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`;
+ };
- const handleOAuthCallback = async () => {
- try {
- const response = await axios.get(`${apiUrl}/auth/google/callback`);
- const { google_sheet_email, files } = response.data;
- } catch (error) {
- setError('Error authenticating with Google');
- }
- };
+ const handleOAuthCallback = async () => {
+ try {
+ const response = await axios.get(`${apiUrl}/auth/google/callback`);
+ const { google_sheet_email, files } = response.data;
+ } catch (error) {
+ setError("Error authenticating with Google");
+ }
+ };
- const fetchSpreadsheetFiles = async () => {
- try {
- const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, {
- withCredentials: true,
- });
- setSpreadsheets(response.data);
- } catch (error: any) {
- console.error('Error fetching spreadsheet files:', error.response?.data?.message || error.message);
- notify('error', `Error fetching spreadsheet files: ${error.response?.data?.message || error.message}`);
+ const fetchSpreadsheetFiles = async () => {
+ try {
+ const response = await axios.get(
+ `${apiUrl}/auth/gsheets/files?robotId=${recordingId}`,
+ {
+ withCredentials: true,
}
- };
+ );
+ setSpreadsheets(response.data);
+ } catch (error: any) {
+ console.error(
+ "Error fetching spreadsheet files:",
+ error.response?.data?.message || error.message
+ );
+ notify(
+ "error",
+ `Error fetching spreadsheet files: ${
+ error.response?.data?.message || error.message
+ }`
+ );
+ }
+ };
- const handleSpreadsheetSelect = (e: React.ChangeEvent) => {
- const selectedSheet = spreadsheets.find(sheet => sheet.id === e.target.value);
- if (selectedSheet) {
- setSettings({ ...settings, spreadsheetId: selectedSheet.id, spreadsheetName: selectedSheet.name });
- }
- };
+ const handleSpreadsheetSelect = (e: React.ChangeEvent) => {
+ const selectedSheet = spreadsheets.find(
+ (sheet) => sheet.id === e.target.value
+ );
+ if (selectedSheet) {
+ setSettings({
+ ...settings,
+ spreadsheetId: selectedSheet.id,
+ spreadsheetName: selectedSheet.name,
+ });
+ }
+ };
- const updateGoogleSheetId = async () => {
- try {
- const response = await axios.post(
- `${apiUrl}/auth/gsheets/update`,
- { spreadsheetId: settings.spreadsheetId, spreadsheetName: settings.spreadsheetName, robotId: recordingId },
- { withCredentials: true }
- );
- console.log('Google Sheet ID updated:', response.data);
- } catch (error: any) {
- console.error('Error updating Google Sheet ID:', error.response?.data?.message || error.message);
- }
- };
+ const updateGoogleSheetId = async () => {
+ try {
+ const response = await axios.post(
+ `${apiUrl}/auth/gsheets/update`,
+ {
+ spreadsheetId: settings.spreadsheetId,
+ spreadsheetName: settings.spreadsheetName,
+ robotId: recordingId,
+ },
+ { withCredentials: true }
+ );
+ notify(`success`, `Google Sheet selected successfully`)
+ console.log("Google Sheet ID updated:", response.data);
+ } catch (error: any) {
+ console.error(
+ "Error updating Google Sheet ID:",
+ error.response?.data?.message || error.message
+ );
+ }
+ };
- const removeIntegration = async () => {
- try {
- await axios.post(
- `${apiUrl}/auth/gsheets/remove`,
- { robotId: recordingId },
- { withCredentials: true }
- );
-
- setRecording(null);
- setSpreadsheets([]);
- setSettings({ spreadsheetId: '', spreadsheetName: '', data: '' });
- } catch (error: any) {
- console.error('Error removing Google Sheets integration:', error.response?.data?.message || error.message);
- }
+ const removeIntegration = async () => {
+ try {
+ await axios.post(
+ `${apiUrl}/auth/gsheets/remove`,
+ { robotId: recordingId },
+ { withCredentials: true }
+ );
+
+ setRecording(null);
+ setSpreadsheets([]);
+ setSettings({ spreadsheetId: "", spreadsheetName: "", data: "" });
+ } catch (error: any) {
+ console.error(
+ "Error removing Google Sheets integration:",
+ error.response?.data?.message || error.message
+ );
+ }
+ };
+
+ useEffect(() => {
+ // Check if there is a success message in cookies
+ const status = Cookies.get("robot_auth_status");
+ const message = Cookies.get("robot_auth_message");
+
+ if (status === "success" && message) {
+ notify("success", message);
+ // Clear the cookies after reading
+ Cookies.remove("robot_auth_status");
+ Cookies.remove("robot_auth_message");
+ }
+
+ // Check if we're on the callback URL
+ const urlParams = new URLSearchParams(window.location.search);
+ const code = urlParams.get("code");
+ if (code) {
+ handleOAuthCallback();
+ }
+
+ const fetchRecordingInfo = async () => {
+ if (!recordingId) return;
+ const recording = await getStoredRecording(recordingId);
+ if (recording) {
+ setRecording(recording);
+ }
};
- useEffect(() => {
- // Check if we're on the callback URL
- const urlParams = new URLSearchParams(window.location.search);
- const code = urlParams.get('code');
- if (code) {
- handleOAuthCallback();
- }
+ fetchRecordingInfo();
+ }, [recordingId]);
+
+ return (
+
+
+
+ Integrate with Google Sheet{" "}
+ {/* */}
+
+
+ {recording && recording.google_sheet_id ? (
+ <>
+
+ Google Sheet Integrated Successfully.
+ Every time this robot creates a successful run, its captured data
+ is appended to your {recording.google_sheet_name} Google Sheet.
+ You can check the data updates{" "}
+
+ here
+
+ .
+
+ Note: The data extracted before integrating with
+ Google Sheets will not be synced in the Google Sheet. Only the
+ data extracted after the integration will be synced.
+
+
+ >
+ ) : (
+ <>
+ {!recording?.google_sheet_email ? (
+ <>
+
+ If you enable this option, every time this robot runs a task
+ successfully, its captured data will be appended to your
+ Google Sheet.
+
- Integrate with Google Sheet
-
- {recording && recording.google_sheet_id ? (
- <>
-
- Google Sheet Integrated Successfully.
- Every time this robot creates a successful run, its captured data is appended to your {recording.google_sheet_name} Google Sheet. You can check the data updates here.
-
- Note: The data extracted before integrating with Google Sheets will not be synced in the Google Sheet. Only the data extracted after the integration will be synced.
-
-
- >
+ {loading ? (
+
+ ) : error ? (
+ {error}
+ ) : spreadsheets.length === 0 ? (
+ <>
+