Merge branch 'integration_airtable' of https://github.com/AmitChauhan63390/maxun into integration_airtable

This commit is contained in:
Rohit
2025-02-26 14:49:03 +05:30
26 changed files with 1455 additions and 660 deletions

View File

@@ -28,29 +28,47 @@ router.post("/register", async (req, res) => {
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");
if (!email) {
return res.status(400).json({
error: "VALIDATION_ERROR",
code: "register.validation.email_required"
});
}
if (!password || password.length < 6) {
return res.status(400).json({
error: "VALIDATION_ERROR",
code: "register.validation.password_requirements"
});
}
let userExist = await User.findOne({ raw: true, where: { email } });
if (userExist) return res.status(400).send("User already exists");
if (userExist) {
return res.status(400).json({
error: "USER_EXISTS",
code: "register.error.user_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}`);
return res.status(500).json({
error: "DATABASE_ERROR",
code: "register.error.creation_failed"
});
}
if (!process.env.JWT_SECRET) {
console.log("JWT_SECRET is not defined in the environment");
return res.status(500).send("Internal Server Error");
return res.status(500).json({
error: "SERVER_ERROR",
code: "register.error.server_error"
});
}
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET as string);
@@ -58,36 +76,60 @@ router.post("/register", async (req, res) => {
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`);
res.json(user);
} catch (error: any) {
console.log(`Could not register user - ${error}`);
res.status(500).send(`Could not register user - ${error.message}`);
return res.status(500).json({
error: "SERVER_ERROR",
code: "register.error.generic"
});
}
});
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");
if (!email || !password) {
return res.status(400).json({
error: "VALIDATION_ERROR",
code: "login.validation.required_fields"
});
}
if (password.length < 6) {
return res.status(400).json({
error: "VALIDATION_ERROR",
code: "login.validation.password_length"
});
}
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({
error: "USER_NOT_FOUND",
code: "login.error.user_not_found"
});
}
const match = await comparePassword(password, user.password);
if (!match) return res.status(400).send("Invalid email or password");
if (!match) {
return res.status(401).json({
error: "INVALID_CREDENTIALS",
code: "login.error.invalid_credentials"
});
}
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;
}
@@ -101,30 +143,43 @@ router.post("/login", async (req, res) => {
});
res.json(user);
} catch (error: any) {
res.status(400).send(`Could not login user - ${error.message}`);
console.log(`Could not login user - ${error}`);
console.error(`Login error: ${error.message}`);
res.status(500).json({
error: "SERVER_ERROR",
code: "login.error.server_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}`);
try {
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",
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) {
@@ -147,7 +202,7 @@ router.get(
router.get(
"/user/:id",
requireSignIn,
async (req: Request, res) => {
async (req: AuthenticatedRequest, res) => {
try {
const { id } = req.params;
if (!id) {
@@ -176,13 +231,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"] },
});
@@ -217,28 +271,41 @@ 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, error: "Unauthorized" });
if (!req.user) {
return res.status(401).json({
ok: false,
error: "Unauthorized",
code: "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({ 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",
});
}
}
);
@@ -246,14 +313,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" });
@@ -263,7 +329,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,
@@ -309,8 +375,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) {
@@ -336,12 +401,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" });
@@ -419,13 +484,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" });
@@ -537,14 +601,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" });
}
@@ -566,7 +629,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(),
});
@@ -866,4 +929,6 @@ router.get("/airtable/tables", requireSignIn,async (req: Request, res) => {
} catch (error: any) {
res.status(500).json({ message: error.message });
}
});
});

View File

@@ -26,35 +26,43 @@ chromium.use(stealthPlugin());
export const router = Router();
export const decryptWorkflowActions = async (workflow: any[],): Promise<any[]> => {
// 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<any[]> => {
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.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.
@@ -89,7 +97,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,
);
}
@@ -164,54 +172,82 @@ interface Credentials {
[key: string]: CredentialInfo;
}
function updateTypeActionsInWorkflow(workflow: any[], credentials: Credentials) {
function handleWorkflowActions(workflow: any[], credentials: Credentials) {
return workflow.map(step => {
if (!step.what) return step;
const indicesToRemove = new Set<number>();
step.what.forEach((action: any, index: number) => {
if (!action.action || !action.args?.[0]) return;
const newWhat: any[] = [];
const processedSelectors = new Set<string>();
for (let i = 0; i < step.what.length; i++) {
const action = step.what[i];
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);
}
if (!action?.action || !action?.args?.[0]) {
newWhat.push(action);
continue;
}
});
const filteredWhat = step.what.filter((_: any, index: number) => !indicesToRemove.has(index));
const selector = action.args[0];
const credential = credentials[selector];
Object.entries(credentials).forEach(([selector, credentialInfo]) => {
const clickIndex = filteredWhat.findIndex((action: any) =>
action.action === 'click' && action.args?.[0] === selector
);
if (!credential) {
newWhat.push(action);
continue;
}
if (clickIndex !== -1) {
const chars = credentialInfo.value.split('');
if (action.action === 'click') {
newWhat.push(action);
chars.forEach((char, i) => {
filteredWhat.splice(clickIndex + 1 + (i * 2), 0, {
if (!processedSelectors.has(selector) &&
i + 1 < step.what.length &&
(step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press')) {
newWhat.push({
action: 'type',
args: [
selector,
encrypt(char),
credentialInfo.type
]
args: [selector, encrypt(credential.value), credential.type]
});
filteredWhat.splice(clickIndex + 2 + (i * 2), 0, {
newWhat.push({
action: 'waitForLoadState',
args: ['networkidle']
});
processedSelectors.add(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)) {
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 {
...step,
what: filteredWhat
what: newWhat
};
});
}
@@ -244,7 +280,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
@@ -282,9 +318,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 } });
@@ -502,6 +552,7 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) =>
return res.send({
browserId: id,
runId: plainRun.runId,
robotMetaId: recording.recording_meta.id,
});
} catch (e) {
const { message } = e as Error;