mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
feat: prevent duplicate OAuth account connections
This commit is contained in:
parent
3ff87a218d
commit
4b3d427e90
13 changed files with 240 additions and 6 deletions
|
|
@ -23,7 +23,7 @@ from app.db import (
|
|||
from app.connectors.airtable_connector import fetch_airtable_user_email
|
||||
from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase
|
||||
from app.users import current_active_user
|
||||
from app.utils.connector_naming import generate_unique_connector_name
|
||||
from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -303,6 +303,22 @@ async def airtable_callback(
|
|||
credentials_dict = credentials.to_dict()
|
||||
credentials_dict["_token_encrypted"] = True
|
||||
|
||||
# Check for duplicate connector (same account already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.AIRTABLE_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
user_email,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Airtable connector detected for user {user_id} with email {user_email}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=airtable-connector"
|
||||
)
|
||||
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase
|
|||
from app.users import current_active_user
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
from app.utils.connector_naming import (
|
||||
check_duplicate_connector,
|
||||
extract_identifier_from_credentials,
|
||||
generate_unique_connector_name,
|
||||
)
|
||||
|
|
@ -296,6 +297,23 @@ async def confluence_callback(
|
|||
connector_identifier = extract_identifier_from_credentials(
|
||||
SearchSourceConnectorType.CONFLUENCE_CONNECTOR, connector_config
|
||||
)
|
||||
|
||||
# Check for duplicate connector (same Confluence instance already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
connector_identifier,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Confluence connector detected for user {user_id} with instance {connector_identifier}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=confluence-connector"
|
||||
)
|
||||
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from app.schemas.discord_auth_credentials import DiscordAuthCredentialsBase
|
|||
from app.users import current_active_user
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
from app.utils.connector_naming import (
|
||||
check_duplicate_connector,
|
||||
extract_identifier_from_credentials,
|
||||
generate_unique_connector_name,
|
||||
)
|
||||
|
|
@ -292,6 +293,23 @@ async def discord_callback(
|
|||
connector_identifier = extract_identifier_from_credentials(
|
||||
SearchSourceConnectorType.DISCORD_CONNECTOR, connector_config
|
||||
)
|
||||
|
||||
# Check for duplicate connector (same server already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.DISCORD_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
connector_identifier,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Discord connector detected for user {user_id} with server {connector_identifier}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=discord-connector"
|
||||
)
|
||||
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from app.db import (
|
|||
get_async_session,
|
||||
)
|
||||
from app.users import current_active_user
|
||||
from app.utils.connector_naming import generate_unique_connector_name
|
||||
from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -195,6 +195,22 @@ async def calendar_callback(
|
|||
# Mark that credentials are encrypted for backward compatibility
|
||||
creds_dict["_token_encrypted"] = True
|
||||
|
||||
# Check for duplicate connector (same account already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
user_email,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Google Calendar connector detected for user {user_id} with email {user_email}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-calendar-connector"
|
||||
)
|
||||
|
||||
try:
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ from app.db import (
|
|||
get_async_session,
|
||||
)
|
||||
from app.users import current_active_user
|
||||
from app.utils.connector_naming import generate_unique_connector_name
|
||||
from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
|
||||
# Relax token scope validation for Google OAuth
|
||||
|
|
@ -250,6 +250,22 @@ async def drive_callback(
|
|||
# Mark that credentials are encrypted for backward compatibility
|
||||
creds_dict["_token_encrypted"] = True
|
||||
|
||||
# Check for duplicate connector (same account already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
user_email,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Google Drive connector detected for user {user_id} with email {user_email}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-drive-connector"
|
||||
)
|
||||
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from app.db import (
|
|||
get_async_session,
|
||||
)
|
||||
from app.users import current_active_user
|
||||
from app.utils.connector_naming import generate_unique_connector_name
|
||||
from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -226,6 +226,22 @@ async def gmail_callback(
|
|||
# Mark that credentials are encrypted for backward compatibility
|
||||
creds_dict["_token_encrypted"] = True
|
||||
|
||||
# Check for duplicate connector (same account already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
user_email,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Gmail connector detected for user {user_id} with email {user_email}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-gmail-connector"
|
||||
)
|
||||
|
||||
try:
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase
|
|||
from app.users import current_active_user
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
from app.utils.connector_naming import (
|
||||
check_duplicate_connector,
|
||||
extract_identifier_from_credentials,
|
||||
generate_unique_connector_name,
|
||||
)
|
||||
|
|
@ -314,6 +315,23 @@ async def jira_callback(
|
|||
connector_identifier = extract_identifier_from_credentials(
|
||||
SearchSourceConnectorType.JIRA_CONNECTOR, connector_config
|
||||
)
|
||||
|
||||
# Check for duplicate connector (same Jira instance already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
connector_identifier,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Jira connector detected for user {user_id} with instance {connector_identifier}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=jira-connector"
|
||||
)
|
||||
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from app.db import (
|
|||
from app.connectors.linear_oauth import fetch_linear_organization_name
|
||||
from app.schemas.linear_auth_credentials import LinearAuthCredentialsBase
|
||||
from app.users import current_active_user
|
||||
from app.utils.connector_naming import generate_unique_connector_name
|
||||
from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -265,6 +265,22 @@ async def linear_callback(
|
|||
"_token_encrypted": True,
|
||||
}
|
||||
|
||||
# Check for duplicate connector (same organization already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.LINEAR_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
org_name,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Linear connector detected for user {user_id} with org {org_name}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=linear-connector"
|
||||
)
|
||||
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from app.schemas.notion_auth_credentials import NotionAuthCredentialsBase
|
|||
from app.users import current_active_user
|
||||
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
||||
from app.utils.connector_naming import (
|
||||
check_duplicate_connector,
|
||||
extract_identifier_from_credentials,
|
||||
generate_unique_connector_name,
|
||||
)
|
||||
|
|
@ -270,6 +271,23 @@ async def notion_callback(
|
|||
connector_identifier = extract_identifier_from_credentials(
|
||||
SearchSourceConnectorType.NOTION_CONNECTOR, connector_config
|
||||
)
|
||||
|
||||
# Check for duplicate connector (same workspace already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.NOTION_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
connector_identifier,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Notion connector detected for user {user_id} with workspace {connector_identifier}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=notion-connector"
|
||||
)
|
||||
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from app.db import (
|
|||
from app.schemas.slack_auth_credentials import SlackAuthCredentialsBase
|
||||
from app.users import current_active_user
|
||||
from app.utils.connector_naming import (
|
||||
check_duplicate_connector,
|
||||
extract_identifier_from_credentials,
|
||||
generate_unique_connector_name,
|
||||
)
|
||||
|
|
@ -280,6 +281,23 @@ async def slack_callback(
|
|||
connector_identifier = extract_identifier_from_credentials(
|
||||
SearchSourceConnectorType.SLACK_CONNECTOR, connector_config
|
||||
)
|
||||
|
||||
# Check for duplicate connector (same workspace already connected)
|
||||
is_duplicate = await check_duplicate_connector(
|
||||
session,
|
||||
SearchSourceConnectorType.SLACK_CONNECTOR,
|
||||
space_id,
|
||||
user_id,
|
||||
connector_identifier,
|
||||
)
|
||||
if is_duplicate:
|
||||
logger.warning(
|
||||
f"Duplicate Slack connector detected for user {user_id} with workspace {connector_identifier}"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=slack-connector"
|
||||
)
|
||||
|
||||
# Generate a unique, user-friendly connector name
|
||||
connector_name = await generate_unique_connector_name(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -114,6 +114,41 @@ async def count_connectors_of_type(
|
|||
return result.scalar() or 0
|
||||
|
||||
|
||||
async def check_duplicate_connector(
|
||||
session: AsyncSession,
|
||||
connector_type: SearchSourceConnectorType,
|
||||
search_space_id: int,
|
||||
user_id: UUID,
|
||||
identifier: str | None,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a connector with the same identifier already exists.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
connector_type: The type of connector
|
||||
search_space_id: The search space ID
|
||||
user_id: The user ID
|
||||
identifier: User identifier (email, workspace name, etc.)
|
||||
|
||||
Returns:
|
||||
True if a duplicate exists, False otherwise
|
||||
"""
|
||||
if not identifier:
|
||||
return False
|
||||
|
||||
expected_name = f"{get_base_name_for_type(connector_type)} - {identifier}"
|
||||
result = await session.execute(
|
||||
select(func.count(SearchSourceConnector.id)).where(
|
||||
SearchSourceConnector.connector_type == connector_type,
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.name == expected_name,
|
||||
)
|
||||
)
|
||||
return (result.scalar() or 0) > 0
|
||||
|
||||
|
||||
async def generate_unique_connector_name(
|
||||
session: AsyncSession,
|
||||
connector_type: SearchSourceConnectorType,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const connectorPopupQueryParamsSchema = z.object({
|
|||
connectorId: z.string().optional(),
|
||||
connectorType: z.string().optional(),
|
||||
success: z.enum(["true", "false"]).optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ConnectorPopupQueryParams = z.infer<typeof connectorPopupQueryParamsSchema>;
|
||||
|
|
|
|||
|
|
@ -245,11 +245,39 @@ export const useConnectorDialog = () => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, allConnectors, editingConnector, indexingConfig, connectingConnectorType, viewingAccountsType]);
|
||||
|
||||
// Detect OAuth success and transition to config view
|
||||
// Detect OAuth success / Failure and transition to config view
|
||||
useEffect(() => {
|
||||
try {
|
||||
const params = parseConnectorPopupQueryParams(searchParams);
|
||||
|
||||
// Handle OAuth errors (e.g., duplicate account)
|
||||
if (params.error && params.modal === "connectors") {
|
||||
const oauthConnector = params.connector
|
||||
? OAUTH_CONNECTORS.find((c) => c.id === params.connector)
|
||||
: null;
|
||||
const connectorName = oauthConnector?.title || "connector";
|
||||
|
||||
if (params.error === "duplicate_account") {
|
||||
toast.error(`This ${connectorName} account is already connected`, {
|
||||
description: "Please use a different account or manage the existing connection.",
|
||||
});
|
||||
} else {
|
||||
toast.error(`Failed to connect ${connectorName}`, {
|
||||
description: params.error.replace(/_/g, " "),
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up error params from URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("error");
|
||||
url.searchParams.delete("connector");
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
|
||||
// Open the popup to show the connectors
|
||||
setIsOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
params.success === "true" &&
|
||||
params.connector &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue