Skip to content

Commit 79558b3

Browse files
authored
Merge pull request #363 from RohitR311/cred-chng
feat: ability to update credentials for robots with authentication
2 parents 0773210 + 76d88c9 commit 79558b3

File tree

13 files changed

+273
-16
lines changed

13 files changed

+273
-16
lines changed

public/locales/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"new": "Roboter erstellen",
3333
"modal": {
3434
"title": "Geben Sie die URL ein",
35+
"login_title": "Erfordert diese Seite eine Anmeldung?",
3536
"label": "URL",
3637
"button": "Aufnahme starten"
3738
},

public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"new":"Create Robot",
3333
"modal":{
3434
"title":"Enter the URL",
35+
"login_title": "Does this site require login?",
3536
"label":"URL",
3637
"button":"Start Recording"
3738
},

public/locales/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"new": "Crear Robot",
3333
"modal": {
3434
"title": "Ingresa la URL",
35+
"login_title": "¿Este sitio requiere inicio de sesión?",
3536
"label": "URL",
3637
"button": "Comenzar grabación"
3738
},

public/locales/ja.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"new": "ロボットを作成",
3333
"modal": {
3434
"title": "URLを入力してください",
35+
"login_title": "このサイトはログインが必要ですか?",
3536
"label": "URL",
3637
"button": "録画を開始"
3738
},

public/locales/zh.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"new": "创建机器人",
3333
"modal": {
3434
"title": "输入URL",
35+
"login_title": "此网站需要登录吗?",
3536
"label": "URL",
3637
"button": "开始录制"
3738
},

server/src/models/Robot.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface RobotAttributes {
2626
google_access_token?: string | null;
2727
google_refresh_token?: string | null;
2828
schedule?: ScheduleConfig | null;
29+
isLogin?: boolean;
2930
}
3031

3132
interface ScheduleConfig {
@@ -54,6 +55,7 @@ class Robot extends Model<RobotAttributes, RobotCreationAttributes> implements R
5455
public google_access_token!: string | null;
5556
public google_refresh_token!: string | null;
5657
public schedule!: ScheduleConfig | null;
58+
public isLogin!: boolean;
5759
}
5860

