diff --git a/surfsense_backend/app/connectors/linear_connector.py b/surfsense_backend/app/connectors/linear_connector.py index 61980e7ba..b8206a40d 100644 --- a/surfsense_backend/app/connectors/linear_connector.py +++ b/surfsense_backend/app/connectors/linear_connector.py @@ -16,7 +16,6 @@ from sqlalchemy.future import select from app.config import config from app.db import SearchSourceConnector -from app.routes.linear_add_connector_route import refresh_linear_token from app.schemas.linear_auth_credentials import LinearAuthCredentialsBase from app.utils.oauth_security import TokenEncryption @@ -169,6 +168,9 @@ class LinearConnector: f"Connector {self._connector_id} not found; cannot refresh token." ) + # Lazy import to avoid circular dependency + from app.routes.linear_add_connector_route import refresh_linear_token + # Refresh token connector = await refresh_linear_token(self._session, connector) @@ -640,4 +642,3 @@ class LinearConnector: return dt.strftime("%Y-%m-%d %H:%M:%S") except ValueError: return iso_date - diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index 92fcbc67e..5fa8180bf 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -277,7 +277,6 @@ async def airtable_callback( status_code=400, detail="No access token received from Airtable" ) - # Fetch user email before encrypting credentials user_email = await fetch_airtable_user_email(access_token) diff --git a/surfsense_backend/app/routes/google_calendar_add_connector_route.py b/surfsense_backend/app/routes/google_calendar_add_connector_route.py index a721b62c1..a1292fa43 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -174,7 +174,7 @@ async def calendar_callback( creds = flow.credentials creds_dict = json.loads(creds.to_json()) - # Fetch user email before encrypting credentials + # Fetch user email user_email = fetch_google_user_email(creds) # Encrypt sensitive credentials before storing diff --git a/surfsense_backend/app/routes/google_drive_add_connector_route.py b/surfsense_backend/app/routes/google_drive_add_connector_route.py index 1b02543d3..30a46a618 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -229,7 +229,7 @@ async def drive_callback( creds = flow.credentials creds_dict = json.loads(creds.to_json()) - # Fetch user email before encrypting credentials + # Fetch user email user_email = fetch_google_user_email(creds) # Encrypt sensitive credentials before storing diff --git a/surfsense_backend/app/routes/google_gmail_add_connector_route.py b/surfsense_backend/app/routes/google_gmail_add_connector_route.py index 4a7631919..9919894f3 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -205,7 +205,7 @@ async def gmail_callback( creds = flow.credentials creds_dict = json.loads(creds.to_json()) - # Fetch user email before encrypting credentials + # Fetch user email user_email = fetch_google_user_email(creds) # Encrypt sensitive credentials before storing diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index db79afdcb..ce5cdbfb3 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -242,7 +242,7 @@ async def linear_callback( status_code=400, detail="No access token received from Linear" ) - # Fetch organization name before encrypting credentials + # Fetch organization name org_name = await fetch_linear_organization_name(access_token) # Calculate expiration time (UTC, tz-aware) diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index a92be5f6e..58a50a6f8 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -7,7 +7,8 @@ PUT /search-source-connectors/{connector_id} - Update a specific connector DELETE /search-source-connectors/{connector_id} - Delete a specific connector POST /search-source-connectors/{connector_id}/index - Index content from a connector to a search space -Note: Each search space can have multiple connectors of the same type per user (uniqueness is no longer enforced, you may connect several accounts of the same type). +Note: OAuth connectors (Gmail, Drive, Slack, etc.) support multiple accounts per search space. +Non-OAuth connectors (BookStack, GitHub, etc.) are limited to one per search space. """ import logging @@ -111,7 +112,7 @@ async def create_search_source_connector( Create a new search source connector. Requires CONNECTORS_CREATE permission. - Each search space can have multiple connectors of the same type (e.g., multiple Gmail, Slack, etc. accounts). + Each search space can have only one connector of each type (based on search_space_id and connector_type). The config must contain the appropriate keys for the connector type. """ try: @@ -124,6 +125,21 @@ async def create_search_source_connector( "You don't have permission to create connectors in this search space", ) + # Check if a connector with the same type already exists for this search space + # (for non-OAuth connectors that don't support multiple accounts) + result = await session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.connector_type == connector.connector_type, + ) + ) + existing_connector = result.scalars().first() + if existing_connector: + raise HTTPException( + status_code=409, + detail=f"A connector with type {connector.connector_type} already exists in this search space.", + ) + # Prepare connector data connector_data = connector.model_dump() @@ -169,7 +185,7 @@ async def create_search_source_connector( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: {e!s}", + detail=f"Integrity error: A connector with this type already exists in this search space. {e!s}", ) from e except HTTPException: await session.rollback()