Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions backend/connector_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ def get_queryset(self) -> QuerySet | None:
def _get_connector_metadata(self, connector_id: str) -> dict[str, str] | None:
"""Gets connector metadata for the ConnectorInstance.

For non oauth based - obtains from request
For oauth based - obtains from cache

Raises:
e: MissingParamException, CacheMissException
Expand All @@ -84,23 +82,42 @@ def _get_connector_metadata(self, connector_id: str) -> dict[str, str] | None:
if ConnectorInstance.supportsOAuth(connector_id=connector_id):
logger.info(f"Fetching oauth data for {connector_id}")
oauth_key = self.request.query_params.get(ConnectorAuthKey.OAUTH_KEY)
if oauth_key is None:
raise MissingParamException(param=ConnectorAuthKey.OAUTH_KEY)
# Preserve OAuth cache for reuse across multiple operations (Test Connection, Submit,
# File System browsing). Frontend localStorage stores cache keys which must correspond
# to persistent backend credentials for tab switching and repeated operations to work.
if not oauth_key:
raise MissingParamException(
"OAuth authentication required. Please sign in with Google first."
Comment thread
kirtimanmishrazipstack marked this conversation as resolved.
)
logger.info(f"Using OAuth cache key for {connector_id}")
connector_metadata = ConnectorAuthHelper.get_oauth_creds_from_cache(
cache_key=oauth_key,
delete_key=False, # Keep cache - frontend persistence depends on backend credential storage
delete_key=False, # Don't delete yet - wait for successful operation
)
if connector_metadata is None:
raise CacheMissException(
f"Couldn't find credentials for {oauth_key} from cache"
)
raise MissingParamException(param=ConnectorAuthKey.OAUTH_KEY)
else:
connector_metadata = self.request.data.get(CIKey.CONNECTOR_METADATA)
return connector_metadata

def _cleanup_oauth_cache(self, connector_id: str) -> None:
"""Clean up OAuth cache after successful operation."""
if not ConnectorInstance.supportsOAuth(connector_id=connector_id):
return

oauth_key = self.request.query_params.get(ConnectorAuthKey.OAUTH_KEY)
if not oauth_key:
return
logger.info(f"Cleaning up OAuth cache for {connector_id}")
try:
ConnectorAuthHelper.get_oauth_creds_from_cache(
cache_key=oauth_key,
delete_key=True, # Delete after successful operation
)
except CacheMissException:
logger.debug("OAuth cache already cleared for %s", connector_id)
except Exception:
logger.warning(
"Failed to clean up OAuth cache for %s", connector_id, exc_info=True
)

def perform_update(self, serializer: ConnectorInstanceSerializer) -> None:
connector_metadata = None
connector_id = self.request.data.get(
Expand All @@ -122,6 +139,9 @@ def perform_update(self, serializer: ConnectorInstanceSerializer) -> None:
modified_by=self.request.user,
) # type: ignore

# Clean up OAuth cache after successful update
self._cleanup_oauth_cache(connector_id)

def perform_create(self, serializer: ConnectorInstanceSerializer) -> None:
connector_metadata = None
connector_id = self.request.data.get(CIKey.CONNECTOR_ID)
Expand All @@ -138,6 +158,9 @@ def perform_create(self, serializer: ConnectorInstanceSerializer) -> None:
modified_by=self.request.user,
) # type: ignore

# Clean up OAuth cache after successful create
self._cleanup_oauth_cache(connector_id)

def create(self, request: Any) -> Response:
# Overriding default exception behavior
serializer = self.get_serializer(data=request.data)
Expand Down
24 changes: 20 additions & 4 deletions frontend/src/components/input-output/configure-ds/ConfigureDs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { createRef, useEffect, useState } from "react";

import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate";
import { useExceptionHandler } from "../../../hooks/useExceptionHandler.jsx";
import usePostHogEvents from "../../../hooks/usePostHogEvents.js";
import useRequestUrl from "../../../hooks/useRequestUrl";
import { RjsfFormLayout } from "../../../layouts/rjsf-form-layout/RjsfFormLayout.jsx";
import { useAlertStore } from "../../../store/alert-store";
import { useSessionStore } from "../../../store/session-store";
import { OAuthDs } from "../../oauth-ds/oauth-ds/OAuthDs.jsx";
import { CustomButton } from "../../widgets/custom-button/CustomButton.jsx";
import "./ConfigureDs.css";
import usePostHogEvents from "../../../hooks/usePostHogEvents.js";
import useRequestUrl from "../../../hooks/useRequestUrl";

function ConfigureDs({
spec,
Expand Down Expand Up @@ -51,6 +51,11 @@ function ConfigureDs({
const oauthCacheKey = `oauth-cachekey-${selectedSourceId}`;
const oauthStatusKey = `oauth-status-${selectedSourceId}`;

// Determine if this is a new or existing connector
const hasOAuthCredentials =
metadata && (metadata.access_token || (metadata.provider && metadata.uid));
const isExistingConnector = Boolean(editItemId || hasOAuthCredentials);

// Initialize OAuth state from localStorage after keys are available
useEffect(() => {
if (!oAuthProvider?.length) {
Expand Down Expand Up @@ -132,6 +137,14 @@ function ConfigureDs({
}
}, [selectedSourceId, oAuthProvider, oauthStatusKey, oauthCacheKey]);

// Cleanup OAuth localStorage when component unmounts (modal close)
useEffect(() => {
return () => {
localStorage.removeItem(oauthCacheKey);
localStorage.removeItem(oauthStatusKey);
};
}, [oauthCacheKey, oauthStatusKey]);

Comment thread
kirtimanmishrazipstack marked this conversation as resolved.
const handleTestConnection = (updatedFormData) => {
// Check if there any error in form proceed to test connection only there is no error.
if (formRef && !formRef.current?.validateForm()) {
Expand Down Expand Up @@ -320,8 +333,10 @@ function ConfigureDs({
updateSession(type);
}

// Keep OAuth state after successful submission for potential re-use
// OAuth state will be cleared only when switching to different connectors
if (oAuthProvider?.length > 0) {
localStorage.removeItem(oauthCacheKey);
localStorage.removeItem(oauthStatusKey);
}

setOpen(false);
})
Expand Down Expand Up @@ -351,6 +366,7 @@ function ConfigureDs({
setCacheKey={handleSetCacheKey}
setStatus={handleSetStatus}
selectedSourceId={selectedSourceId}
isExistingConnector={isExistingConnector}
/>
)}
<RjsfFormLayout
Expand Down
15 changes: 10 additions & 5 deletions frontend/src/components/oauth-ds/google/GoogleOAuthButton.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { Typography } from "antd";
import PropTypes from "prop-types";
import { GoogleLoginButton } from "react-social-login-buttons";
import { useEffect, useState } from "react";
import { Typography } from "antd";
import { GoogleLoginButton } from "react-social-login-buttons";

import "./GoogleOAuthButton.css";

const GoogleOAuthButton = ({ handleOAuth, status }) => {
const GoogleOAuthButton = ({
handleOAuth,
status,
buttonText = "Authenticate with Google",
}) => {
const [text, setText] = useState("");
useEffect(() => {
if (status === "success") {
setText("Authenticated");
return;
}
setText("Sign in with Google");
}, [status]);
setText(buttonText);
}, [status, buttonText]);

return (
<div className="google-oauth-layout">
Expand All @@ -27,6 +31,7 @@ const GoogleOAuthButton = ({ handleOAuth, status }) => {
GoogleOAuthButton.propTypes = {
handleOAuth: PropTypes.func.isRequired,
status: PropTypes.string,
buttonText: PropTypes.string,
};

export default GoogleOAuthButton;
48 changes: 32 additions & 16 deletions frontend/src/components/oauth-ds/oauth-ds/OAuthDs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,38 @@ import { useEffect, useState } from "react";

import { O_AUTH_PROVIDERS, getBaseUrl } from "../../../helpers/GetStaticData";
import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate.js";
import { useExceptionHandler } from "../../../hooks/useExceptionHandler.jsx";
import { useAlertStore } from "../../../store/alert-store";
import GoogleOAuthButton from "../google/GoogleOAuthButton.jsx";
import { useExceptionHandler } from "../../../hooks/useExceptionHandler.jsx";
function OAuthDs({ oAuthProvider, setCacheKey, setStatus, selectedSourceId }) {
function OAuthDs({
oAuthProvider,
setCacheKey,
setStatus,
selectedSourceId,
isExistingConnector,
}) {
Comment thread
kirtimanmishrazipstack marked this conversation as resolved.
const axiosPrivate = useAxiosPrivate();
const { setAlertDetails } = useAlertStore();
const handleException = useExceptionHandler();

// Simple OAuth storage keys per connector
const oauthCacheKey = `oauth-cachekey-${selectedSourceId}`;
const oauthStatusKey = `oauth-status-${selectedSourceId}`;

// Determine button text based on connector state
const buttonText = isExistingConnector
? "Reauthenticate"
: "Authenticate with Google";

const [oauthStatus, setOAuthStatus] = useState(() => {
// Initialize from connector-specific status only to prevent contamination
return localStorage.getItem(`oauth-status-${selectedSourceId}`);
// Initialize from connector-specific status
return localStorage.getItem(oauthStatusKey);
});

useEffect(() => {
const connectorStatusKey = `oauth-status-${selectedSourceId}`;

const handleStorageChange = () => {
// Listen for changes to our specific connector's status only
const updatedOAuthStatus = localStorage.getItem(connectorStatusKey);
// Listen for changes to our specific connector only
const updatedOAuthStatus = localStorage.getItem(oauthStatusKey);
if (updatedOAuthStatus) {
Comment thread
kirtimanmishrazipstack marked this conversation as resolved.
setOAuthStatus(updatedOAuthStatus);
setStatus(updatedOAuthStatus);
Expand All @@ -39,22 +50,22 @@ function OAuthDs({ oAuthProvider, setCacheKey, setStatus, selectedSourceId }) {
setCacheKey(persistedCacheKey);
}

// Set initial status from connector-specific status only
const connectorSpecificStatus = localStorage.getItem(connectorStatusKey);
if (connectorSpecificStatus) {
setStatus(connectorSpecificStatus);
setOAuthStatus(connectorSpecificStatus);
// Set initial status from connector-specific status
const connectorStatus = localStorage.getItem(oauthStatusKey);
if (connectorStatus) {
setStatus(connectorStatus);
setOAuthStatus(connectorStatus);
}

return () => {
window.removeEventListener("storage", handleStorageChange);
// Don't clear localStorage on unmount to persist across tab switches
};
}, [selectedSourceId]);
}, [selectedSourceId, oauthCacheKey, oauthStatusKey, setCacheKey, setStatus]);

const handleOAuth = async () => {
try {
// Store connector ID in sessionStorage for OAuth callback (survives window.open)
// Store connector context in sessionStorage for OAuth callback (survives window.open)
sessionStorage.setItem("oauth-current-connector", selectedSourceId);

const requestOptions = {
Expand Down Expand Up @@ -88,7 +99,11 @@ function OAuthDs({ oAuthProvider, setCacheKey, setStatus, selectedSourceId }) {
if (O_AUTH_PROVIDERS["GOOGLE"] === oAuthProvider) {
return (
<>
<GoogleOAuthButton handleOAuth={handleOAuth} status={oauthStatus} />
<GoogleOAuthButton
handleOAuth={handleOAuth}
status={oauthStatus}
buttonText={buttonText}
/>
</>
);
}
Expand All @@ -101,6 +116,7 @@ OAuthDs.propTypes = {
setCacheKey: PropTypes.func,
setStatus: PropTypes.func,
selectedSourceId: PropTypes.string.isRequired,
isExistingConnector: PropTypes.bool,
};

export { OAuthDs };