5961
Robot.init(
@@ -99,6 +101,11 @@ Robot.init(
99101
type: DataTypes.JSONB,
100102
allowNull: true,
101103
},
104+
isLogin: {
105+
type: DataTypes.BOOLEAN,
106+
allowNull: false,
107+
defaultValue: false,
108+
},
102109
},
103110
{
104111
sequelize,

server/src/routes/storage.ts

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,43 @@ import { AuthenticatedRequest } from './record';
1818
import { computeNextRun } from '../utils/schedule';
1919
import { capture } from "../utils/analytics";
2020
import { tryCatch } from 'bullmq';
21+
import { encrypt, decrypt } from '../utils/auth';
2122
import { WorkflowFile } from 'maxun-core';
2223
import { Page } from 'playwright';
2324
chromium.use(stealthPlugin());
2425

2526
export const router = Router();
2627

28+
export const decryptWorkflowActions = async (workflow: any[],): Promise<any[]> => {
29+
// Create a deep copy to avoid mutating the original workflow
30+
const processedWorkflow = JSON.parse(JSON.stringify(workflow));
31+
32+
// Process each step in the workflow
33+
for (const step of processedWorkflow) {
34+
if (!step.what) continue;
35+
36+
// Process each action in the step
37+
for (const action of step.what) {
38+
// Only process type and press actions
39+
if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) {
40+
// The second argument contains the encrypted value
41+
const encryptedValue = action.args[1];
42+
if (typeof encryptedValue === 'string') {
43+
try {
44+
// Decrypt the value and update the args array
45+
action.args[1] = await decrypt(encryptedValue);
46+
} catch (error) {
47+
console.error('Failed to decrypt value:', error);
48+
// Keep the encrypted value if decryption fails
49+
}
50+
}
51+
}
52+
}
53+
}
54+
55+
return processedWorkflow;
56+
};
57+
2758
/**
2859
* Logs information about recordings API.
2960
*/
@@ -55,6 +86,13 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => {
5586
raw: true
5687
}
5788
);
89+
90+
if (data?.recording?.workflow) {
91+
data.recording.workflow = await decryptWorkflowActions(
92+
data.recording.workflow,
93+
);
94+
}
95+
5896
return res.send(data);
5997
} catch (e) {
6098
logger.log('info', 'Error while reading robots');
@@ -116,13 +154,70 @@ function formatRunResponse(run: any) {
116154
return formattedRun;
117155
}
118156

157+
interface CredentialUpdate {
158+
[selector: string]: string;
159+
}
160+
161+
function updateTypeActionsInWorkflow(workflow: any[], credentials: CredentialUpdate) {
162+
return workflow.map(step => {
163+
if (!step.what) return step;
164+
165+
// First pass: mark indices to remove
166+
const indicesToRemove = new Set<number>();
167+
step.what.forEach((action: any, index: any) => {
168+
if (!action.action || !action.args?.[0]) return;
169+
170+
// If it's a type/press action for a credential
171+
if ((action.action === 'type' || action.action === 'press') && credentials[action.args[0]]) {
172+
indicesToRemove.add(index);
173+
// Check if next action is waitForLoadState
174+
if (step.what[index + 1]?.action === 'waitForLoadState') {
175+
indicesToRemove.add(index + 1);
176+
}
177+
}
178+
});
179+
180+
// Filter out marked indices and create new what array
181+
const filteredWhat = step.what.filter((_: any, index: any) => !indicesToRemove.has(index));
182+
183+
// Add new type actions after click actions
184+
Object.entries(credentials).forEach(([selector, credential]) => {
185+
const clickIndex = filteredWhat.findIndex((action: any) =>
186+
action.action === 'click' && action.args?.[0] === selector
187+
);
188+
189+
if (clickIndex !== -1) {
190+
const chars = credential.split('');
191+
chars.forEach((char, i) => {
192+
// Add type action
193+
filteredWhat.splice(clickIndex + 1 + (i * 2), 0, {
194+
action: 'type',
195+
args: [selector, encrypt(char)]
196+
});
197+
198+
// Add waitForLoadState
199+
filteredWhat.splice(clickIndex + 2 + (i * 2), 0, {
200+
action: 'waitForLoadState',
201+
args: ['networkidle']
202+
});
203+
});
204+
}
205+
});
206+
207+
return {
208+
...step,
209+
what: filteredWhat
210+
};
211+
});
212+
}
213+
119214
/**
120215
* PUT endpoint to update the name and limit of a robot.
121216
*/
122217
router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, res) => {
123218
try {
124219
const { id } = req.params;
125-
const { name, limit } = req.body;
220+
const { name, limit, credentials } = req.body;
126221

127222
// Validate input
128223
if (!name && limit === undefined) {
@@ -141,17 +236,21 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r
141236
robot.set('recording_meta', { ...robot.recording_meta, name });
142237
}
143238

239+
let workflow = [...robot.recording.workflow]; // Create a copy of the workflow
240+
241+
if (credentials) {
242+
workflow = updateTypeActionsInWorkflow(workflow, credentials);
243+
}
244+
144245
// Update the limit
145246
if (limit !== undefined) {
146-
const workflow = [...robot.recording.workflow]; // Create a copy of the workflow
147-
148247
// Ensure the workflow structure is valid before updating
149248
if (
150249
workflow.length > 0 &&
151250
workflow[0]?.what?.[0]
152251
) {
153252
// Create a new workflow object with the updated limit
154-
const updatedWorkflow = workflow.map((step, index) => {
253+
workflow = workflow.map((step, index) => {
155254
if (index === 0) { // Assuming you want to update the first step
156255
return {
157256
...step,
@@ -173,14 +272,13 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r
173272
}
174273
return step;
175274
});
176-
177-
// Replace the workflow in the recording object
178-
robot.set('recording', { ...robot.recording, workflow: updatedWorkflow });
179275
} else {
180276
return res.status(400).json({ error: 'Invalid workflow structure for updating limit.' });
181277
}
182278
}
183279

280+
robot.set('recording', { ...robot.recording, workflow });
281+
184282
await robot.save();
185283

186284
const updatedRobot = await Robot.findOne({ where: { 'recording_meta.id': id } });
@@ -248,6 +346,7 @@ router.post('/recordings/:id/duplicate', requireSignIn, async (req: Authenticate
248346
updatedAt: currentTimestamp,
249347
},
250348
recording: { ...originalRobot.recording, workflow },
349+
isLogin: originalRobot.isLogin,
251350
google_sheet_email: null,
252351
google_sheet_name: null,
253352
google_sheet_id: null,

server/src/workflow-management/classes/Generator.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,9 @@ export class WorkflowGenerator {
134134
*/
135135
private registerEventHandlers = (socket: Socket) => {
136136
socket.on('save', (data) => {
137-
const { fileName, userId } = data;
137+
const { fileName, userId, isLogin } = data;
138138
logger.log('debug', `Saving workflow ${fileName} for user ID ${userId}`);
139-
this.saveNewWorkflow(fileName, userId);
139+
this.saveNewWorkflow(fileName, userId, isLogin);
140140
});
141141
socket.on('new-recording', () => this.workflowRecord = {
142142
workflow: [],
@@ -660,7 +660,7 @@ export class WorkflowGenerator {
660660
* @param fileName The name of the file.
661661
* @returns {Promise<void>}
662662
*/
663-
public saveNewWorkflow = async (fileName: string, userId: number) => {
663+
public saveNewWorkflow = async (fileName: string, userId: number, isLogin: boolean) => {
664664
const recording = this.optimizeWorkflow(this.workflowRecord);
665665
try {
666666
this.recordingMeta = {
@@ -675,6 +675,7 @@ export class WorkflowGenerator {
675675
userId,
676676
recording_meta: this.recordingMeta,
677677
recording: recording,
678+
isLogin: isLogin,
678679
});
679680
capture(
680681
'maxun-oss-robot-created',

src/api/storage.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { ScheduleSettings } from "../components/robot/ScheduleSettings";
55
import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage";
66
import { apiUrl } from "../apiConfig";
77

8+
interface Credentials {
9+
[key: string]: string;
10+
}
11+
812
export const getStoredRecordings = async (): Promise<string[] | null> => {
913
try {
1014
const response = await axios.get(`${apiUrl}/storage/recordings`);
@@ -19,7 +23,7 @@ export const getStoredRecordings = async (): Promise<string[] | null> => {
1923
}
2024
};
2125

22-
export const updateRecording = async (id: string, data: { name?: string; limit?: number }): Promise<boolean> => {
26+
export const updateRecording = async (id: string, data: { name?: string; limit?: number, credentials?: Credentials }): Promise<boolean> => {
2327
try {
2428
const response = await axios.put(`${apiUrl}/storage/recordings/${id}`, data);
2529
if (response.status === 200) {

src/components/recorder/SaveRecording.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
2222
const [recordingName, setRecordingName] = useState<string>(fileName);
2323
const [waitingForSave, setWaitingForSave] = useState<boolean>(false);
2424

25-
const { browserId, setBrowserId, notify, recordings } = useGlobalInfoStore();
25+
const { browserId, setBrowserId, notify, recordings, isLogin } = useGlobalInfoStore();
2626
const { socket } = useSocketStore();
2727
const { state, dispatch } = useContext(AuthContext);
2828
const { user } = state;
@@ -59,7 +59,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
5959
// releases resources and changes the view for main page by clearing the global browserId
6060
const saveRecording = async () => {
6161
if (user) {
62-
const payload = { fileName: recordingName, userId: user.id };
62+
const payload = { fileName: recordingName, userId: user.id, isLogin: isLogin };
6363
socket?.emit('save', payload);
6464
setWaitingForSave(true);
6565
console.log(`Saving the recording as ${recordingName} for userId ${user.id}`);

0 commit comments

Comments
 (0)