From d03b8dae34088a931542b50058ad116c34dc43f2 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 08:16:04 +0200 Subject: [PATCH] feat: add Linear org name fetch and update route --- .../app/connectors/linear_connector.py | 1 + .../app/connectors/linear_oauth.py | 60 +++++++++++++++++++ .../app/routes/linear_add_connector_route.py | 20 ++++--- 3 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 surfsense_backend/app/connectors/linear_oauth.py diff --git a/surfsense_backend/app/connectors/linear_connector.py b/surfsense_backend/app/connectors/linear_connector.py index 148aa4d0a..404f60e66 100644 --- a/surfsense_backend/app/connectors/linear_connector.py +++ b/surfsense_backend/app/connectors/linear_connector.py @@ -592,3 +592,4 @@ class LinearConnector: return dt.strftime("%Y-%m-%d %H:%M:%S") except ValueError: return iso_date + diff --git a/surfsense_backend/app/connectors/linear_oauth.py b/surfsense_backend/app/connectors/linear_oauth.py new file mode 100644 index 000000000..96336fe94 --- /dev/null +++ b/surfsense_backend/app/connectors/linear_oauth.py @@ -0,0 +1,60 @@ +""" +Linear OAuth Utilities. + +Provides functions for fetching user/organization info from Linear API. +Separated from linear_connector.py to avoid circular imports. +""" + +import logging + +import httpx + +logger = logging.getLogger(__name__) + +LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql" + +ORGANIZATION_QUERY = """ +query { + organization { + name + } +} +""" + + +async def fetch_linear_organization_name(access_token: str) -> str | None: + """ + Fetch organization/workspace name from Linear GraphQL API. + + Args: + access_token: The Linear OAuth access token + + Returns: + Organization name or None if fetch fails + """ + try: + async with httpx.AsyncClient() as client: + response = await client.post( + LINEAR_GRAPHQL_URL, + headers={ + "Authorization": access_token, + "Content-Type": "application/json", + }, + json={"query": ORGANIZATION_QUERY}, + timeout=10.0, + ) + + if response.status_code == 200: + data = response.json() + org_name = data.get("data", {}).get("organization", {}).get("name") + if org_name: + logger.debug(f"Fetched Linear organization name: {org_name}") + return org_name + + logger.warning(f"Failed to fetch Linear org info: {response.status_code}") + return None + + except Exception as e: + logger.warning(f"Error fetching Linear organization name: {e!s}") + return None + diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index e13c2dc5f..73bf500a3 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -23,10 +23,11 @@ from app.db import ( User, get_async_session, ) +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.oauth_security import OAuthStateManager, TokenEncryption -from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -241,6 +242,9 @@ async def linear_callback( status_code=400, detail="No access token received from Linear" ) + # Fetch organization name before encrypting credentials + org_name = await fetch_linear_organization_name(access_token) + # Calculate expiration time (UTC, tz-aware) expires_at = None if token_json.get("expires_in"): @@ -261,13 +265,13 @@ async def linear_callback( "_token_encrypted": True, } - # Extract unique identifier from connector credentials - connector_identifier = extract_identifier_from_credentials( - SearchSourceConnectorType.LINEAR_CONNECTOR, connector_config - ) - # Generate a unique, user-friendly connector name from credentials/account info - connector_name = generate_unique_connector_name( - SearchSourceConnectorType.LINEAR_CONNECTOR, connector_identifier + # Generate a unique, user-friendly connector name + connector_name = await generate_unique_connector_name( + session, + SearchSourceConnectorType.LINEAR_CONNECTOR, + space_id, + user_id, + org_name, ) # Create new connector new_connector = SearchSourceConnector(