feat: Support OAuth for all plugin types (#9657)

* customize datasource authorization

* improve performance

* fix save datasource bug

* switch auth type from form config

* better naming of components

* fix minor bug

* minor bug fix

* minor bug fix

* syntax cleanup

* Add comments where necessary

* Added comments where necessary

* fix broken airtable page

* code refactor and annotation
This commit is contained in:
Favour Ohanekwu 2022-01-13 22:31:54 -08:00 committed by GitHub
parent 2bcd73e41d
commit 72f8b7e2e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 514 additions and 273 deletions

View File

@ -194,7 +194,7 @@ export const storeAsDatasource = () => {
export const getOAuthAccessToken = (datasourceId: string) => {
return {
type: ReduxActionTypes.SAAS_GET_OAUTH_ACCESS_TOKEN,
type: ReduxActionTypes.GET_OAUTH_ACCESS_TOKEN,
payload: { datasourceId },
};
};

View File

@ -2,5 +2,5 @@ import { getAppsmithConfigs } from "@appsmith/configs";
const { cloudServicesBaseUrl: BASE_URL } = getAppsmithConfigs();
export const authorizeSaasWithAppsmithToken = (appsmithToken: string) =>
export const authorizeDatasourceWithAppsmithToken = (appsmithToken: string) =>
`${BASE_URL}/api/v1/integrations/oauth/authorize?appsmithToken=${appsmithToken}`;

View File

@ -0,0 +1,28 @@
import Api from "./Api";
import { AxiosPromise } from "axios";
import { GenericApiResponse } from "api/ApiResponses";
import { Datasource } from "entities/Datasource";
class OAuthApi extends Api {
static url = "v1/saas";
// Api endpoint to get "Appsmith token" from server
static getAppsmithToken(
datasourceId: string,
pageId: string,
): AxiosPromise<GenericApiResponse<string>> {
return Api.post(`${OAuthApi.url}/${datasourceId}/pages/${pageId}/oauth`);
}
// Api endpoint to get access token for datasource authorization
static getAccessToken(
datasourceId: string,
token: string,
): AxiosPromise<GenericApiResponse<Datasource>> {
return Api.post(
`${OAuthApi.url}/${datasourceId}/token?appsmithToken=${token}`,
);
}
}
export default OAuthApi;

View File

@ -555,6 +555,7 @@ export const ReduxActionTypes = {
RESET_APPLICATION_WIDGET_STATE_REQUEST:
"RESET_APPLICATION_WIDGET_STATE_REQUEST",
SAAS_GET_OAUTH_ACCESS_TOKEN: "SAAS_GET_OAUTH_ACCESS_TOKEN",
GET_OAUTH_ACCESS_TOKEN: "GET_OAUTH_ACCESS_TOKEN",
UPDATE_RECENT_ENTITY: "UPDATE_RECENT_ENTITY",
RESTORE_RECENT_ENTITIES_REQUEST: "RESTORE_RECENT_ENTITIES_REQUEST",
RESTORE_RECENT_ENTITIES_SUCCESS: "RESTORE_RECENT_ENTITIES_SUCCESS",

View File

@ -287,12 +287,12 @@ export const REST_API_AUTHORIZATION_FAILED = () =>
export const REST_API_AUTHORIZATION_APPSMITH_ERROR = () =>
"Something went wrong.";
export const SAAS_AUTHORIZATION_SUCCESSFUL = "Authorization was successful!";
export const SAAS_AUTHORIZATION_FAILED =
export const OAUTH_AUTHORIZATION_SUCCESSFUL = "Authorization was successful!";
export const OAUTH_AUTHORIZATION_FAILED =
"Authorization failed. Please check your details or try again.";
// Todo: improve this for appsmith_error error message
export const SAAS_AUTHORIZATION_APPSMITH_ERROR = "Something went wrong.";
export const SAAS_APPSMITH_TOKEN_NOT_FOUND = "Appsmith token not found";
export const OAUTH_AUTHORIZATION_APPSMITH_ERROR = "Something went wrong.";
export const OAUTH_APPSMITH_TOKEN_NOT_FOUND = "Appsmith token not found";
export const LOCAL_STORAGE_QUOTA_EXCEEDED_MESSAGE = () =>
"Error saving a key in localStorage. You have exceeded the allowed storage size limit";

View File

@ -1,6 +1,17 @@
import { APIResponseError } from "api/ApiResponses";
import { ActionConfig, Property } from "entities/Action";
import _ from "lodash";
export enum AuthType {
OAUTH2 = "oAuth2",
DBAUTH = "dbAuth",
}
export enum AuthenticationStatus {
NONE = "NONE",
IN_PROGRESS = "IN_PROGRESS",
SUCCESS = "SUCCESS",
}
export interface DatasourceAuthentication {
authType?: string;
username?: string;
@ -11,6 +22,7 @@ export interface DatasourceAuthentication {
addTo?: string;
bearerToken?: string;
authenticationStatus?: string;
authenticationType?: string;
}
export interface DatasourceColumns {

View File

@ -9,8 +9,6 @@ import Button, { Category } from "components/ads/Button";
import { Colors } from "constants/Colors";
import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleHelp";
import Connected from "./Connected";
import EditButton from "components/editorComponents/Button";
import { Datasource } from "entities/Datasource";
import { reduxForm, InjectedFormProps } from "redux-form";
import { APPSMITH_IP_ADDRESSES } from "constants/DatasourceEditorConstants";
@ -24,47 +22,33 @@ import Callout from "components/ads/Callout";
import { Variant } from "components/ads/common";
import { AppState } from "reducers";
import {
ActionButton,
FormTitleContainer,
Header,
JSONtoForm,
JSONtoFormProps,
PluginImage,
SaveButtonContainer,
} from "./JSONtoForm";
import { ButtonVariantTypes } from "components/constants";
import DatasourceAuth from "../../common/datasourceAuth";
const { cloudHosting } = getAppsmithConfigs();
interface DatasourceDBEditorProps extends JSONtoFormProps {
onSave: (formValues: Datasource) => void;
onTest: (formValus: Datasource) => void;
handleDelete: (id: string) => void;
setDatasourceEditorMode: (id: string, viewMode: boolean) => void;
openOmnibarReadMore: (text: string) => void;
isSaving: boolean;
isDeleting: boolean;
datasourceId: string;
applicationId: string;
pageId: string;
isTesting: boolean;
isNewDatasource: boolean;
pluginImage: string;
viewMode: boolean;
pluginType: string;
messages?: Array<string>;
datasource: Datasource;
}
type Props = DatasourceDBEditorProps &
InjectedFormProps<Datasource, DatasourceDBEditorProps>;
const StyledButton = styled(EditButton)`
&&&& {
width: 87px;
height: 32px;
}
`;
const StyledOpenDocsIcon = styled(Icon)`
svg {
width: 12px;
@ -93,15 +77,9 @@ class DatasourceDBEditor extends JSONtoForm<Props> {
this.props.setDatasourceEditorMode(this.props.datasourceId, true);
}
}
save = () => {
const normalizedValues = this.normalizeValues();
const trimmedValues = this.getTrimmedData(normalizedValues);
AnalyticsUtil.logEvent("SAVE_DATA_SOURCE_CLICK", {
pageId: this.props.pageId,
appId: this.props.applicationId,
});
this.props.onSave(trimmedValues);
// returns normalized and trimmed datasource form data
getSanitizedData = () => {
return this.getTrimmedData(this.normalizeValues());
};
openOmnibarReadMore = () => {
@ -110,34 +88,16 @@ class DatasourceDBEditor extends JSONtoForm<Props> {
AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "READ_MORE_DATASOURCE" });
};
test = () => {
const normalizedValues = this.normalizeValues();
const trimmedValues = this.getTrimmedData(normalizedValues);
AnalyticsUtil.logEvent("TEST_DATA_SOURCE_CLICK", {
pageId: this.props.pageId,
appId: this.props.applicationId,
});
this.props.onTest(trimmedValues);
};
render() {
const { formConfig } = this.props;
const content = this.renderDataSourceConfigForm(formConfig);
return this.renderForm(content);
}
renderDataSourceConfigForm = (sections: any) => {
const {
datasourceId,
handleDelete,
isDeleting,
isSaving,
isTesting,
messages,
pluginType,
} = this.props;
const { viewMode } = this.props;
const { datasource, formData, messages, pluginType, viewMode } = this.props;
return (
<form
onSubmit={(e) => {
@ -193,41 +153,21 @@ class DatasourceDBEditor extends JSONtoForm<Props> {
{!_.isNil(sections)
? _.map(sections, this.renderMainSection)
: undefined}
<SaveButtonContainer>
<ActionButton
buttonStyle="DANGER"
buttonVariant={ButtonVariantTypes.PRIMARY}
// accent="error"
className="t--delete-datasource"
loading={isDeleting}
onClick={() => handleDelete(datasourceId)}
text="Delete"
/>
<ActionButton
// accent="secondary"
buttonStyle="PRIMARY"
buttonVariant={ButtonVariantTypes.SECONDARY}
className="t--test-datasource"
loading={isTesting}
onClick={this.test}
text="Test"
/>
<StyledButton
className="t--save-datasource"
disabled={this.validate()}
filled
intent="primary"
loading={isSaving}
onClick={this.save}
size="small"
text="Save"
/>
</SaveButtonContainer>
{""}
</>
) : (
<Connected />
)}
{/* Render datasource form call-to-actions */}
{datasource && (
<DatasourceAuth
datasource={datasource}
formData={formData}
getSanitizedFormData={_.memoize(this.getSanitizedData)}
isInvalid={this.validate()}
shouldRender={!viewMode}
/>
)}
</form>
);
};
@ -242,6 +182,7 @@ const mapStateToProps = (state: AppState, props: any) => {
return {
messages: hintMessages,
datasource,
};
};

View File

@ -4,7 +4,6 @@ import _, { merge } from "lodash";
import { DATASOURCE_SAAS_FORM } from "constants/forms";
import { SAAS_EDITOR_DATASOURCE_ID_URL } from "./constants";
import FormTitle from "pages/Editor/DataSourceEditor/FormTitle";
import Button from "components/editorComponents/Button";
import AdsButton, { Category } from "components/ads/Button";
import { Datasource } from "entities/Datasource";
import { getFormValues, InjectedFormProps, reduxForm } from "redux-form";
@ -12,39 +11,21 @@ import { RouteComponentProps } from "react-router";
import { connect } from "react-redux";
import { AppState } from "reducers";
import { getDatasource, getPluginImages } from "selectors/entitiesSelector";
import { ReduxAction } from "constants/ReduxActionConstants";
import {
deleteDatasource,
getOAuthAccessToken,
redirectAuthorizationCode,
updateDatasource,
} from "actions/datasourceActions";
import { createActionRequest } from "actions/pluginActionActions";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import {
ActionButton,
FormTitleContainer,
Header,
JSONtoForm,
JSONtoFormProps,
PluginImage,
SaveButtonContainer,
} from "../DataSourceEditor/JSONtoForm";
import { getConfigInitialValues } from "components/formControls/utils";
import {
SAAS_AUTHORIZATION_APPSMITH_ERROR,
SAAS_AUTHORIZATION_FAILED,
} from "constants/messages";
import { Variant } from "components/ads/common";
import { Toaster } from "components/ads/Toast";
import { Action, PluginType } from "entities/Action";
import AnalyticsUtil from "utils/AnalyticsUtil";
import Connected from "../DataSourceEditor/Connected";
import { Colors } from "constants/Colors";
import { redirectToNewIntegrations } from "../../../actions/apiPaneActions";
import { ButtonVariantTypes } from "components/constants";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import DatasourceAuth from "../../common/datasourceAuth";
import EntityNotFoundPane from "../EntityNotFoundPane";
interface StateProps extends JSONtoFormProps {
applicationId: string;
@ -58,16 +39,7 @@ interface StateProps extends JSONtoFormProps {
datasource?: Datasource;
}
interface DispatchFunctions {
updateDatasource: (formData: any, onSuccess?: ReduxAction<unknown>) => void;
deleteDatasource: (id: string, onSuccess?: ReduxAction<unknown>) => void;
getOAuthAccessToken: (id: string) => void;
createAction: (data: Partial<Action>) => void;
redirectToNewIntegrations: (applicationId: string, pageId: string) => void;
}
type DatasourceSaaSEditorProps = StateProps &
DispatchFunctions &
RouteComponentProps<{
datasourceId: string;
pageId: string;
@ -77,19 +49,6 @@ type DatasourceSaaSEditorProps = StateProps &
type Props = DatasourceSaaSEditorProps &
InjectedFormProps<Datasource, DatasourceSaaSEditorProps>;
enum AuthenticationStatus {
NONE = "NONE",
IN_PROGRESS = "IN_PROGRESS",
SUCCESS = "SUCCESS",
}
const StyledButton = styled(Button)`
&&&& {
width: 180px;
height: 32px;
}
`;
const EditDatasourceButton = styled(AdsButton)`
padding: 10px 20px;
&&&& {
@ -100,60 +59,25 @@ const EditDatasourceButton = styled(AdsButton)`
}
`;
const StyledAuthMessage = styled.div`
color: ${(props) => props.theme.colors.error};
margin-top: 15px;
&:after {
content: " *";
color: inherit;
}
`;
class DatasourceSaaSEditor extends JSONtoForm<Props> {
componentDidMount() {
super.componentDidMount();
const search = new URLSearchParams(this.props.location.search);
const status = search.get("response_status");
if (status) {
const display_message = search.get("display_message");
// Set default error message
let message = SAAS_AUTHORIZATION_FAILED;
const variant = Variant.danger;
if (status !== "success") {
if (status === "appsmith_error") {
message = SAAS_AUTHORIZATION_APPSMITH_ERROR;
}
Toaster.show({ text: display_message || message, variant });
} else {
this.props.getOAuthAccessToken(this.props.match.params.datasourceId);
}
AnalyticsUtil.logEvent("GSHEET_AUTH_COMPLETE", {
applicationId: _.get(this.props, "applicationId"),
datasourceId: _.get(this.props, "match.params.datasourceId"),
pageId: _.get(this.props, "match.params.pageId"),
});
}
}
save = (onSuccess?: ReduxAction<unknown>) => {
const normalizedValues = this.normalizeValues();
this.props.updateDatasource(normalizedValues, onSuccess);
};
render() {
const { formConfig } = this.props;
const { formConfig, pluginId } = this.props;
if (!pluginId) {
return <EntityNotFoundPane />;
}
const content = this.renderDataSourceConfigForm(formConfig);
return this.renderForm(content);
}
getSanitizedData = () => {
return this.normalizeValues();
};
renderDataSourceConfigForm = (sections: any) => {
const {
applicationId,
datasource,
deleteDatasource,
isDeleting,
isSaving,
formData,
match: {
params: { datasourceId, pageId, pluginPackageName },
},
@ -161,10 +85,6 @@ class DatasourceSaaSEditor extends JSONtoForm<Props> {
const params: string = location.search;
const viewMode = new URLSearchParams(params).get("viewMode");
const isAuthorized =
datasource?.datasourceConfiguration.authentication
?.authenticationStatus === AuthenticationStatus.SUCCESS;
return (
<form
onSubmit={(e) => {
@ -203,60 +123,20 @@ class DatasourceSaaSEditor extends JSONtoForm<Props> {
{!_.isNil(sections)
? _.map(sections, this.renderMainSection)
: null}
{!isAuthorized && (
<StyledAuthMessage>Datasource not authorized</StyledAuthMessage>
)}
<SaveButtonContainer>
<ActionButton
// accent="error"
buttonStyle="DANGER"
buttonVariant={ButtonVariantTypes.PRIMARY}
className="t--delete-datasource"
loading={isDeleting}
onClick={() =>
deleteDatasource(
datasourceId,
this.props.redirectToNewIntegrations(
applicationId,
pageId,
) as any,
)
}
text="Delete"
/>
<StyledButton
className="t--save-datasource"
disabled={this.validate()}
filled
intent="primary"
loading={isSaving}
onClick={() => {
AnalyticsUtil.logEvent("GSHEET_AUTH_INIT", {
applicationId,
datasourceId,
pageId,
});
this.save(
redirectAuthorizationCode(
pageId,
datasourceId,
PluginType.SAAS,
),
);
}}
size="small"
text={isAuthorized ? "Re-authorize" : "Authorize"}
/>
</SaveButtonContainer>
{""}
</>
) : (
<>
<Connected />
{!isAuthorized && (
<StyledAuthMessage>Datasource not authorized</StyledAuthMessage>
)}
</>
<Connected />
)}
{/* Render datasource form call-to-actions */}
{datasource && (
<DatasourceAuth
datasource={datasource}
formData={formData}
getSanitizedFormData={_.memoize(this.getSanitizedData)}
isInvalid={this.validate()}
shouldRender={!viewMode}
/>
)}
</form>
);
@ -293,27 +173,7 @@ const mapStateToProps = (state: AppState, props: any) => {
};
};
const mapDispatchToProps = (dispatch: any): DispatchFunctions => {
return {
deleteDatasource: (id: string, onSuccess?: ReduxAction<unknown>) =>
dispatch(deleteDatasource({ id }, onSuccess)),
updateDatasource: (formData: any, onSuccess?: ReduxAction<unknown>) =>
dispatch(updateDatasource(formData, onSuccess)),
getOAuthAccessToken: (datasourceId: string) =>
dispatch(getOAuthAccessToken(datasourceId)),
createAction: (data: Partial<Action>) => {
dispatch(createActionRequest(data));
},
redirectToNewIntegrations: (applicationId: string, pageId: string) => {
dispatch(redirectToNewIntegrations(applicationId, pageId));
},
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(
export default connect(mapStateToProps)(
reduxForm<Datasource, DatasourceSaaSEditorProps>({
form: DATASOURCE_SAAS_FORM,
enableReinitialize: true,

View File

@ -0,0 +1,132 @@
import { ButtonVariantTypes } from "components/constants";
import { Datasource } from "entities/Datasource";
import {
ActionButton,
SaveButtonContainer,
} from "pages/Editor/DataSourceEditor/JSONtoForm";
import React from "react";
import styled from "styled-components";
import EditButton from "components/editorComponents/Button";
import { useDispatch, useSelector } from "react-redux";
import { getEntities } from "selectors/entitiesSelector";
import {
testDatasource,
deleteDatasource,
updateDatasource,
} from "actions/datasourceActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { redirectToNewIntegrations } from "actions/apiPaneActions";
import { getQueryParams } from "utils/AppsmithUtils";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { useParams } from "react-router";
import { ExplorerURLParams } from "pages/Editor/Explorer/helpers";
import { getIsGeneratePageInitiator } from "utils/GenerateCrudUtil";
interface Props {
datasource: Datasource;
getSanitizedFormData: () => Datasource;
isInvalid: boolean;
shouldRender: boolean;
}
const StyledButton = styled(EditButton)`
&&&& {
width: 87px;
height: 32px;
}
`;
export default function DefaultAuth({
datasource,
getSanitizedFormData,
isInvalid,
shouldRender,
}: Props): JSX.Element {
const { id: datasourceId } = datasource;
const applicationId = useSelector(getCurrentApplicationId);
const dispatch = useDispatch();
const {
datasources: { isDeleting, isTesting, loading: isSaving },
} = useSelector(getEntities);
const { pageId } = useParams<ExplorerURLParams>();
// Handles datasource deletion
const handleDatasourceDelete = () => {
dispatch(deleteDatasource({ id: datasourceId }));
};
// Handles datasource testing
const handleDatasourceTest = () => {
AnalyticsUtil.logEvent("TEST_DATA_SOURCE_CLICK", {
pageId: pageId,
appId: applicationId,
});
dispatch(testDatasource(getSanitizedFormData()));
};
// Handles datasource saving
const handleDatasourceSave = () => {
const isGeneratePageInitiator = getIsGeneratePageInitiator();
AnalyticsUtil.logEvent("SAVE_DATA_SOURCE_CLICK", {
pageId: pageId,
appId: applicationId,
});
// After saving datasource, only redirect to the 'new integrations' page
// if datasource is not used to generate a page
dispatch(
updateDatasource(
getSanitizedFormData(),
!isGeneratePageInitiator
? dispatch(
redirectToNewIntegrations(
applicationId,
pageId,
getQueryParams(),
),
)
: undefined,
),
);
};
return (
<>
{shouldRender && (
<SaveButtonContainer>
<ActionButton
buttonStyle="DANGER"
buttonVariant={ButtonVariantTypes.PRIMARY}
// accent="error"
className="t--delete-datasource"
loading={isDeleting}
onClick={handleDatasourceDelete}
text="Delete"
/>
<ActionButton
// accent="secondary"
buttonStyle="PRIMARY"
buttonVariant={ButtonVariantTypes.SECONDARY}
className="t--test-datasource"
loading={isTesting}
onClick={handleDatasourceTest}
text="Test"
/>
<StyledButton
className="t--save-datasource"
disabled={isInvalid}
filled
intent="primary"
loading={isSaving}
onClick={handleDatasourceSave}
size="small"
text="Save"
/>
</SaveButtonContainer>
)}
{""}
</>
);
}

View File

@ -0,0 +1,161 @@
import { ButtonVariantTypes } from "components/constants";
import { AuthenticationStatus, Datasource } from "entities/Datasource";
import {
ActionButton,
SaveButtonContainer,
} from "pages/Editor/DataSourceEditor/JSONtoForm";
import React, { useEffect } from "react";
import styled from "styled-components";
import EditButton from "components/editorComponents/Button";
import { useDispatch, useSelector } from "react-redux";
import {
getEntities,
getPluginTypeFromDatasourceId,
} from "selectors/entitiesSelector";
import {
deleteDatasource,
getOAuthAccessToken,
redirectAuthorizationCode,
updateDatasource,
} from "actions/datasourceActions";
import { AppState } from "reducers";
import {
OAUTH_AUTHORIZATION_APPSMITH_ERROR,
OAUTH_AUTHORIZATION_FAILED,
} from "constants/messages";
import { Variant } from "components/ads/common";
import { Toaster } from "components/ads/Toast";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { useLocation, useParams } from "react-router";
import { ExplorerURLParams } from "pages/Editor/Explorer/helpers";
interface Props {
datasource: Datasource;
getSanitizedFormData: () => Datasource;
isInvalid: boolean;
shouldRender: boolean;
}
enum AuthorizationStatus {
SUCCESS = "success",
APPSMITH_ERROR = "appsmith_error",
}
const StyledButton = styled(EditButton)`
&&&& {
height: 32px;
}
`;
const StyledAuthMessage = styled.div`
color: ${(props) => props.theme.colors.error};
margin-top: 15px;
&:after {
content: " *";
color: inherit;
}
`;
function OAuth({
datasource,
getSanitizedFormData,
isInvalid,
shouldRender,
}: Props): JSX.Element {
const { id: datasourceId } = datasource;
const {
datasources: { isDeleting, loading: isSaving },
} = useSelector(getEntities);
const isAuthorized =
datasource.datasourceConfiguration.authentication?.authenticationStatus ===
AuthenticationStatus.SUCCESS;
const pluginType = useSelector((state: AppState) =>
getPluginTypeFromDatasourceId(state, datasourceId),
);
const applicationId = useSelector(getCurrentApplicationId);
const dispatch = useDispatch();
const location = useLocation();
const { pageId } = useParams<ExplorerURLParams>();
// Handles datasource saving
const handleDatasourceSave = () => {
dispatch(
updateDatasource(
getSanitizedFormData(),
pluginType
? redirectAuthorizationCode(pageId, datasourceId, pluginType)
: undefined,
),
);
};
// Handles datasource deletion
const handleDatasourceDelete = () => {
dispatch(deleteDatasource({ id: datasourceId }));
};
useEffect(() => {
// When the authorization server redirects a user to the datasource form page, the url contains the "response_status" query parameter .
// Get the access token if response_status is successful else show a toast error
const search = new URLSearchParams(location.search);
const status = search.get("response_status");
if (status) {
const display_message = search.get("display_message");
const variant = Variant.danger;
if (status !== AuthorizationStatus.SUCCESS) {
const message =
status === AuthorizationStatus.APPSMITH_ERROR
? OAUTH_AUTHORIZATION_APPSMITH_ERROR
: OAUTH_AUTHORIZATION_FAILED;
Toaster.show({ text: display_message || message, variant });
} else {
dispatch(getOAuthAccessToken(datasourceId));
}
AnalyticsUtil.logEvent("DATASOURCE_AUTH_COMPLETE", {
applicationId,
datasourceId,
pageId,
});
}
}, []);
return (
<>
{!isAuthorized && (
<StyledAuthMessage>Datasource not authorized</StyledAuthMessage>
)}
{shouldRender ? (
<SaveButtonContainer>
<ActionButton
// accent="error"
buttonStyle="DANGER"
buttonVariant={ButtonVariantTypes.PRIMARY}
className="t--delete-datasource"
loading={isDeleting}
onClick={handleDatasourceDelete}
text="Delete"
/>
<StyledButton
className="t--save-datasource"
disabled={isInvalid}
filled
intent="primary"
loading={isSaving}
onClick={handleDatasourceSave}
size="small"
text={isAuthorized ? "Save and Re-authorize" : "Save and Authorize"}
/>
</SaveButtonContainer>
) : null}{" "}
</>
);
}
export default OAuth;

View File

@ -0,0 +1,60 @@
import { AuthType, Datasource } from "entities/Datasource";
import React from "react";
import OAuth from "./OAuth";
import DefaultAuth from "./DefaultAuth";
interface Props {
datasource: Datasource;
formData: Datasource;
getSanitizedFormData: () => Datasource;
isInvalid: boolean;
shouldRender: boolean;
}
function DatasourceAuth({
datasource,
formData,
getSanitizedFormData,
isInvalid,
shouldRender,
}: Props) {
const authType =
formData &&
formData.datasourceConfiguration?.authentication?.authenticationType;
// Render call-to-actions depending on the datasource authentication type
switch (authType) {
case AuthType.OAUTH2:
return (
<OAuth
datasource={datasource}
getSanitizedFormData={getSanitizedFormData}
isInvalid={isInvalid}
shouldRender={shouldRender}
/>
);
case AuthType.DBAUTH:
return (
<DefaultAuth
datasource={datasource}
getSanitizedFormData={getSanitizedFormData}
isInvalid={isInvalid}
shouldRender={shouldRender}
/>
);
default:
return (
<DefaultAuth
datasource={datasource}
getSanitizedFormData={getSanitizedFormData}
isInvalid={isInvalid}
shouldRender={shouldRender}
/>
);
}
}
export default DatasourceAuth;

View File

@ -26,6 +26,7 @@ import {
getDatasourceDraft,
getPluginForm,
getGenerateCRUDEnabledPluginMap,
getPluginPackageFromDatasourceId,
} from "selectors/entitiesSelector";
import {
changeDatasource,
@ -62,24 +63,26 @@ import { Variant } from "components/ads/common";
import { Toaster } from "components/ads/Toast";
import { getConfigInitialValues } from "components/formControls/utils";
import { setActionProperty } from "actions/pluginActionActions";
import SaasApi from "api/SaasApi";
import { authorizeSaasWithAppsmithToken } from "api/CloudServicesApi";
import { authorizeDatasourceWithAppsmithToken } from "api/CloudServicesApi";
import {
createMessage,
DATASOURCE_CREATE,
DATASOURCE_DELETE,
DATASOURCE_UPDATE,
DATASOURCE_VALID,
SAAS_APPSMITH_TOKEN_NOT_FOUND,
SAAS_AUTHORIZATION_APPSMITH_ERROR,
SAAS_AUTHORIZATION_FAILED,
SAAS_AUTHORIZATION_SUCCESSFUL,
OAUTH_APPSMITH_TOKEN_NOT_FOUND,
OAUTH_AUTHORIZATION_APPSMITH_ERROR,
OAUTH_AUTHORIZATION_FAILED,
OAUTH_AUTHORIZATION_SUCCESSFUL,
} from "constants/messages";
import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import localStorage from "utils/localStorage";
import log from "loglevel";
import { APPSMITH_TOKEN_STORAGE_KEY } from "pages/Editor/SaaSEditor/constants";
import {
APPSMITH_TOKEN_STORAGE_KEY,
SAAS_EDITOR_DATASOURCE_ID_URL,
} from "pages/Editor/SaaSEditor/constants";
import { checkAndGetPluginFormConfigsSaga } from "sagas/PluginSagas";
import { PluginType } from "entities/Action";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
@ -90,6 +93,8 @@ import { GenerateCRUDEnabledPluginMap } from "../api/PluginApi";
import { getIsGeneratePageInitiator } from "../utils/GenerateCrudUtil";
import { trimQueryString } from "utils/helpers";
import { updateReplayEntity } from "actions/pageActions";
import OAuthApi from "api/OAuthApi";
import { AppState } from "reducers";
function* fetchDatasourcesSaga() {
try {
@ -218,11 +223,26 @@ export function* deleteDatasourceSaga(
if (isValidResponse) {
const pageId = yield select(getCurrentPageId);
const pluginPackageName = yield select((state: AppState) =>
getPluginPackageFromDatasourceId(state, id),
);
const datasourcePathWithoutQuery = trimQueryString(
DATA_SOURCES_EDITOR_ID_URL(applicationId, pageId, id),
);
if (window.location.pathname === datasourcePathWithoutQuery) {
const saasPathWithoutQuery = trimQueryString(
SAAS_EDITOR_DATASOURCE_ID_URL(
applicationId,
pageId,
pluginPackageName,
id,
),
);
if (
window.location.pathname === datasourcePathWithoutQuery ||
window.location.pathname === saasPathWithoutQuery
) {
history.push(
INTEGRATION_EDITOR_URL(
applicationId,
@ -369,23 +389,26 @@ function* redirectAuthorizationCodeSaga(
if (pluginType === PluginType.API) {
window.location.href = `/api/v1/datasources/${datasourceId}/pages/${pageId}/code`;
} else if (pluginType === PluginType.SAAS) {
} else {
try {
// Get an "appsmith token" from the server
const response: ApiResponse = yield SaasApi.getAppsmithToken(
const response: ApiResponse = yield OAuthApi.getAppsmithToken(
datasourceId,
pageId,
);
if (validateResponse(response)) {
const appsmithToken = response.data;
// Save the token for later use once we come back from the auth flow
localStorage.setItem(APPSMITH_TOKEN_STORAGE_KEY, appsmithToken);
// Redirect to the cloud services to authorise
window.location.assign(authorizeSaasWithAppsmithToken(appsmithToken));
window.location.assign(
authorizeDatasourceWithAppsmithToken(appsmithToken),
);
}
} catch (e) {
Toaster.show({
text: SAAS_AUTHORIZATION_FAILED,
text: OAUTH_AUTHORIZATION_FAILED,
variant: Variant.danger,
});
log.error(e);
@ -401,16 +424,16 @@ function* getOAuthAccessTokenSaga(
const appsmithToken = localStorage.getItem(APPSMITH_TOKEN_STORAGE_KEY);
if (!appsmithToken) {
// Error out because auth token should been here
log.error(SAAS_APPSMITH_TOKEN_NOT_FOUND);
log.error(OAUTH_APPSMITH_TOKEN_NOT_FOUND);
Toaster.show({
text: SAAS_AUTHORIZATION_APPSMITH_ERROR,
text: OAUTH_AUTHORIZATION_APPSMITH_ERROR,
variant: Variant.danger,
});
return;
}
try {
// Get access token for datasource
const response = yield SaasApi.getAccessToken(datasourceId, appsmithToken);
const response = yield OAuthApi.getAccessToken(datasourceId, appsmithToken);
if (validateResponse(response)) {
// Update the datasource object
yield put({
@ -418,7 +441,7 @@ function* getOAuthAccessTokenSaga(
payload: response.data,
});
Toaster.show({
text: SAAS_AUTHORIZATION_SUCCESSFUL,
text: OAUTH_AUTHORIZATION_SUCCESSFUL,
variant: Variant.success,
});
// Remove the token because it is supposed to be short lived
@ -426,7 +449,7 @@ function* getOAuthAccessTokenSaga(
}
} catch (e) {
Toaster.show({
text: SAAS_AUTHORIZATION_FAILED,
text: OAUTH_AUTHORIZATION_FAILED,
variant: Variant.danger,
});
log.error(e);
@ -780,6 +803,7 @@ function* updateDatasourceSuccessSaga(action: UpdateDatasourceSuccessAction) {
const isGeneratePageInitiator = getIsGeneratePageInitiator(
queryParams.isGeneratePageMode,
);
if (
isGeneratePageInitiator &&
updatedDatasource.pluginId &&
@ -990,10 +1014,7 @@ export function* watchDatasourcesSagas() {
ReduxActionTypes.REDIRECT_AUTHORIZATION_CODE,
redirectAuthorizationCodeSaga,
),
takeEvery(
ReduxActionTypes.SAAS_GET_OAUTH_ACCESS_TOKEN,
getOAuthAccessTokenSaga,
),
takeEvery(ReduxActionTypes.GET_OAUTH_ACCESS_TOKEN, getOAuthAccessTokenSaga),
takeEvery(
ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_INIT,
fetchDatasourceStructureSaga,

View File

@ -114,6 +114,21 @@ export const getPluginNameFromId = (
return plugin.name;
};
export const getPluginTypeFromDatasourceId = (
state: AppState,
datasourceId: string,
): PluginType | undefined => {
const datasource = state.entities.datasources.list.find(
(datasource) => datasource.id === datasourceId,
);
const plugin = state.entities.plugins.list.find(
(plugin) => plugin.id === datasource?.pluginId,
);
if (!plugin) return undefined;
return plugin.type;
};
export const getPluginForm = (state: AppState, pluginId: string): any[] => {
return state.entities.plugins.formConfigs[pluginId];
};

View File

@ -199,7 +199,17 @@ export type EventName =
| "GS_CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK"
| "GS_IMPORT_VIA_GIT_CLICK"
| "GS_CONTACT_SALES_CLICK"
| "REFLOW_BETA_FLAG";
| "REFLOW_BETA_FLAG"
| "CONNECT_GIT_CLICK"
| "REPO_URL_EDIT"
| "GENERATE_KEY_BUTTON_CLICK"
| "COPY_SSH_KEY_BUTTON_CLICK"
| "LEARN_MORE_LINK_FOR_REMOTEURL_CLICK"
| "LEARN_MORE_LINK_FOR_SSH_CLICK"
| "DEFAULT_CONFIGURATION_EDIT_BUTTON_CLICK"
| "DEFAULT_CONFIGURATION_CHECKBOX_TOGGLED"
| "CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK"
| "DATASOURCE_AUTH_COMPLETE";
function getApplicationId(location: Location) {
const pathSplit = location.pathname.split("/");

View File

@ -179,7 +179,7 @@
"controlType": "INPUT_TEXT",
"placeholderText": "Password",
"encrypted": true
}
}
]
}
]