From 49c8211f26f7d72a25209586b200603464877849 Mon Sep 17 00:00:00 2001 From: Sneha Kumari Date: Tue, 23 Jun 2026 17:55:45 +0530 Subject: [PATCH 1/3] feat: chrome extension backend changes --- .../AuthenticateExtensionUserReactor.java | 174 +++++++++++ .../SaveRecordingFromExtensionReactor.java | 289 ++++++++++++++++++ 2 files changed, 463 insertions(+) create mode 100644 src/prerna/reactor/playwright/AuthenticateExtensionUserReactor.java create mode 100644 src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java diff --git a/src/prerna/reactor/playwright/AuthenticateExtensionUserReactor.java b/src/prerna/reactor/playwright/AuthenticateExtensionUserReactor.java new file mode 100644 index 00000000000..9778b08ba60 --- /dev/null +++ b/src/prerna/reactor/playwright/AuthenticateExtensionUserReactor.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.playwright; + +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONArray; +import org.json.JSONObject; + +import prerna.auth.AccessToken; +import prerna.auth.User; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.auth.utils.SecurityUserAccessKeyUtils; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; + +/** + * Authenticates Chrome Extension users via API keys and returns accessible projects. + * + *

Pixel Syntax:

+ *
AuthenticateExtensionUser(clientKey=[string], secretKey=[string])
+ * + *

Parameters:

+ * + * + *

Returns:

+ *
+ * {
+ *   "success": true,
+ *   "userId": "user123",
+ *   "userName": "John Doe",
+ *   "userEmail": "john@example.com",
+ *   "projects": [
+ *     {
+ *       "id": "project-uuid",
+ *       "name": "ProjectAlias",
+ *       "displayName": "Project Display Name",
+ *       "canEdit": true
+ *     }
+ *   ]
+ * }
+ * 
+ * + *

Note: Uses string literals "clientKey" and "secretKey" as ReactorKeysEnum + * does not contain CLIENT_KEY or SECRET_KEY constants.

+ */ +public class AuthenticateExtensionUserReactor extends AbstractReactor { + + private static final Logger classLogger = LogManager.getLogger(AuthenticateExtensionUserReactor.class); + + public AuthenticateExtensionUserReactor() { + // Note: String literals used as CLIENT_KEY and SECRET_KEY don't exist in ReactorKeysEnum + this.keysToGet = new String[] { + "clientKey", + "secretKey" + }; + this.keyRequired = new int[] { 1, 1 }; + } + + @Override + public NounMetadata execute() { + organizeKeys(); + + String clientKey = this.keyValue.get("clientKey"); + String secretKey = this.keyValue.get("secretKey"); + + classLogger.info("Authenticating extension user with client key"); + + // Validate keys and get access token + AccessToken token = null; + try { + token = SecurityUserAccessKeyUtils.validateKeysAndReturnToken(clientKey, secretKey); + } catch (IllegalAccessException e) { + classLogger.error("Authentication failed for client key: {}", clientKey, e); + throw new IllegalArgumentException("Invalid credentials: " + e.getMessage()); + } + + if (token == null) { + throw new IllegalArgumentException("Invalid credentials"); + } + + // Convert token to user + User user = new User(); + user.setAccessToken(token); + + // Get user's accessible projects + List projectIds = SecurityProjectUtils.getFullUserProjectIds(user); + + // Get user info from access token + String userId = token.getId(); + String userName = token.getName(); + String userEmail = token.getEmail() != null ? token.getEmail() : ""; + + // Build response with user info and projects + JSONObject response = new JSONObject(); + response.put("success", true); + response.put("userId", userId); + response.put("userName", userName); + response.put("userEmail", userEmail); + + // Build projects array with details + JSONArray projects = new JSONArray(); + for (String projectId : projectIds) { + try { + JSONObject project = new JSONObject(); + project.put("id", projectId); + project.put("name", SecurityProjectUtils.getProjectAliasForId(projectId)); + + String displayName = SecurityProjectUtils.getProjectDisplayNameForId(projectId); + project.put("displayName", displayName != null ? displayName : SecurityProjectUtils.getProjectAliasForId(projectId)); + + // Check if user has edit permission + boolean canEdit = SecurityProjectUtils.userCanEditProject(user, projectId); + project.put("canEdit", canEdit); + + projects.put(project); + } catch (Exception e) { + classLogger.warn("Could not load details for project: {}", projectId, e); + } + } + response.put("projects", projects); + + classLogger.info("Successfully authenticated user: {} with {} accessible projects", userName, projectIds.size()); + + return new NounMetadata(response, PixelDataType.JSON_OBJECT); + } + + @Override + public String getReactorDescription() { + return "Authenticates Chrome Extension user using API keys and returns accessible projects"; + } + + @Override + protected String getDescriptionForKey(String key) { + if (key.equals("clientKey")) { + return "The client/access key from Semoss user settings"; + } else if (key.equals("secretKey")) { + return "The secret key from Semoss user settings"; + } + return super.getDescriptionForKey(key); + } +} \ No newline at end of file diff --git a/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java b/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java new file mode 100644 index 00000000000..31d08ca74ad --- /dev/null +++ b/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java @@ -0,0 +1,289 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.playwright; + +import java.io.FileWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONObject; + +import prerna.auth.AccessToken; +import prerna.auth.User; +import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.auth.utils.SecurityUserAccessKeyUtils; +import prerna.cluster.util.ClusterUtil; +import prerna.project.api.IProject; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.AssetUtility; +import prerna.util.Constants; +import prerna.util.Utility; +import prerna.util.git.GitRepoUtils; + +/** + * Saves a recording from Chrome Extension to project recordings folder and auto-updates MCP. + * Supports dual authentication: API keys (for extension) or session-based (for UI). + * + *

Pixel Syntax:

+ *
SaveRecordingFromExtension(project=[string], name=[string], jsonPayload=[string], 
+ *                             title=[string], description=[string], intent=[string])
+ * + *

Parameters:

+ * + * + *

Returns:

+ *
+ * {
+ *   "success": true,
+ *   "fileName": "recording.json",
+ *   "filePath": "/path/to/recording.json",
+ *   "message": "Recording saved successfully"
+ * }
+ * 
+ * + *

Note: Uses string literals for "name", "jsonPayload", "title", "intent" as these keys + * are specific to Playwright recordings and don't exist in ReactorKeysEnum. Uses + * ReactorKeysEnum.DESCRIPTION for description parameter.

+ */ +public class SaveRecordingFromExtensionReactor extends AbstractReactor { + + private static final Logger classLogger = LogManager.getLogger(SaveRecordingFromExtensionReactor.class); + + public SaveRecordingFromExtensionReactor() { + // Note: String literals used for name, jsonPayload, title, intent as they are + // Playwright-specific and don't exist in ReactorKeysEnum + this.keysToGet = new String[] { + ReactorKeysEnum.PROJECT.getKey(), + "name", + "jsonPayload", + "title", + ReactorKeysEnum.DESCRIPTION.getKey(), + "intent" + }; + this.keyRequired = new int[] { 1, 1, 1, 0, 0, 0 }; + } + + @Override + public NounMetadata execute() { + organizeKeys(); + + User user = null; + + // Try API key authentication first (for extension usage without session) + String clientKey = getRequestParameter("clientKey"); + String secretKey = getRequestParameter("secretKey"); + + if (clientKey != null && secretKey != null && !clientKey.isEmpty() && !secretKey.isEmpty()) { + // Authenticate using API keys + AccessToken token = null; + try { + token = SecurityUserAccessKeyUtils.validateKeysAndReturnToken(clientKey, secretKey); + classLogger.info("User authenticated via API keys: {}", token.getId()); + } catch (IllegalAccessException e) { + classLogger.error("API key authentication failed: {}", e.getMessage(), e); + throw new IllegalArgumentException("Invalid API credentials: " + e.getMessage()); + } + + if (token == null) { + throw new IllegalArgumentException("Invalid API credentials"); + } + + // Convert token to user + user = new User(); + user.setAccessToken(token); + } else { + // Fall back to session-based authentication + user = this.insight.getUser(); + + // Check if user is logged in + if (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous()) { + throwAnonymousUserError(); + } + } + + String projectId = this.keyValue.get(this.keysToGet[0]); + // Note: name, title, intent may need URL decoding if coming from form data + String name = this.keyValue.get(this.keysToGet[1]); + String jsonPayload = this.keyValue.get(this.keysToGet[2]); + String title = this.keyValue.get(this.keysToGet[3]); + String desc = this.keyValue.get(this.keysToGet[4]); + String intent = this.keyValue.get(this.keysToGet[5]); + + // Validate user has edit access to project + if (!SecurityProjectUtils.userCanEditProject(user, projectId)) { + throw new IllegalArgumentException( + "Project " + projectId + " does not exist or user does not have access to edit."); + } + + IProject project = Utility.getProject(projectId); + + // Validate JSON payload + try { + GSON.fromJson(jsonPayload, Object.class); + } catch (Exception e) { + classLogger.error("Invalid JSON payload: {}", e.getMessage(), e); + throw new IllegalArgumentException("Invalid JSON payload: " + e.getMessage()); + } + + // Sanitize filename + String base = PlaywrightUtility.sanitizeFilename( + name == null || name.isBlank() ? ("Recording-" + PlaywrightUtility.generateTimestamp()) : name); + String fileName = base.endsWith(".json") ? base : (base + ".json"); + + // Get recordings directory + Path recordingsDir = PlaywrightUtility.initRecordingsDir(projectId); + Path file = recordingsDir.resolve(fileName); + + // Check if file exists and auto-increment filename to avoid overwrite + if (Files.exists(file)) { + String baseName = fileName.substring(0, fileName.length() - 5); // Remove .json + int counter = 1; + do { + fileName = baseName + "_" + counter + ".json"; + file = recordingsDir.resolve(fileName); + counter++; + } while (Files.exists(file)); + classLogger.info("File already exists, using auto-incremented name: {}", fileName); + } + + // Save the JSON file - extension already formats it correctly with JSON.stringify() + try (FileWriter writer = new FileWriter(file.toFile())) { + // Validate JSON format but don't reformat (preserve extension's formatting) + GSON.fromJson(jsonPayload, Object.class); + // Write original string from extension + writer.write(jsonPayload); + classLogger.info("Saved recording to: {}", file.toAbsolutePath()); + } catch (Exception e) { + classLogger.error("Failed to save recording to: {}", file, e); + throw new RuntimeException("Failed to save recording to: " + file, e); + } + + // Auto-update MCP by calling MakePlaywrightMCPReactor + try { + MakePlaywrightMCPReactor mcpReactor = new MakePlaywrightMCPReactor(); + mcpReactor.setInsight(this.insight); + mcpReactor.setNounStore(this.store); + + // Set the project parameter + this.store.makeNoun(ReactorKeysEnum.PROJECT.getKey()).add(new NounMetadata(projectId, PixelDataType.CONST_STRING)); + + mcpReactor.execute(); + classLogger.info("MCP updated successfully"); + } catch (Exception e) { + classLogger.error("Failed to update MCP: {}", e.getMessage(), e); + // Don't fail the whole operation, just log the error + } + + // Git operations + String versionGitFolder = AssetUtility.getProjectVersionFolder(project.getProjectName(), + project.getProjectId()); + String assetFolder = AssetUtility.getProjectAssetsFolder(project.getProjectName(), project.getProjectId()); + String comment = "Added recording from Chrome Extension: " + fileName; + + // Add recording file to git + List gitRelativeFilePaths = new ArrayList<>(); + gitRelativeFilePaths.add(Constants.ASSETS_FOLDER + "/recordings/" + fileName); + + // Get the user's email + AccessToken accessToken = user.getAccessToken(user.getPrimaryLogin()); + String email = accessToken.getEmail(); + String author = accessToken.getUsername(); + + try { + GitRepoUtils.addSpecificFiles(versionGitFolder, gitRelativeFilePaths); + GitRepoUtils.commitAddedFiles(versionGitFolder, comment, author, email); + ClusterUtil.pushProjectFolder(project, assetFolder); + classLogger.info("Recording committed to git"); + } catch (Exception e) { + classLogger.error("Git operations failed: {}", e.getMessage(), e); + // Don't fail the whole operation + } + + // Return success response + JSONObject response = new JSONObject(); + response.put("success", true); + response.put("fileName", fileName); + response.put("filePath", file.toAbsolutePath().toString()); + response.put("message", "Recording saved successfully"); + + return new NounMetadata(response, PixelDataType.JSON_OBJECT); + } + + @Override + public String getReactorDescription() { + return "Saves a recording from Chrome Extension to project recordings folder and updates MCP"; + } + + @Override + protected String getDescriptionForKey(String key) { + if (key.equals("name")) { + return "The name of the recording file"; + } else if (key.equals("jsonPayload")) { + return "The complete recording JSON as a string"; + } else if (key.equals(ReactorKeysEnum.DESCRIPTION.getKey())) { + return "The description of the recording"; + } else if (key.equals("title")) { + return "The title of the recording"; + } else if (key.equals("intent")) { + return "The intention or purpose of the recording"; + } + return super.getDescriptionForKey(key); + } + + /** + * Gets a parameter from the request (supports both form data and query parameters) + * + * @param paramName the parameter name + * @return the parameter value or null if not found + */ + private String getRequestParameter(String paramName) { + // Try to get from keyValue first (URL encoded form data) + if (this.keyValue.containsKey(paramName)) { + return this.keyValue.get(paramName); + } + + return null; + } +} \ No newline at end of file From bb5aa780c0bec1c8739d0b6995380d6bbdd0bace Mon Sep 17 00:00:00 2001 From: kritysingh630 Date: Fri, 26 Jun 2026 13:10:31 +0530 Subject: [PATCH 2/3] feat: refactor browser extension reactors --- .../AuthenticateExtensionUserReactor.java | 174 ------------------ .../SaveRecordingFromExtensionReactor.java | 58 +----- 2 files changed, 7 insertions(+), 225 deletions(-) delete mode 100644 src/prerna/reactor/playwright/AuthenticateExtensionUserReactor.java diff --git a/src/prerna/reactor/playwright/AuthenticateExtensionUserReactor.java b/src/prerna/reactor/playwright/AuthenticateExtensionUserReactor.java deleted file mode 100644 index 9778b08ba60..00000000000 --- a/src/prerna/reactor/playwright/AuthenticateExtensionUserReactor.java +++ /dev/null @@ -1,174 +0,0 @@ -/******************************************************************************* - * Copyright 2015 Defense Health Agency (DHA) - * - * If your use of this software does not include any GPLv2 components: - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ---------------------------------------------------------------------------- - * If your use of this software includes any GPLv2 components: - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *******************************************************************************/ -package prerna.reactor.playwright; - -import java.util.List; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.json.JSONArray; -import org.json.JSONObject; - -import prerna.auth.AccessToken; -import prerna.auth.User; -import prerna.auth.utils.SecurityProjectUtils; -import prerna.auth.utils.SecurityUserAccessKeyUtils; -import prerna.reactor.AbstractReactor; -import prerna.sablecc2.om.PixelDataType; -import prerna.sablecc2.om.ReactorKeysEnum; -import prerna.sablecc2.om.nounmeta.NounMetadata; - -/** - * Authenticates Chrome Extension users via API keys and returns accessible projects. - * - *

Pixel Syntax:

- *
AuthenticateExtensionUser(clientKey=[string], secretKey=[string])
- * - *

Parameters:

- *
    - *
  • clientKey - The client/access key from Semoss user settings (required)
  • - *
  • secretKey - The secret key from Semoss user settings (required)
  • - *
- * - *

Returns:

- *
- * {
- *   "success": true,
- *   "userId": "user123",
- *   "userName": "John Doe",
- *   "userEmail": "john@example.com",
- *   "projects": [
- *     {
- *       "id": "project-uuid",
- *       "name": "ProjectAlias",
- *       "displayName": "Project Display Name",
- *       "canEdit": true
- *     }
- *   ]
- * }
- * 
- * - *

Note: Uses string literals "clientKey" and "secretKey" as ReactorKeysEnum - * does not contain CLIENT_KEY or SECRET_KEY constants.

- */ -public class AuthenticateExtensionUserReactor extends AbstractReactor { - - private static final Logger classLogger = LogManager.getLogger(AuthenticateExtensionUserReactor.class); - - public AuthenticateExtensionUserReactor() { - // Note: String literals used as CLIENT_KEY and SECRET_KEY don't exist in ReactorKeysEnum - this.keysToGet = new String[] { - "clientKey", - "secretKey" - }; - this.keyRequired = new int[] { 1, 1 }; - } - - @Override - public NounMetadata execute() { - organizeKeys(); - - String clientKey = this.keyValue.get("clientKey"); - String secretKey = this.keyValue.get("secretKey"); - - classLogger.info("Authenticating extension user with client key"); - - // Validate keys and get access token - AccessToken token = null; - try { - token = SecurityUserAccessKeyUtils.validateKeysAndReturnToken(clientKey, secretKey); - } catch (IllegalAccessException e) { - classLogger.error("Authentication failed for client key: {}", clientKey, e); - throw new IllegalArgumentException("Invalid credentials: " + e.getMessage()); - } - - if (token == null) { - throw new IllegalArgumentException("Invalid credentials"); - } - - // Convert token to user - User user = new User(); - user.setAccessToken(token); - - // Get user's accessible projects - List projectIds = SecurityProjectUtils.getFullUserProjectIds(user); - - // Get user info from access token - String userId = token.getId(); - String userName = token.getName(); - String userEmail = token.getEmail() != null ? token.getEmail() : ""; - - // Build response with user info and projects - JSONObject response = new JSONObject(); - response.put("success", true); - response.put("userId", userId); - response.put("userName", userName); - response.put("userEmail", userEmail); - - // Build projects array with details - JSONArray projects = new JSONArray(); - for (String projectId : projectIds) { - try { - JSONObject project = new JSONObject(); - project.put("id", projectId); - project.put("name", SecurityProjectUtils.getProjectAliasForId(projectId)); - - String displayName = SecurityProjectUtils.getProjectDisplayNameForId(projectId); - project.put("displayName", displayName != null ? displayName : SecurityProjectUtils.getProjectAliasForId(projectId)); - - // Check if user has edit permission - boolean canEdit = SecurityProjectUtils.userCanEditProject(user, projectId); - project.put("canEdit", canEdit); - - projects.put(project); - } catch (Exception e) { - classLogger.warn("Could not load details for project: {}", projectId, e); - } - } - response.put("projects", projects); - - classLogger.info("Successfully authenticated user: {} with {} accessible projects", userName, projectIds.size()); - - return new NounMetadata(response, PixelDataType.JSON_OBJECT); - } - - @Override - public String getReactorDescription() { - return "Authenticates Chrome Extension user using API keys and returns accessible projects"; - } - - @Override - protected String getDescriptionForKey(String key) { - if (key.equals("clientKey")) { - return "The client/access key from Semoss user settings"; - } else if (key.equals("secretKey")) { - return "The secret key from Semoss user settings"; - } - return super.getDescriptionForKey(key); - } -} \ No newline at end of file diff --git a/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java b/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java index 31d08ca74ad..47d8d56dbd7 100644 --- a/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java +++ b/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java @@ -41,7 +41,6 @@ import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; import prerna.auth.utils.SecurityProjectUtils; -import prerna.auth.utils.SecurityUserAccessKeyUtils; import prerna.cluster.util.ClusterUtil; import prerna.project.api.IProject; import prerna.reactor.AbstractReactor; @@ -55,7 +54,7 @@ /** * Saves a recording from Chrome Extension to project recordings folder and auto-updates MCP. - * Supports dual authentication: API keys (for extension) or session-based (for UI). + * Uses session-based authentication (Google OAuth from extension). * *

Pixel Syntax:

*
SaveRecordingFromExtension(project=[string], name=[string], jsonPayload=[string], 
@@ -69,8 +68,6 @@
  *   
  • title - Recording title (optional)
  • *
  • description - Recording description (optional)
  • *
  • intent - Purpose of the recording (optional)
  • - *
  • clientKey - API client key for extension authentication (optional, via request params)
  • - *
  • secretKey - API secret key for extension authentication (optional, via request params)
  • * * *

    Returns:

    @@ -109,38 +106,12 @@ public SaveRecordingFromExtensionReactor() { public NounMetadata execute() { organizeKeys(); - User user = null; - - // Try API key authentication first (for extension usage without session) - String clientKey = getRequestParameter("clientKey"); - String secretKey = getRequestParameter("secretKey"); - - if (clientKey != null && secretKey != null && !clientKey.isEmpty() && !secretKey.isEmpty()) { - // Authenticate using API keys - AccessToken token = null; - try { - token = SecurityUserAccessKeyUtils.validateKeysAndReturnToken(clientKey, secretKey); - classLogger.info("User authenticated via API keys: {}", token.getId()); - } catch (IllegalAccessException e) { - classLogger.error("API key authentication failed: {}", e.getMessage(), e); - throw new IllegalArgumentException("Invalid API credentials: " + e.getMessage()); - } - - if (token == null) { - throw new IllegalArgumentException("Invalid API credentials"); - } - - // Convert token to user - user = new User(); - user.setAccessToken(token); - } else { - // Fall back to session-based authentication - user = this.insight.getUser(); - - // Check if user is logged in - if (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous()) { - throwAnonymousUserError(); - } + // Get user from session (Google OAuth) + User user = this.insight.getUser(); + + // Check if user is logged in + if (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous()) { + throwAnonymousUserError(); } String projectId = this.keyValue.get(this.keysToGet[0]); @@ -271,19 +242,4 @@ protected String getDescriptionForKey(String key) { } return super.getDescriptionForKey(key); } - - /** - * Gets a parameter from the request (supports both form data and query parameters) - * - * @param paramName the parameter name - * @return the parameter value or null if not found - */ - private String getRequestParameter(String paramName) { - // Try to get from keyValue first (URL encoded form data) - if (this.keyValue.containsKey(paramName)) { - return this.keyValue.get(paramName); - } - - return null; - } } \ No newline at end of file From 0c889fae5b734997d50b536d80f8856cd4734207 Mon Sep 17 00:00:00 2001 From: kritysingh630 Date: Fri, 10 Jul 2026 17:43:13 +0530 Subject: [PATCH 3/3] feat: add CreateProjectPortalFromTemplateReactor for portal creation from HTML content --- .../SaveRecordingFromExtensionReactor.java | 364 ++++++++++-------- ...reateProjectPortalFromTemplateReactor.java | 137 +++++++ 2 files changed, 344 insertions(+), 157 deletions(-) create mode 100644 src/prerna/util/git/reactors/CreateProjectPortalFromTemplateReactor.java diff --git a/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java b/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java index 47d8d56dbd7..88e88084015 100644 --- a/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java +++ b/src/prerna/reactor/playwright/SaveRecordingFromExtensionReactor.java @@ -25,6 +25,33 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. *******************************************************************************/ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ package prerna.reactor.playwright; import java.io.FileWriter; @@ -86,160 +113,183 @@ */ public class SaveRecordingFromExtensionReactor extends AbstractReactor { - private static final Logger classLogger = LogManager.getLogger(SaveRecordingFromExtensionReactor.class); - - public SaveRecordingFromExtensionReactor() { - // Note: String literals used for name, jsonPayload, title, intent as they are - // Playwright-specific and don't exist in ReactorKeysEnum - this.keysToGet = new String[] { - ReactorKeysEnum.PROJECT.getKey(), - "name", - "jsonPayload", - "title", - ReactorKeysEnum.DESCRIPTION.getKey(), - "intent" - }; - this.keyRequired = new int[] { 1, 1, 1, 0, 0, 0 }; - } - - @Override - public NounMetadata execute() { - organizeKeys(); - - // Get user from session (Google OAuth) - User user = this.insight.getUser(); - - // Check if user is logged in - if (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous()) { - throwAnonymousUserError(); - } - - String projectId = this.keyValue.get(this.keysToGet[0]); - // Note: name, title, intent may need URL decoding if coming from form data - String name = this.keyValue.get(this.keysToGet[1]); - String jsonPayload = this.keyValue.get(this.keysToGet[2]); - String title = this.keyValue.get(this.keysToGet[3]); - String desc = this.keyValue.get(this.keysToGet[4]); - String intent = this.keyValue.get(this.keysToGet[5]); - - // Validate user has edit access to project - if (!SecurityProjectUtils.userCanEditProject(user, projectId)) { - throw new IllegalArgumentException( - "Project " + projectId + " does not exist or user does not have access to edit."); - } - - IProject project = Utility.getProject(projectId); - - // Validate JSON payload - try { - GSON.fromJson(jsonPayload, Object.class); - } catch (Exception e) { - classLogger.error("Invalid JSON payload: {}", e.getMessage(), e); - throw new IllegalArgumentException("Invalid JSON payload: " + e.getMessage()); - } - - // Sanitize filename - String base = PlaywrightUtility.sanitizeFilename( - name == null || name.isBlank() ? ("Recording-" + PlaywrightUtility.generateTimestamp()) : name); - String fileName = base.endsWith(".json") ? base : (base + ".json"); - - // Get recordings directory - Path recordingsDir = PlaywrightUtility.initRecordingsDir(projectId); - Path file = recordingsDir.resolve(fileName); - - // Check if file exists and auto-increment filename to avoid overwrite - if (Files.exists(file)) { - String baseName = fileName.substring(0, fileName.length() - 5); // Remove .json - int counter = 1; - do { - fileName = baseName + "_" + counter + ".json"; - file = recordingsDir.resolve(fileName); - counter++; - } while (Files.exists(file)); - classLogger.info("File already exists, using auto-incremented name: {}", fileName); - } - - // Save the JSON file - extension already formats it correctly with JSON.stringify() - try (FileWriter writer = new FileWriter(file.toFile())) { - // Validate JSON format but don't reformat (preserve extension's formatting) - GSON.fromJson(jsonPayload, Object.class); - // Write original string from extension - writer.write(jsonPayload); - classLogger.info("Saved recording to: {}", file.toAbsolutePath()); - } catch (Exception e) { - classLogger.error("Failed to save recording to: {}", file, e); - throw new RuntimeException("Failed to save recording to: " + file, e); - } - - // Auto-update MCP by calling MakePlaywrightMCPReactor - try { - MakePlaywrightMCPReactor mcpReactor = new MakePlaywrightMCPReactor(); - mcpReactor.setInsight(this.insight); - mcpReactor.setNounStore(this.store); - - // Set the project parameter - this.store.makeNoun(ReactorKeysEnum.PROJECT.getKey()).add(new NounMetadata(projectId, PixelDataType.CONST_STRING)); - - mcpReactor.execute(); - classLogger.info("MCP updated successfully"); - } catch (Exception e) { - classLogger.error("Failed to update MCP: {}", e.getMessage(), e); - // Don't fail the whole operation, just log the error - } - - // Git operations - String versionGitFolder = AssetUtility.getProjectVersionFolder(project.getProjectName(), - project.getProjectId()); - String assetFolder = AssetUtility.getProjectAssetsFolder(project.getProjectName(), project.getProjectId()); - String comment = "Added recording from Chrome Extension: " + fileName; - - // Add recording file to git - List gitRelativeFilePaths = new ArrayList<>(); - gitRelativeFilePaths.add(Constants.ASSETS_FOLDER + "/recordings/" + fileName); - - // Get the user's email - AccessToken accessToken = user.getAccessToken(user.getPrimaryLogin()); - String email = accessToken.getEmail(); - String author = accessToken.getUsername(); - - try { - GitRepoUtils.addSpecificFiles(versionGitFolder, gitRelativeFilePaths); - GitRepoUtils.commitAddedFiles(versionGitFolder, comment, author, email); - ClusterUtil.pushProjectFolder(project, assetFolder); - classLogger.info("Recording committed to git"); - } catch (Exception e) { - classLogger.error("Git operations failed: {}", e.getMessage(), e); - // Don't fail the whole operation - } - - // Return success response - JSONObject response = new JSONObject(); - response.put("success", true); - response.put("fileName", fileName); - response.put("filePath", file.toAbsolutePath().toString()); - response.put("message", "Recording saved successfully"); - - return new NounMetadata(response, PixelDataType.JSON_OBJECT); - } - - @Override - public String getReactorDescription() { - return "Saves a recording from Chrome Extension to project recordings folder and updates MCP"; - } - - @Override - protected String getDescriptionForKey(String key) { - if (key.equals("name")) { - return "The name of the recording file"; - } else if (key.equals("jsonPayload")) { - return "The complete recording JSON as a string"; - } else if (key.equals(ReactorKeysEnum.DESCRIPTION.getKey())) { - return "The description of the recording"; - } else if (key.equals("title")) { - return "The title of the recording"; - } else if (key.equals("intent")) { - return "The intention or purpose of the recording"; - } - return super.getDescriptionForKey(key); - } -} \ No newline at end of file + private static final Logger classLogger = LogManager.getLogger(SaveRecordingFromExtensionReactor.class); + + public SaveRecordingFromExtensionReactor() { + // Note: String literals used for name, jsonPayload, title, intent as they are + // Playwright-specific and don't exist in ReactorKeysEnum + this.keysToGet = new String[] { + ReactorKeysEnum.PROJECT.getKey(), + "name", + "jsonPayload", + "title", + ReactorKeysEnum.DESCRIPTION.getKey(), + "intent" + }; + this.keyRequired = new int[] { 1, 1, 1, 0, 0, 0 }; + } + + @Override + public NounMetadata execute() { + organizeKeys(); + + // Get user from session (Google OAuth) + User user = this.insight.getUser(); + + // Check if user is logged in + if (user == null) { + throw new IllegalArgumentException("User must be signed in to save a recording."); + } + if (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous()) { + throwAnonymousUserError(); + } + + String projectId = this.keyValue.get(this.keysToGet[0]); + // Note: name, title, intent may need URL decoding if coming from form data + String name = this.keyValue.get(this.keysToGet[1]); + String jsonPayload = this.keyValue.get(this.keysToGet[2]); + String title = this.keyValue.get(this.keysToGet[3]); + String desc = this.keyValue.get(this.keysToGet[4]); + String intent = this.keyValue.get(this.keysToGet[5]); + + // Validate user has edit access to project + if (!SecurityProjectUtils.userCanEditProject(user, projectId)) { + throw new IllegalArgumentException( + "Project " + projectId + " does not exist or user does not have access to edit."); + } + + IProject project = Utility.getProject(projectId); + + // Validate JSON payload + try { + GSON.fromJson(jsonPayload, Object.class); + } catch (Exception e) { + classLogger.error("Invalid JSON payload: {}", e.getMessage(), e); + throw new IllegalArgumentException("Invalid JSON payload: " + e.getMessage()); + } + + // Sanitize filename + String base = PlaywrightUtility.sanitizeFilename( + name == null || name.isBlank() ? ("Recording-" + PlaywrightUtility.generateTimestamp()) : name); + String fileName = base.endsWith(".json") ? base : (base + ".json"); + + // Get recordings directory + Path recordingsDir = PlaywrightUtility.initRecordingsDir(projectId); + Path file = recordingsDir.resolve(fileName); + + // Check if file exists and auto-increment filename to avoid overwrite + if (Files.exists(file)) { + String baseName = fileName.substring(0, fileName.length() - 5); // Remove .json + int counter = 1; + do { + fileName = baseName + "_" + counter + ".json"; + file = recordingsDir.resolve(fileName); + counter++; + } while (Files.exists(file)); + classLogger.info("File already exists, using auto-incremented name: {}", fileName); + } + + // Save the JSON file - extension already formats it correctly with JSON.stringify() + try (FileWriter writer = new FileWriter(file.toFile())) { + // Validate JSON format but don't reformat (preserve extension's formatting) + GSON.fromJson(jsonPayload, Object.class); + // Write original string from extension + writer.write(jsonPayload); + classLogger.info("Saved recording to: {}", file.toAbsolutePath()); + } catch (Exception e) { + classLogger.error("Failed to save recording to: {}", file, e); + throw new RuntimeException("Failed to save recording to: " + file, e); + } + + // Auto-update MCP by calling MakePlaywrightMCPReactor + boolean mcpUpdated = true; + String mcpWarning = null; + try { + MakePlaywrightMCPReactor mcpReactor = new MakePlaywrightMCPReactor(); + mcpReactor.setInsight(this.insight); + mcpReactor.setNounStore(this.store); + + // Set the project parameter + this.store.makeNoun(ReactorKeysEnum.PROJECT.getKey()) + .add(new NounMetadata(projectId, PixelDataType.CONST_STRING)); + + mcpReactor.execute(); + classLogger.info("MCP updated successfully"); + } catch (Exception e) { + classLogger.error("Failed to update MCP: {}", e.getMessage(), e); + mcpUpdated = false; + mcpWarning = e.getMessage(); + // Don't fail the whole operation, just log the error + } + + // Git operations + String versionGitFolder = AssetUtility.getProjectVersionFolder(project.getProjectName(), + project.getProjectId()); + String assetFolder = AssetUtility.getProjectAssetsFolder(project.getProjectName(), project.getProjectId()); + String comment = "Added recording from Chrome Extension: " + fileName; + + // Add recording file to git + List gitRelativeFilePaths = new ArrayList<>(); + gitRelativeFilePaths.add(Constants.ASSETS_FOLDER + "/recordings/" + fileName); + + // Get the user's email + AccessToken accessToken = user.getAccessToken(user.getPrimaryLogin()); + String email = accessToken != null && accessToken.getEmail() != null ? accessToken.getEmail() + : "semoss@localhost"; + String author = accessToken != null && accessToken.getUsername() != null ? accessToken.getUsername() + : "SEMOSS Extension"; + + boolean gitCommitted = true; + String gitWarning = null; + try { + GitRepoUtils.addSpecificFiles(versionGitFolder, gitRelativeFilePaths); + GitRepoUtils.commitAddedFiles(versionGitFolder, comment, author, email); + ClusterUtil.pushProjectFolder(project, assetFolder); + classLogger.info("Recording committed to git"); + } catch (Exception e) { + classLogger.error("Git operations failed: {}", e.getMessage(), e); + gitCommitted = false; + gitWarning = e.getMessage(); + // Don't fail the whole operation + } + + // Return success response + JSONObject response = new JSONObject(); + response.put("success", true); + response.put("fileName", fileName); + response.put("filePath", file.toAbsolutePath().toString()); + response.put("message", "Recording saved successfully"); + response.put("mcpUpdated", mcpUpdated); + if (mcpWarning != null) { + response.put("mcpWarning", mcpWarning); + } + response.put("gitCommitted", gitCommitted); + if (gitWarning != null) { + response.put("gitWarning", gitWarning); + } + + return new NounMetadata(response, PixelDataType.JSON_OBJECT); + } + + @Override + public String getReactorDescription() { + return "Saves a recording from Chrome Extension to project recordings folder and updates MCP"; + } + + @Override + protected String getDescriptionForKey(String key) { + if (key.equals("name")) { + return "The name of the recording file"; + } else if (key.equals("jsonPayload")) { + return "The complete recording JSON as a string"; + } else if (key.equals(ReactorKeysEnum.DESCRIPTION.getKey())) { + return "The description of the recording"; + } else if (key.equals("title")) { + return "The title of the recording"; + } else if (key.equals("intent")) { + return "The intention or purpose of the recording"; + } + return super.getDescriptionForKey(key); + } +} + \ No newline at end of file diff --git a/src/prerna/util/git/reactors/CreateProjectPortalFromTemplateReactor.java b/src/prerna/util/git/reactors/CreateProjectPortalFromTemplateReactor.java new file mode 100644 index 00000000000..e409601f219 --- /dev/null +++ b/src/prerna/util/git/reactors/CreateProjectPortalFromTemplateReactor.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.util.git.reactors; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import prerna.auth.AccessToken; +import prerna.auth.User; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.cluster.util.ClusterUtil; +import prerna.project.api.IProject; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.AssetUtility; +import prerna.util.Constants; +import prerna.util.Utility; +import prerna.util.git.GitRepoUtils; + +/** + * Creates a portal index.html file from HTML content (no external dependencies). + * Usage: CreateProjectPortalFromTemplate(project="projectId", htmlContent="..."); + * + * This reactor provides an optimized way to create portals without git cloning, + * reducing dependencies and improving reliability. + */ +public class CreateProjectPortalFromTemplateReactor extends AbstractReactor { + + private static final String HTML_CONTENT = "htmlContent"; + + public CreateProjectPortalFromTemplateReactor() { + this.keysToGet = new String[] { + ReactorKeysEnum.PROJECT.getKey(), + HTML_CONTENT, + ReactorKeysEnum.COMMENT_KEY.getKey() + }; + this.keyRequired = new int[] { 1, 1, 0 }; + } + + @Override + public NounMetadata execute() { + organizeKeys(); + + User user = this.insight.getUser(); + String projectId = keyValue.get(keysToGet[0]); + + // Security check + if (!SecurityProjectUtils.userCanEditProject(user, projectId)) { + throw new IllegalArgumentException( + "Project " + projectId + " does not exist or user does not have access to edit assets."); + } + + IProject project = Utility.getProject(projectId); + String htmlContent = keyValue.get(keysToGet[1]); + + String comment = this.keyValue.get(this.keysToGet[2]); + if (comment == null) { + comment = "add: creating portal from bundled template"; + } + + String projectAssetFolder = AssetUtility.getProjectAssetsFolder(projectId); + String versionGitFolder = AssetUtility.getProjectVersionFolder( + project.getProjectName(), + project.getProjectId() + ); + + try { + // Create portals directory if it doesn't exist + File portalsDir = new File(projectAssetFolder + File.separator + "portals"); + if (!portalsDir.exists()) { + portalsDir.mkdirs(); + } + + // Write the HTML content to index.html + File portalFile = new File(portalsDir, "index.html"); + try (FileWriter writer = new FileWriter(portalFile, StandardCharsets.UTF_8)) { + writer.write(htmlContent); + } + + // Git commit the changes + List gitRelativeFilePaths = new ArrayList<>(); + gitRelativeFilePaths.add(Constants.ASSETS_FOLDER + DIR_SEPARATOR + Constants.PORTALS_FOLDER + "/"); + + // Get the user's credentials for git commit + AccessToken accessToken = user.getAccessToken(user.getPrimaryLogin()); + String email = accessToken.getEmail(); + String author = accessToken.getUsername(); + + GitRepoUtils.addSpecificFiles(versionGitFolder, gitRelativeFilePaths); + GitRepoUtils.commitAddedFiles(versionGitFolder, comment, author, email); + + // Handle synchronization to the cloud + ClusterUtil.pushProjectFolder(project, projectAssetFolder); + + return new NounMetadata( + "Portal created successfully at " + portalFile.getAbsolutePath(), + PixelDataType.CONST_STRING + ); + + } catch (IOException e) { + throw new IllegalArgumentException("Failed to create portal file: " + e.getMessage(), e); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to commit portal changes: " + e.getMessage(), e); + } + } +}