diff --git a/app/client/package.json b/app/client/package.json
index add895688c86..f4b971e8fe2d 100644
--- a/app/client/package.json
+++ b/app/client/package.json
@@ -106,6 +106,7 @@
"@uppy/url": "^1.5.16",
"@uppy/utils": "^6.0.2",
"@uppy/webcam": "^1.8.4",
+ "@useparagon/connect": "^1.0.18",
"@welldone-software/why-did-you-render": "^4.2.5",
"acorn": "8.10.0",
"acorn-walk": "8.2.0",
diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/QueryResponseTab.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/QueryResponseTab.tsx
index e5880bd50320..851dd22f0b61 100644
--- a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/QueryResponseTab.tsx
+++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/QueryResponseTab.tsx
@@ -176,7 +176,11 @@ const QueryResponseTab = (props: Props) => {
// Pass the error to be shown in the error tab
error = actionResponse.readableError
? getErrorAsString(actionResponse.readableError)
- : getErrorAsString(actionResponse.body);
+ : getErrorAsString(
+ (actionResponse.body as any)?.message
+ ? (actionResponse.body as any).message
+ : actionResponse.body,
+ );
} else if (isString(actionResponse.body)) {
//reset error.
error = "";
@@ -278,7 +282,10 @@ const QueryResponseTab = (props: Props) => {
>
) : (
actionResponse.body && (
-
{actionResponse.body}
+
+ {(actionResponse.body as any).message ||
+ actionResponse.body}
+
)
))}
> {
return Api.delete(`${WorkspaceApi.workspacesURL}/${workspaceId}`);
}
+ static async fetchWorkspaceToken(
+ request: FetchWorkspaceRequest,
+ ): Promise> {
+ return Api.get(
+ WorkspaceApi.workspacesURL + "/" + request.workspaceId + "/token",
+ );
+ }
}
export default WorkspaceApi;
diff --git a/app/client/src/ce/constants/workspaceConstants.ts b/app/client/src/ce/constants/workspaceConstants.ts
index e7e8321ee11c..a9f715127fa2 100644
--- a/app/client/src/ce/constants/workspaceConstants.ts
+++ b/app/client/src/ce/constants/workspaceConstants.ts
@@ -5,7 +5,7 @@ export interface WorkspaceRole {
isDefault?: boolean;
}
-export interface Workspace {
+export interface Workspace extends WorkspaceToken {
id: string;
name: string;
website?: string;
@@ -37,6 +37,12 @@ export interface WorkspaceUser {
roles: WorkspaceUserRoles[];
}
+export interface WorkspaceToken {
+ workspaceId?: string;
+ token?: string;
+ projectId?: string;
+}
+
export enum ENTITY_TYPE {
WORKSPACE = "Workspace",
APPLICATION = "Application",
diff --git a/app/client/src/ce/sagas/WorkspaceSagas.ts b/app/client/src/ce/sagas/WorkspaceSagas.ts
index 5670661b597c..36e8bfca84b7 100644
--- a/app/client/src/ce/sagas/WorkspaceSagas.ts
+++ b/app/client/src/ce/sagas/WorkspaceSagas.ts
@@ -25,6 +25,7 @@ import type {
FetchAllRolesRequest,
SaveWorkspaceLogo,
FetchWorkspacesResponse,
+ FetchWorkspaceTokenResponse,
} from "ee/api/WorkspaceApi";
import WorkspaceApi from "ee/api/WorkspaceApi";
import type { ApiResponse } from "api/ApiResponses";
@@ -118,10 +119,18 @@ export function* fetchWorkspaceSaga(
const isValidResponse: boolean = yield request.skipValidation ||
validateResponse(response);
+ const responseToken: FetchWorkspaceTokenResponse = yield call(
+ WorkspaceApi.fetchWorkspaceToken,
+ request,
+ );
+
if (isValidResponse && response) {
yield put({
type: ReduxActionTypes.FETCH_WORKSPACE_SUCCESS,
- payload: response.data || {},
+ payload: {
+ ...(response.data || {}),
+ ...(responseToken.data || {}),
+ },
});
}
} catch (error) {
diff --git a/app/client/src/ce/utils/autocomplete/EntityDefinitions.ts b/app/client/src/ce/utils/autocomplete/EntityDefinitions.ts
index a0a59b4f9cfd..1038fe649402 100644
--- a/app/client/src/ce/utils/autocomplete/EntityDefinitions.ts
+++ b/app/client/src/ce/utils/autocomplete/EntityDefinitions.ts
@@ -87,6 +87,15 @@ export const entityDefinitions = {
"https://docs.appsmith.com/reference/appsmith-framework/context-object#geolocationclearwatch",
},
},
+ datasource: {
+ "!doc": "Object to enable running custom datasources",
+ "!url": "",
+ request: {
+ "!type":
+ "fn(requestType: string, apiPath: string, options: object) -> +Promise|void",
+ "!url": "",
+ },
+ },
...eeAppsmithAutocompleteDefs(generatedTypeDef),
};
}
diff --git a/app/client/src/components/editorComponents/ActionCreator/constants.ts b/app/client/src/components/editorComponents/ActionCreator/constants.ts
index 859e75217316..d4792f6d22f3 100644
--- a/app/client/src/components/editorComponents/ActionCreator/constants.ts
+++ b/app/client/src/components/editorComponents/ActionCreator/constants.ts
@@ -20,6 +20,7 @@ export enum APPSMITH_NAMESPACED_FUNCTIONS {
getGeolocation = "appsmith.geolocation.getCurrentPosition",
watchGeolocation = "appsmith.geolocation.watchPosition",
stopWatchGeolocation = "appsmith.geolocation.clearWatch",
+ datasourceRequest = "appsmith.datasource.request",
}
export enum APPSMITH_INTEGRATIONS {
diff --git a/app/client/src/pages/Editor/APIEditor/index.tsx b/app/client/src/pages/Editor/APIEditor/index.tsx
index 568fca9c0199..67edf8f37925 100644
--- a/app/client/src/pages/Editor/APIEditor/index.tsx
+++ b/app/client/src/pages/Editor/APIEditor/index.tsx
@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "react-redux";
import type { RouteComponentProps } from "react-router";
import {
+ getDatasource,
getIsActionConverting,
getPageList,
getPluginSettingConfigs,
@@ -39,6 +40,8 @@ import { ENTITY_ICON_SIZE, EntityIcon } from "../Explorer/ExplorerIcons";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "ee/entities/IDE/constants";
import { AppPluginActionEditor } from "pages/Editor/AppPluginActionEditor";
+import { getCurrentEnvironmentId } from "ce/selectors/environmentSelectors";
+import QueryEditor from "pages/Editor/QueryEditor";
type ApiEditorWrapperProps = RouteComponentProps;
@@ -60,6 +63,23 @@ function ApiEditorWrapper(props: ApiEditorWrapperProps) {
const apiName = action?.name || "";
const pluginId = get(action, "pluginId", "");
const datasourceId = action?.datasource.id || "";
+ if (datasourceId) {
+ const currentEnvironment = useSelector(getCurrentEnvironmentId);
+ const datasource = useSelector((state) =>
+ getDatasource(state, datasourceId),
+ );
+ const datasourceConfigurationProps =
+ datasource?.datasourceStorages?.[currentEnvironment]
+ ?.datasourceConfiguration?.properties;
+ if (
+ datasourceConfigurationProps &&
+ datasourceConfigurationProps.some(
+ ({ key }) => key === "integrationId" || key === "integrationType",
+ )
+ ) {
+ return ;
+ }
+ }
const plugins = useSelector(getPlugins);
const pages = useSelector(getPageList);
const pageName = getPageName(pages, basePageId);
diff --git a/app/client/src/pages/Editor/DataSourceEditor/DSFormHeader.tsx b/app/client/src/pages/Editor/DataSourceEditor/DSFormHeader.tsx
index 34561ee1f529..5551e537fc2a 100644
--- a/app/client/src/pages/Editor/DataSourceEditor/DSFormHeader.tsx
+++ b/app/client/src/pages/Editor/DataSourceEditor/DSFormHeader.tsx
@@ -28,6 +28,7 @@ import type { PluginType } from "entities/Action";
import { useEditorType } from "ee/hooks";
import { useHistory } from "react-router";
import { useHeaderActions } from "ee/hooks/datasourceEditorHooks";
+import { paragon } from "@useparagon/connect";
export const ActionWrapper = styled.div`
display: flex;
@@ -172,6 +173,32 @@ export const DSFormHeader = (props: DSFormHeaderProps) => {
showReconnectButton,
});
+ const onClickConnect = () => {
+ const datasourceConfigurationProps = (datasource as Datasource)
+ ?.datasourceStorages?.[currentEnv]?.datasourceConfiguration?.properties;
+
+ const integrationId =
+ datasourceConfigurationProps?.find(({ key }) => key === "integrationId")
+ ?.value || "";
+ const integrationType =
+ datasourceConfigurationProps?.find(({ key }) => key === "integrationType")
+ ?.value || "";
+
+ if (!integrationId || !integrationType) return;
+
+ paragon.connect(integrationType, {
+ // selectedCredentialId: integrationId,
+ // allowMultipleCredentials: true,
+ accountType: "default",
+ onSuccess: (event, user) => {
+ console.log(event);
+ },
+ onError: (err) => {
+ console.log(err);
+ },
+ });
+ };
+
return (
@@ -232,6 +259,20 @@ export const DSFormHeader = (props: DSFormHeaderProps) => {
{headerActions && headerActions.generatePageButton
? headerActions.generatePageButton
: null}
+
)}
diff --git a/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx b/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx
index 8bbdc69257f4..e744acc2d4b0 100644
--- a/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx
+++ b/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx
@@ -10,19 +10,20 @@ import { getHasCreateDatasourcePermission } from "ee/utils/BusinessFeatures/perm
import {
getDatasources,
getMockDatasources,
+ getPlugins,
} from "ee/selectors/entitiesSelector";
import {
getCurrentApplicationId,
getCurrentPageId,
} from "selectors/editorSelectors";
-import { connect } from "react-redux";
+import { connect, useDispatch, useSelector } from "react-redux";
import type { Datasource, MockDatasource } from "entities/Datasource";
import scrollIntoView from "scroll-into-view-if-needed";
import { Text } from "@appsmith/ads";
import MockDataSources from "./MockDataSources";
-import NewApiScreen from "./NewApi";
+import NewApiScreen, { ApiCard, CardContentWrapper } from "./NewApi";
import NewQueryScreen from "./NewQuery";
-import { isAirgapped } from "ee/utils/airgapHelpers";
+import { getAssetUrl, isAirgapped } from "ee/utils/airgapHelpers";
import { showDebuggerFlag } from "selectors/debuggerSelectors";
import {
createMessage,
@@ -40,6 +41,14 @@ import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks";
import AIDataSources from "./AIDataSources";
import Debugger from "../DataSourceEditor/Debugger";
import { isPluginActionCreating } from "PluginActionEditor/store";
+import { paragon } from "@useparagon/connect";
+import AnalyticsUtil from "ee/utils/AnalyticsUtil";
+import { useParagonIntegrations } from "utils/paragonHooks";
+import { DATASOURCE_NAME_DEFAULT_PREFIX } from "constants/Datasource";
+import { ReduxActionTypes } from "ce/constants/ReduxActionConstants";
+import { PluginPackageName } from "entities/Action";
+import type { Plugin } from "api/PluginApi";
+import { getNextEntityName } from "utils/AppsmithUtils";
const NewIntegrationsContainer = styled.div`
${thinScrollbar};
@@ -174,6 +183,88 @@ function CreateNewDatasource({
);
}
+function ParagonIntegrations() {
+ const { integrations } = useParagonIntegrations();
+ const dispatch = useDispatch();
+ const dsList: Datasource[] = useSelector(getDatasources);
+ const plugins: Plugin[] = useSelector(getPlugins);
+ const apiPlugin = plugins.find(
+ (plugin) => plugin.packageName === PluginPackageName.REST_API,
+ );
+ const datasourceName = getNextEntityName(
+ DATASOURCE_NAME_DEFAULT_PREFIX,
+ dsList.map((el: Datasource) => el.name),
+ );
+ const handleOnClick = (type: string) => {
+ paragon.installIntegration(type, {
+ allowMultipleCredentials: true,
+ accountType: "default",
+ onSuccess: (event, user) => {
+ console.log(event);
+ dispatch({
+ type: ReduxActionTypes.CREATE_DATASOURCE_FROM_FORM_INIT,
+ payload: {
+ type: "API",
+ pluginId: apiPlugin!.id,
+ datasourceStorages: {
+ unused_env: {
+ environmentId: "unused_env",
+ isValid: false,
+ datasourceConfiguration: {
+ url: "https://proxy.useparagon.com",
+ properties: Object.keys(event).reduce((acc, key) => {
+ // @ts-expect-error
+ acc.push({
+ key: key as string,
+ // @ts-expect-error
+ value: event[key] as any,
+ });
+ return acc;
+ }, []),
+ },
+ toastMessage: "EMPTY_TOAST_MESSAGE",
+ },
+ },
+ name: datasourceName,
+ },
+ });
+ },
+ onError: (err) => {
+ console.log(err);
+ },
+ });
+ };
+
+ return (
+ <>
+ {integrations.map((integration) => (
+ {
+ AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", {
+ pluginName: integration.name,
+ pluginPackageName: integration.type,
+ });
+ handleOnClick(integration.type);
+ }}
+ >
+
+
+ {integration.name}
+
+
+ ))}
+ >
+ );
+}
+
function CreateNewSaasIntegration({
active,
isCreating,
@@ -210,7 +301,9 @@ function CreateNewSaasIntegration({
pageId={pageId}
showSaasAPIs
showUnsupportedPluginDialog={showUnsupportedPluginDialog}
- />
+ >
+
+
>
) : null;
diff --git a/app/client/src/pages/Editor/IntegrationEditor/NewApi.tsx b/app/client/src/pages/Editor/IntegrationEditor/NewApi.tsx
index 4364bad281e3..743dd16a6e0d 100644
--- a/app/client/src/pages/Editor/IntegrationEditor/NewApi.tsx
+++ b/app/client/src/pages/Editor/IntegrationEditor/NewApi.tsx
@@ -142,6 +142,7 @@ interface ApiHomeScreenProps {
apiType: string,
) => void;
isOnboardingScreen?: boolean;
+ children?: JSX.Element;
}
type Props = ApiHomeScreenProps;
@@ -328,6 +329,7 @@ function NewApiScreen(props: Props) {
))}
+ {props.children}
);
diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
index d82a9640211c..d055fb051229 100644
--- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
+++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
@@ -19,7 +19,7 @@ import type { AppState } from "ee/reducers";
import { thinScrollbar } from "constants/DefaultTheme";
import type { ActionResponse } from "api/ActionAPI";
import type { Plugin } from "api/PluginApi";
-import type { UIComponentTypes } from "api/PluginApi";
+import { UIComponentTypes } from "api/PluginApi";
import { EDITOR_TABS, SQL_DATASOURCES } from "constants/QueryEditorConstants";
import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer";
import {
@@ -39,7 +39,15 @@ import RunHistory from "ee/components/RunHistory";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { getHasExecuteActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
-import { getPluginNameFromId } from "ee/selectors/entitiesSelector";
+import {
+ getDatasource,
+ getPluginNameFromId,
+} from "ee/selectors/entitiesSelector";
+import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
+import {
+ useParagonIntegrationsWithWorklows,
+} from "utils/paragonHooks";
+import { klona } from "klona";
const QueryFormContainer = styled.form`
flex: 1;
@@ -143,6 +151,108 @@ const StyledNotificationWrapper = styled.div`
var(--ads-v2-spaces-7);
`;
+const PARAGON_EDITOR_JSON = [
+ {
+ controlType: "DOUBLE_COLUMN_ZONE",
+ identifier: "FIND-Z1",
+ children: [
+ {
+ label: "Method",
+ configProperty: "actionConfiguration.httpMethod",
+ controlType: "DROP_DOWN",
+ initialValue: "GET",
+ options: [
+ {
+ label: "GET",
+ value: "GET",
+ },
+ {
+ label: "POST",
+ value: "POST",
+ },
+ {
+ label: "PUT",
+ value: "PUT",
+ },
+ {
+ label: "DELETE",
+ value: "DELETE",
+ },
+ ],
+ },
+ {
+ label: "Action Type",
+ configProperty: "actionConfiguration.formData.actionType",
+ controlType: "DROP_DOWN",
+ options: [
+ {
+ label: "Workflow",
+ value: "workflow",
+ },
+ {
+ label: "Api Path",
+ value: "api",
+ },
+ ],
+ initialValue: "workflow",
+ },
+ ],
+ },
+ {
+ controlType: "SECTION_V2",
+ identifier: "SECTION-ONE",
+ children: [
+ {
+ controlType: "SINGLE_COLUMN_ZONE",
+ identifier: "API_PATH",
+ children: [
+ {
+ label: "Api Path",
+ configProperty: "actionConfiguration.formData.apiPath",
+ controlType: "QUERY_DYNAMIC_INPUT_TEXT",
+ evaluationSubstitutionType: "TEMPLATE",
+ placeholderText: "v1/example",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ controlType: "SECTION_V3",
+ identifier: "SECTION-TWO",
+ children: [
+ {
+ controlType: "SINGLE_COLUMN_ZONE",
+ identifier: "SO-Z2",
+ children: [
+ {
+ label: "Query Params",
+ configProperty: "actionConfiguration.queryParameters",
+ controlType: "KEYVALUE_ARRAY",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ controlType: "SECTION_V4",
+ identifier: "SECTION-THREE",
+ children: [
+ {
+ controlType: "SINGLE_COLUMN_ZONE",
+ identifier: "SO-Z3",
+ children: [
+ {
+ label: "Body",
+ configProperty: "actionConfiguration.body",
+ controlType: "QUERY_DYNAMIC_TEXT",
+ },
+ ],
+ },
+ ],
+ },
+];
+
interface QueryFormProps {
onDeleteClick: () => void;
onRunClick: () => void;
@@ -190,7 +300,6 @@ export function EditorJSONtoForm(props: Props) {
actionResponse,
dataSources,
documentationLink,
- editorConfig,
formName,
handleSubmit,
isRunning,
@@ -199,9 +308,10 @@ export function EditorJSONtoForm(props: Props) {
plugin,
runErrorMessage,
settingConfig,
- uiComponent,
} = props;
+ let { editorConfig, uiComponent } = props;
+
const { actionRightPaneAdditionSections, notification } =
useContext(QueryEditorContext);
@@ -273,6 +383,47 @@ export function EditorJSONtoForm(props: Props) {
(!actionBody && SQL_DATASOURCES.includes(currentActionPluginName)) ||
!isExecutePermitted;
+ const formData = props.formData;
+
+ // Paragon Integration
+ const datasource = useSelector((state) =>
+ getDatasource(state, props.datasourceId),
+ );
+ const currentEnvironment = useSelector(getCurrentEnvironmentId);
+ const datasourceConfigurationProps =
+ datasource?.datasourceStorages?.[currentEnvironment]
+ ?.datasourceConfiguration?.properties;
+
+ const integrationId =
+ datasourceConfigurationProps?.find(({ key }) => key === "integrationId")
+ ?.value || "";
+
+ const integrationsWithWorkflows = useParagonIntegrationsWithWorklows();
+ const integrationData = integrationsWithWorkflows[integrationId];
+ if (integrationId && integrationData) {
+ const paragonEditorJson = klona(PARAGON_EDITOR_JSON);
+ paragonEditorJson[0].children.push({
+ controlType: "SINGLE_COLUMN_ZONE",
+ identifier: "WORKFLOW",
+ children: [
+ {
+ label: "Workflow",
+ configProperty: "actionConfiguration.formData.workflowId",
+ controlType: "DROP_DOWN",
+ initialValue: "",
+ options: integrationData.workflows.map((i) => ({
+ label: i.description,
+ value: i.id,
+ })),
+ },
+ ],
+ } as any);
+ editorConfig = paragonEditorJson;
+ uiComponent = UIComponentTypes.UQIDbEditorForm;
+ }
+
+ if (integrationId && !integrationData) return null;
+
// when switching between different redux forms, make sure this redux form has been initialized before rendering anything.
// the initialized prop below comes from redux-form.
if (!props.initialized) {
@@ -326,7 +477,7 @@ export function EditorJSONtoForm(props: Props) {
>
;
+ datasource: Record;
}
const initialState: AppDataState = {
@@ -59,6 +60,7 @@ const initialState: AppDataState = {
currentPosition: {},
},
workflows: {},
+ datasource: {},
};
const appReducer = createReducer(initialState, {
diff --git a/app/client/src/utils/paragonHooks.tsx b/app/client/src/utils/paragonHooks.tsx
new file mode 100644
index 000000000000..16a44c517842
--- /dev/null
+++ b/app/client/src/utils/paragonHooks.tsx
@@ -0,0 +1,71 @@
+import { paragon, type AuthenticatedConnectUser } from "@useparagon/connect";
+import type {
+ IConnectIntegration,
+ IIntegrationMetadata,
+} from "@useparagon/connect/dist/src/entities/integration.interface";
+import { getCurrentAppWorkspace } from "ee/selectors/selectedWorkspaceSelectors";
+import { keyBy } from "lodash";
+import { useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+
+export function useParagonUser() {
+ const [paragonUser, setParagonUser] = useState();
+ const currentWorkspace = useSelector(getCurrentAppWorkspace);
+
+ useEffect(() => {
+ paragon.configureGlobal({
+ host: "https://useparagon.com",
+ });
+ if (currentWorkspace.token && currentWorkspace.projectId) {
+ paragon
+ .authenticate(currentWorkspace.projectId, currentWorkspace.token)
+ .then(() => {
+ setParagonUser(paragon.getUser() as AuthenticatedConnectUser);
+ });
+ }
+ }, [currentWorkspace.token]);
+
+ return {
+ paragonUser,
+ };
+}
+
+export function useParagonIntegrations() {
+ const [integrations, setIntegrations] = useState([]);
+ const { paragonUser } = useParagonUser();
+ useEffect(() => {
+ setIntegrations(paragon.getIntegrationMetadata());
+ }, [paragonUser]);
+
+ return {
+ integrations,
+ };
+}
+
+export function useParagonIntegrationsWithWorklows() {
+ const currentWorkspace = useSelector(getCurrentAppWorkspace);
+ const [integrations, setIntegrations] = useState<
+ Record
+ >({});
+
+ const getIntegrations = async () => {
+ const data = await fetch(
+ `https://api.useparagon.com/projects/${currentWorkspace.projectId}/sdk/integrations`,
+ {
+ headers: {
+ Authorization: `Bearer ${currentWorkspace.token}`,
+ },
+ },
+ ).then((res) => res.json());
+
+ setIntegrations(keyBy(data || [], "id"));
+ };
+
+ useEffect(() => {
+ if (currentWorkspace.token && currentWorkspace.projectId) {
+ getIntegrations();
+ }
+ }, [currentWorkspace.token]);
+
+ return integrations;
+}
diff --git a/app/client/src/workers/Evaluation/fns/datasourceFns.ts b/app/client/src/workers/Evaluation/fns/datasourceFns.ts
new file mode 100644
index 000000000000..54dd5ad51eb6
--- /dev/null
+++ b/app/client/src/workers/Evaluation/fns/datasourceFns.ts
@@ -0,0 +1,26 @@
+const generateJWT = () => {
+ return "JWT token";
+};
+
+export async function datasourceRequest(...args: any) {
+ const [requestType, apiPath, options = {}] = args;
+ try {
+ const PROJECT_ID = "PROJECT_ID";
+ const data = await fetch(
+ `https://proxy.useparagon.com/projects/${PROJECT_ID}/sdk/proxy/${requestType}/${apiPath}`,
+ {
+ method: options.method || "GET",
+ body: options.body ? JSON.stringify(options.body) : undefined,
+ headers: {
+ Authorization: `Bearer ${generateJWT()}`,
+ "Content-Type": "application/json",
+ },
+ },
+ ).then((res) => res.json());
+ console.log(data);
+ return data;
+ } catch (e) {
+ console.trace(e);
+ throw e;
+ }
+}
diff --git a/app/client/src/workers/Evaluation/fns/index.ts b/app/client/src/workers/Evaluation/fns/index.ts
index a932688c11ca..70df0d838b0b 100644
--- a/app/client/src/workers/Evaluation/fns/index.ts
+++ b/app/client/src/workers/Evaluation/fns/index.ts
@@ -60,6 +60,7 @@ import {
} from "./geolocationFns";
import { getFnWithGuards, isAsyncGuard } from "./utils/fnGuard";
import { isRunNClearFnQualifierEntity } from "ee/workers/Evaluation/fns/utils/isRunNClearFnQualifierEntity";
+import { datasourceRequest } from "./datasourceFns";
export const getPlatformFunctions = () => {
return platformFns;
@@ -172,6 +173,15 @@ const entityFns = [
isAsyncGuard,
]),
},
+ {
+ name: "request",
+ path: "appsmith.datasource.request",
+ qualifier: (entity: DataTreeEntity) => isAppsmithEntity(entity),
+ fn: () =>
+ getFnWithGuards(datasourceRequest, "appsmith.datasource.request", [
+ isAsyncGuard,
+ ]),
+ },
];
export type ActionTriggerKeys =
diff --git a/app/client/yarn.lock b/app/client/yarn.lock
index da11c6eb1932..328f407c9ff7 100644
--- a/app/client/yarn.lock
+++ b/app/client/yarn.lock
@@ -12111,6 +12111,18 @@ __metadata:
languageName: node
linkType: hard
+"@useparagon/connect@npm:^1.0.18":
+ version: 1.0.18
+ resolution: "@useparagon/connect@npm:1.0.18"
+ dependencies:
+ hash.js: ^1.1.7
+ jwt-decode: ^3.1.2
+ react: ^17.0.2
+ tslib: 2.3.1
+ checksum: 968b3f3d824186b2335870d2c80117626698b6457253cfadb49fce073f2957e6b647470a89295b2fbc117a6b57d283722dd0f41283575895421722bb47b791b7
+ languageName: node
+ linkType: hard
+
"@vitejs/plugin-react@npm:^4.3.1":
version: 4.3.1
resolution: "@vitejs/plugin-react@npm:4.3.1"
@@ -12958,6 +12970,7 @@ __metadata:
"@uppy/url": ^1.5.16
"@uppy/utils": ^6.0.2
"@uppy/webcam": ^1.8.4
+ "@useparagon/connect": ^1.0.18
"@welldone-software/why-did-you-render": ^4.2.5
acorn: 8.10.0
acorn-walk: 8.2.0
@@ -20208,7 +20221,7 @@ __metadata:
languageName: node
linkType: hard
-"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3":
+"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3, hash.js@npm:^1.1.7":
version: 1.1.7
resolution: "hash.js@npm:1.1.7"
dependencies:
@@ -23358,6 +23371,13 @@ __metadata:
languageName: node
linkType: hard
+"jwt-decode@npm:^3.1.2":
+ version: 3.1.2
+ resolution: "jwt-decode@npm:3.1.2"
+ checksum: 20a4b072d44ce3479f42d0d2c8d3dabeb353081ba4982e40b83a779f2459a70be26441be6c160bfc8c3c6eadf9f6380a036fbb06ac5406b5674e35d8c4205eeb
+ languageName: node
+ linkType: hard
+
"kdbush@npm:^3.0.0":
version: 3.0.0
resolution: "kdbush@npm:3.0.0"
@@ -32739,6 +32759,13 @@ __metadata:
languageName: node
linkType: hard
+"tslib@npm:2.3.1":
+ version: 2.3.1
+ resolution: "tslib@npm:2.3.1"
+ checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9
+ languageName: node
+ linkType: hard
+
"tslib@npm:^1.13.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/InitUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/InitUtils.java
index 932487650a61..585aa698b04e 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/InitUtils.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/InitUtils.java
@@ -5,13 +5,45 @@
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.DatasourceConfiguration;
import lombok.NoArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
@NoArgsConstructor
+@Component
public class InitUtils {
+ @Value("${appsmith.integration.provider.project.id}")
+ private String integrationProviderProjectId = "bd856e23-29d6-418f-9a5d-3c04d11c5fd9";
+
public String initializeRequestUrl(
ActionConfiguration actionConfiguration, DatasourceConfiguration datasourceConfiguration) {
String path = (actionConfiguration.getPath() == null) ? "" : actionConfiguration.getPath();
+ if ((datasourceConfiguration.getProperties().size() != 0)
+ && (datasourceConfiguration.getProperties().get(0).getKey().equals("integrationId"))) {
+ String apiPath = actionConfiguration
+ .getFormData()
+ .getOrDefault("apiPath", "")
+ .toString();
+ String actionType = actionConfiguration
+ .getFormData()
+ .getOrDefault("actionType", "")
+ .toString();
+ String workflowId = actionConfiguration
+ .getFormData()
+ .getOrDefault("workflowId", "")
+ .toString();
+ if (actionType.equals("workflow")) {
+ return String.format(
+ "https://api.useparagon.com/projects/%s/sdk/triggers/%s",
+ integrationProviderProjectId, workflowId);
+ } else {
+ return String.format(
+ "https://proxy.useparagon.com/projects/%s/sdk/proxy/%s%s",
+ integrationProviderProjectId,
+ datasourceConfiguration.getProperties().get(1).getValue(),
+ apiPath);
+ }
+ }
return datasourceConfiguration.getUrl().trim() + path.trim();
}
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java
index 88a94d2323f6..5ab62c35134d 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java
@@ -16,13 +16,16 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.NoArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
+import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ClientResponse;
@@ -38,6 +41,9 @@
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
@@ -49,7 +55,11 @@
import static org.springframework.util.CollectionUtils.isEmpty;
@NoArgsConstructor
+@Component
public class RestAPIActivateUtils {
+ @Value("${appsmith.integration.provider.signing.key}")
+ private String integrationProviderSigningKey =
+ "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";
public static final String SIGNATURE_HEADER_NAME = "X-APPSMITH-SIGNATURE";
public static final String RESPONSE_DATA_TYPE = "X-APPSMITH-DATATYPE";
@@ -254,6 +264,35 @@ public WebClient.Builder getWebClientBuilder(
return webClientBuilder;
}
+ private PrivateKey getPrivateKey() throws Exception {
+ // Decode the Base64-encoded private key
+ byte[] keyBytes = Base64.getDecoder().decode(integrationProviderSigningKey);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
+
+ // Generate an RSA private key from the key specification
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ return keyFactory.generatePrivate(keySpec);
+ }
+
+ public String generateWorkspaceJWT(String workspaceId) throws Exception {
+ // Get the RSA private key
+ PrivateKey privateKey = getPrivateKey();
+
+ // Define the current time and expiration for the token
+ final Instant now = Instant.now();
+
+ // Build and sign the token with RS256 algorithm
+ final String token = Jwts.builder()
+ .setIssuer("Appsmith") // Token issuer
+ .setSubject(workspaceId) // Subject is the workspaceId
+ .setIssuedAt(new Date(now.toEpochMilli())) // Set issued at time
+ .setExpiration(new Date(now.plusSeconds(86400).toEpochMilli())) // Token expiration (1 day later)
+ .signWith(privateKey, SignatureAlgorithm.RS256) // Sign with RSA private key
+ .compact(); // Build the token
+
+ return token;
+ }
+
protected void addSecretKey(WebClient.Builder webClientBuilder, DatasourceConfiguration datasourceConfiguration)
throws AppsmithPluginException {
// If users have chosen to share the Appsmith signature in the header, calculate and add that
diff --git a/app/server/appsmith-interfaces/src/main/resources/application.properties b/app/server/appsmith-interfaces/src/main/resources/application.properties
new file mode 100644
index 000000000000..d6cfedaa504c
--- /dev/null
+++ b/app/server/appsmith-interfaces/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+appsmith.integration.provider.signing.key=${APPSMITH_INTEGRATION_PROVIDER_SIGNING_KEY:}
+appsmith.integration.provider.project.id=${APPSMITH_INTEGRATION_PROVIDER_PROJECT_ID:}
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java
index 3844efa2bfbd..f01becec43c1 100644
--- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java
+++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java
@@ -72,6 +72,24 @@ public Mono executeParameterized(
DatasourceConfiguration datasourceConfiguration,
ActionConfiguration actionConfiguration) {
+ // for paragon poc
+ if ((datasourceConfiguration.getProperties().size() != 0)
+ && (datasourceConfiguration.getProperties().get(0).getKey().equals("integrationId"))) {
+ Property bearerTokenHeader = new Property();
+ bearerTokenHeader.setKey("Authorization");
+ String generatedToken;
+ try {
+ generatedToken = restAPIActivateUtils.generateWorkspaceJWT(executeActionDTO.getWorkspaceId());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ bearerTokenHeader.setValue("Bearer " + generatedToken);
+ List headers = datasourceConfiguration.getHeaders() == null
+ ? new ArrayList<>()
+ : datasourceConfiguration.getHeaders();
+ headers.add(bearerTokenHeader);
+ datasourceConfiguration.setHeaders(headers);
+ }
log.debug(Thread.currentThread().getName()
+ ": executeParameterized() called for RestAPI plugin. Executing the API call.");
final List properties = actionConfiguration.getPluginSpecifiedTemplates();
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/WorkspaceControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/WorkspaceControllerCE.java
index e64efd89923f..d41f55a2a34b 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/WorkspaceControllerCE.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/WorkspaceControllerCE.java
@@ -7,6 +7,7 @@
import com.appsmith.server.dtos.PermissionGroupInfoDTO;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.dtos.UpdatePermissionGroupDTO;
+import com.appsmith.server.dtos.WorkspaceTokenDTO;
import com.appsmith.server.services.UserWorkspaceService;
import com.appsmith.server.services.WorkspaceService;
import com.fasterxml.jackson.annotation.JsonView;
@@ -113,4 +114,12 @@ public Mono>> workspacesForHome() {
.getUserWorkspacesByRecentlyUsedOrder()
.map(workspaces -> new ResponseDTO<>(HttpStatus.OK.value(), workspaces, null));
}
+
+ @JsonView(Views.Public.class)
+ @GetMapping("/{workspaceId}/token")
+ public Mono> generateWorkspaceToken(@PathVariable String workspaceId)
+ throws Exception {
+ return service.generateWorkspaceToken(workspaceId)
+ .map(workspaceTokenDTO -> new ResponseDTO<>(HttpStatus.OK.value(), workspaceTokenDTO, null));
+ }
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/WorkspaceTokenDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/WorkspaceTokenDTO.java
new file mode 100644
index 000000000000..e414368714b0
--- /dev/null
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/WorkspaceTokenDTO.java
@@ -0,0 +1,12 @@
+package com.appsmith.server.dtos;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class WorkspaceTokenDTO {
+ String workspaceId;
+ String token;
+ String projectId;
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/WorkspaceServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/WorkspaceServiceCE.java
index 40ce39ce0298..a29b5c4ccd3a 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/WorkspaceServiceCE.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/WorkspaceServiceCE.java
@@ -4,6 +4,7 @@
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.PermissionGroupInfoDTO;
+import com.appsmith.server.dtos.WorkspaceTokenDTO;
import com.appsmith.server.services.CrudService;
import org.springframework.http.codec.multipart.Part;
import reactor.core.publisher.Flux;
@@ -43,4 +44,6 @@ public interface WorkspaceServiceCE extends CrudService {
Mono archiveById(String s);
Mono getDefaultEnvironmentId(String workspaceId, AclPermission aclPermission);
+
+ Mono generateWorkspaceToken(String workspaceId) throws Exception;
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/WorkspaceServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/WorkspaceServiceCEImpl.java
index 10f08224596e..113e29d1f81d 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/WorkspaceServiceCEImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/WorkspaceServiceCEImpl.java
@@ -14,6 +14,7 @@
import com.appsmith.server.dtos.Permission;
import com.appsmith.server.dtos.PermissionGroupInfoDTO;
import com.appsmith.server.dtos.WorkspacePluginStatus;
+import com.appsmith.server.dtos.WorkspaceTokenDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.TextUtils;
@@ -30,10 +31,13 @@
import com.appsmith.server.solutions.PermissionGroupPermission;
import com.appsmith.server.solutions.PolicySolution;
import com.appsmith.server.solutions.WorkspacePermission;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.validation.Validator;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.http.codec.multipart.Part;
import org.springframework.stereotype.Service;
@@ -41,7 +45,13 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.time.Instant;
import java.util.Arrays;
+import java.util.Base64;
+import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -81,6 +91,12 @@ public class WorkspaceServiceCEImpl extends BaseService getAll(AclPermission permission) {
return repository.findAll(permission);
}
+
+ private PrivateKey getPrivateKey() throws Exception {
+ // Decode the Base64-encoded private key
+ byte[] keyBytes = Base64.getDecoder().decode(integrationProviderSigningKey);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
+
+ // Generate an RSA private key from the key specification
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ return keyFactory.generatePrivate(keySpec);
+ }
+
+ private String generateWorkspaceJWT(String workspaceId) throws Exception {
+ // Get the RSA private key
+ PrivateKey privateKey = getPrivateKey();
+
+ // Define the current time and expiration for the token
+ final Instant now = Instant.now();
+
+ // Build and sign the token with RS256 algorithm
+ final String token = Jwts.builder()
+ .setIssuer("Appsmith") // Token issuer
+ .setSubject(workspaceId) // Subject is the workspaceId
+ .setIssuedAt(new Date(now.toEpochMilli())) // Set issued at time
+ .setExpiration(new Date(now.plusSeconds(86400).toEpochMilli())) // Token expiration (1 day later)
+ .signWith(privateKey, SignatureAlgorithm.RS256) // Sign with RSA private key
+ .compact(); // Build the token
+
+ return token;
+ }
+
+ @Override
+ public Mono generateWorkspaceToken(String workspaceId) throws Exception {
+ String jwtToken = generateWorkspaceJWT(workspaceId);
+ WorkspaceTokenDTO workspaceTokenDTO = new WorkspaceTokenDTO();
+ workspaceTokenDTO.setToken(jwtToken);
+ workspaceTokenDTO.setWorkspaceId(workspaceId);
+ workspaceTokenDTO.setProjectId(integrationProviderProjectId);
+ return Mono.just(workspaceTokenDTO);
+ }
}
diff --git a/app/server/appsmith-server/src/main/resources/application.properties b/app/server/appsmith-server/src/main/resources/application.properties
index e1794aa238a3..099ca8d9aecc 100644
--- a/app/server/appsmith-server/src/main/resources/application.properties
+++ b/app/server/appsmith-server/src/main/resources/application.properties
@@ -130,3 +130,6 @@ appsmith.index.lock.file.time=${APPSMITH_INDEX_LOCK_FILE_TIME:300}
springdoc.api-docs.path=/v3/docs
springdoc.swagger-ui.path=/v3/swagger
+
+appsmith.integration.provider.signing.key=${APPSMITH_INTEGRATION_PROVIDER_SIGNING_KEY:}
+appsmith.integration.provider.project.id=${APPSMITH_INTEGRATION_PROVIDER_PROJECT_ID:}