chore: ran both frontend and backend linting

This commit is contained in:
Anish Sarkar 2026-01-03 00:18:17 +05:30
parent 45489423d1
commit 645e849d93
21 changed files with 148 additions and 155 deletions

View file

@ -109,14 +109,14 @@ class GoogleCalendarConnector:
raise RuntimeError( raise RuntimeError(
"GOOGLE_CALENDAR_CONNECTOR connector not found; cannot persist refreshed token." "GOOGLE_CALENDAR_CONNECTOR connector not found; cannot persist refreshed token."
) )
# Encrypt sensitive credentials before storing # Encrypt sensitive credentials before storing
from app.config import config from app.config import config
from app.utils.oauth_security import TokenEncryption from app.utils.oauth_security import TokenEncryption
creds_dict = json.loads(self._credentials.to_json()) creds_dict = json.loads(self._credentials.to_json())
token_encrypted = connector.config.get("_token_encrypted", False) token_encrypted = connector.config.get("_token_encrypted", False)
if token_encrypted and config.SECRET_KEY: if token_encrypted and config.SECRET_KEY:
token_encryption = TokenEncryption(config.SECRET_KEY) token_encryption = TokenEncryption(config.SECRET_KEY)
# Encrypt sensitive fields # Encrypt sensitive fields
@ -125,15 +125,19 @@ class GoogleCalendarConnector:
creds_dict["token"] creds_dict["token"]
) )
if creds_dict.get("refresh_token"): if creds_dict.get("refresh_token"):
creds_dict["refresh_token"] = token_encryption.encrypt_token( creds_dict["refresh_token"] = (
creds_dict["refresh_token"] token_encryption.encrypt_token(
creds_dict["refresh_token"]
)
) )
if creds_dict.get("client_secret"): if creds_dict.get("client_secret"):
creds_dict["client_secret"] = token_encryption.encrypt_token( creds_dict["client_secret"] = (
creds_dict["client_secret"] token_encryption.encrypt_token(
creds_dict["client_secret"]
)
) )
creds_dict["_token_encrypted"] = True creds_dict["_token_encrypted"] = True
connector.config = creds_dict connector.config = creds_dict
flag_modified(connector, "config") flag_modified(connector, "config")
await self._session.commit() await self._session.commit()
@ -209,9 +213,15 @@ class GoogleCalendarConnector:
try: try:
# Validate date strings # Validate date strings
if not start_date or start_date.lower() in ("undefined", "null", "none"): if not start_date or start_date.lower() in ("undefined", "null", "none"):
return [], "Invalid start_date: must be a valid date string in YYYY-MM-DD format" return (
[],
"Invalid start_date: must be a valid date string in YYYY-MM-DD format",
)
if not end_date or end_date.lower() in ("undefined", "null", "none"): if not end_date or end_date.lower() in ("undefined", "null", "none"):
return [], "Invalid end_date: must be a valid date string in YYYY-MM-DD format" return (
[],
"Invalid end_date: must be a valid date string in YYYY-MM-DD format",
)
service = await self._get_service() service = await self._get_service()

View file

@ -43,14 +43,16 @@ async def get_valid_credentials(
if not connector: if not connector:
raise ValueError(f"Connector {connector_id} not found") raise ValueError(f"Connector {connector_id} not found")
config_data = connector.config.copy() # Work with a copy to avoid modifying original config_data = (
connector.config.copy()
) # Work with a copy to avoid modifying original
# Decrypt credentials if they are encrypted # Decrypt credentials if they are encrypted
token_encrypted = config_data.get("_token_encrypted", False) token_encrypted = config_data.get("_token_encrypted", False)
if token_encrypted and config.SECRET_KEY: if token_encrypted and config.SECRET_KEY:
try: try:
token_encryption = TokenEncryption(config.SECRET_KEY) token_encryption = TokenEncryption(config.SECRET_KEY)
# Decrypt sensitive fields # Decrypt sensitive fields
if config_data.get("token"): if config_data.get("token"):
config_data["token"] = token_encryption.decrypt_token( config_data["token"] = token_encryption.decrypt_token(
@ -64,7 +66,7 @@ async def get_valid_credentials(
config_data["client_secret"] = token_encryption.decrypt_token( config_data["client_secret"] = token_encryption.decrypt_token(
config_data["client_secret"] config_data["client_secret"]
) )
logger.info( logger.info(
f"Decrypted Google Drive credentials for connector {connector_id}" f"Decrypted Google Drive credentials for connector {connector_id}"
) )
@ -104,10 +106,10 @@ async def get_valid_credentials(
credentials.refresh(Request()) credentials.refresh(Request())
creds_dict = json.loads(credentials.to_json()) creds_dict = json.loads(credentials.to_json())
# Encrypt sensitive credentials before storing # Encrypt sensitive credentials before storing
token_encrypted = connector.config.get("_token_encrypted", False) token_encrypted = connector.config.get("_token_encrypted", False)
if token_encrypted and config.SECRET_KEY: if token_encrypted and config.SECRET_KEY:
token_encryption = TokenEncryption(config.SECRET_KEY) token_encryption = TokenEncryption(config.SECRET_KEY)
# Encrypt sensitive fields # Encrypt sensitive fields
@ -124,7 +126,7 @@ async def get_valid_credentials(
creds_dict["client_secret"] creds_dict["client_secret"]
) )
creds_dict["_token_encrypted"] = True creds_dict["_token_encrypted"] = True
connector.config = creds_dict connector.config = creds_dict
flag_modified(connector, "config") flag_modified(connector, "config")
await session.commit() await session.commit()

View file

@ -44,9 +44,14 @@ class LinearConnector:
ValueError: If no Linear access token has been set ValueError: If no Linear access token has been set
""" """
if not self.access_token: if not self.access_token:
raise ValueError("Linear access token not initialized. Call set_token() first.") raise ValueError(
"Linear access token not initialized. Call set_token() first."
)
return {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.access_token}",
}
def execute_graphql_query( def execute_graphql_query(
self, query: str, variables: dict[str, Any] | None = None self, query: str, variables: dict[str, Any] | None = None
@ -66,7 +71,9 @@ class LinearConnector:
Exception: If the API request fails Exception: If the API request fails
""" """
if not self.access_token: if not self.access_token:
raise ValueError("Linear access token not initialized. Call set_token() first.") raise ValueError(
"Linear access token not initialized. Call set_token() first."
)
headers = self.get_headers() headers = self.get_headers()
payload = {"query": query} payload = {"query": query}
@ -174,9 +181,15 @@ class LinearConnector:
""" """
# Validate date strings # Validate date strings
if not start_date or start_date.lower() in ("undefined", "null", "none"): if not start_date or start_date.lower() in ("undefined", "null", "none"):
return [], "Invalid start_date: must be a valid date string in YYYY-MM-DD format" return (
[],
"Invalid start_date: must be a valid date string in YYYY-MM-DD format",
)
if not end_date or end_date.lower() in ("undefined", "null", "none"): if not end_date or end_date.lower() in ("undefined", "null", "none"):
return [], "Invalid end_date: must be a valid date string in YYYY-MM-DD format" return (
[],
"Invalid end_date: must be a valid date string in YYYY-MM-DD format",
)
# Convert date strings to ISO format # Convert date strings to ISO format
try: try:

View file

@ -15,13 +15,13 @@ from .google_drive_add_connector_route import (
from .google_gmail_add_connector_route import ( from .google_gmail_add_connector_route import (
router as google_gmail_add_connector_router, router as google_gmail_add_connector_router,
) )
from .linear_add_connector_route import router as linear_add_connector_router
from .logs_routes import router as logs_router from .logs_routes import router as logs_router
from .luma_add_connector_route import router as luma_add_connector_router from .luma_add_connector_route import router as luma_add_connector_router
from .new_chat_routes import router as new_chat_router from .new_chat_routes import router as new_chat_router
from .linear_add_connector_route import router as linear_add_connector_router
from .notion_add_connector_route import router as notion_add_connector_router
from .new_llm_config_routes import router as new_llm_config_router from .new_llm_config_routes import router as new_llm_config_router
from .notes_routes import router as notes_router from .notes_routes import router as notes_router
from .notion_add_connector_route import router as notion_add_connector_router
from .podcasts_routes import router as podcasts_router from .podcasts_routes import router as podcasts_router
from .rbac_routes import router as rbac_router from .rbac_routes import router as rbac_router
from .search_source_connectors_routes import router as search_source_connectors_router from .search_source_connectors_routes import router as search_source_connectors_router

View file

@ -191,7 +191,7 @@ async def airtable_callback(
except Exception: except Exception:
# If state is invalid, we'll redirect without space_id # If state is invalid, we'll redirect without space_id
logger.warning("Failed to validate state in error handler") logger.warning("Failed to validate state in error handler")
# Redirect to frontend with error parameter # Redirect to frontend with error parameter
if space_id: if space_id:
return RedirectResponse( return RedirectResponse(
@ -201,16 +201,12 @@ async def airtable_callback(
return RedirectResponse( return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=airtable_oauth_denied" url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=airtable_oauth_denied"
) )
# Validate required parameters for successful flow # Validate required parameters for successful flow
if not code: if not code:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing authorization code")
status_code=400, detail="Missing authorization code"
)
if not state: if not state:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing state parameter")
status_code=400, detail="Missing state parameter"
)
# Validate and decode state with signature verification # Validate and decode state with signature verification
state_manager = get_state_manager() state_manager = get_state_manager()
@ -226,7 +222,7 @@ async def airtable_callback(
user_id = UUID(data["user_id"]) user_id = UUID(data["user_id"])
space_id = data["space_id"] space_id = data["space_id"]
code_verifier = data.get("code_verifier") code_verifier = data.get("code_verifier")
if not code_verifier: if not code_verifier:
raise HTTPException( raise HTTPException(
status_code=400, detail="Missing code_verifier in state parameter" status_code=400, detail="Missing code_verifier in state parameter"
@ -273,7 +269,7 @@ async def airtable_callback(
token_encryption = get_token_encryption() token_encryption = get_token_encryption()
access_token = token_json.get("access_token") access_token = token_json.get("access_token")
refresh_token = token_json.get("refresh_token") refresh_token = token_json.get("refresh_token")
if not access_token: if not access_token:
raise HTTPException( raise HTTPException(
status_code=400, detail="No access token received from Airtable" status_code=400, detail="No access token received from Airtable"
@ -296,7 +292,7 @@ async def airtable_callback(
expires_at=expires_at, expires_at=expires_at,
scope=token_json.get("scope"), scope=token_json.get("scope"),
) )
# Mark that tokens are encrypted for backward compatibility # Mark that tokens are encrypted for backward compatibility
credentials_dict = credentials.to_dict() credentials_dict = credentials.to_dict()
credentials_dict["_token_encrypted"] = True credentials_dict["_token_encrypted"] = True
@ -390,11 +386,11 @@ async def refresh_airtable_token(
logger.info(f"Refreshing Airtable token for connector {connector.id}") logger.info(f"Refreshing Airtable token for connector {connector.id}")
credentials = AirtableAuthCredentialsBase.from_dict(connector.config) credentials = AirtableAuthCredentialsBase.from_dict(connector.config)
# Decrypt tokens if they are encrypted # Decrypt tokens if they are encrypted
token_encryption = get_token_encryption() token_encryption = get_token_encryption()
is_encrypted = connector.config.get("_token_encrypted", False) is_encrypted = connector.config.get("_token_encrypted", False)
refresh_token = credentials.refresh_token refresh_token = credentials.refresh_token
if is_encrypted and refresh_token: if is_encrypted and refresh_token:
try: try:
@ -404,7 +400,7 @@ async def refresh_airtable_token(
raise HTTPException( raise HTTPException(
status_code=500, detail="Failed to decrypt stored refresh token" status_code=500, detail="Failed to decrypt stored refresh token"
) from e ) from e
auth_header = make_basic_auth_header( auth_header = make_basic_auth_header(
config.AIRTABLE_CLIENT_ID, config.AIRTABLE_CLIENT_SECRET config.AIRTABLE_CLIENT_ID, config.AIRTABLE_CLIENT_SECRET
) )
@ -444,7 +440,7 @@ async def refresh_airtable_token(
# Encrypt new tokens before storing # Encrypt new tokens before storing
access_token = token_json.get("access_token") access_token = token_json.get("access_token")
new_refresh_token = token_json.get("refresh_token") new_refresh_token = token_json.get("refresh_token")
if not access_token: if not access_token:
raise HTTPException( raise HTTPException(
status_code=400, detail="No access token received from Airtable refresh" status_code=400, detail="No access token received from Airtable refresh"
@ -453,7 +449,9 @@ async def refresh_airtable_token(
# Update credentials object with encrypted tokens # Update credentials object with encrypted tokens
credentials.access_token = token_encryption.encrypt_token(access_token) credentials.access_token = token_encryption.encrypt_token(access_token)
if new_refresh_token: if new_refresh_token:
credentials.refresh_token = token_encryption.encrypt_token(new_refresh_token) credentials.refresh_token = token_encryption.encrypt_token(
new_refresh_token
)
credentials.expires_in = token_json.get("expires_in") credentials.expires_in = token_json.get("expires_in")
credentials.expires_at = expires_at credentials.expires_at = expires_at
credentials.scope = token_json.get("scope") credentials.scope = token_json.get("scope")

View file

@ -142,13 +142,9 @@ async def calendar_callback(
# Validate required parameters for successful flow # Validate required parameters for successful flow
if not code: if not code:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing authorization code")
status_code=400, detail="Missing authorization code"
)
if not state: if not state:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing state parameter")
status_code=400, detail="Missing state parameter"
)
# Validate and decode state with signature verification # Validate and decode state with signature verification
state_manager = get_state_manager() state_manager = get_state_manager()
@ -178,7 +174,7 @@ async def calendar_callback(
# Encrypt sensitive credentials before storing # Encrypt sensitive credentials before storing
token_encryption = get_token_encryption() token_encryption = get_token_encryption()
# Encrypt sensitive fields: token, refresh_token, client_secret # Encrypt sensitive fields: token, refresh_token, client_secret
if creds_dict.get("token"): if creds_dict.get("token"):
creds_dict["token"] = token_encryption.encrypt_token(creds_dict["token"]) creds_dict["token"] = token_encryption.encrypt_token(creds_dict["token"])
@ -190,7 +186,7 @@ async def calendar_callback(
creds_dict["client_secret"] = token_encryption.encrypt_token( creds_dict["client_secret"] = token_encryption.encrypt_token(
creds_dict["client_secret"] creds_dict["client_secret"]
) )
# Mark that credentials are encrypted for backward compatibility # Mark that credentials are encrypted for backward compatibility
creds_dict["_token_encrypted"] = True creds_dict["_token_encrypted"] = True

View file

@ -68,6 +68,7 @@ def get_token_encryption() -> TokenEncryption:
_token_encryption = TokenEncryption(config.SECRET_KEY) _token_encryption = TokenEncryption(config.SECRET_KEY)
return _token_encryption return _token_encryption
# Google Drive OAuth scopes # Google Drive OAuth scopes
SCOPES = [ SCOPES = [
"https://www.googleapis.com/auth/drive.readonly", # Read-only access to Drive "https://www.googleapis.com/auth/drive.readonly", # Read-only access to Drive
@ -191,13 +192,9 @@ async def drive_callback(
# Validate required parameters for successful flow # Validate required parameters for successful flow
if not code: if not code:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing authorization code")
status_code=400, detail="Missing authorization code"
)
if not state: if not state:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing state parameter")
status_code=400, detail="Missing state parameter"
)
# Validate and decode state with signature verification # Validate and decode state with signature verification
state_manager = get_state_manager() state_manager = get_state_manager()
@ -232,7 +229,7 @@ async def drive_callback(
# Encrypt sensitive credentials before storing # Encrypt sensitive credentials before storing
token_encryption = get_token_encryption() token_encryption = get_token_encryption()
# Encrypt sensitive fields: token, refresh_token, client_secret # Encrypt sensitive fields: token, refresh_token, client_secret
if creds_dict.get("token"): if creds_dict.get("token"):
creds_dict["token"] = token_encryption.encrypt_token(creds_dict["token"]) creds_dict["token"] = token_encryption.encrypt_token(creds_dict["token"])
@ -244,7 +241,7 @@ async def drive_callback(
creds_dict["client_secret"] = token_encryption.encrypt_token( creds_dict["client_secret"] = token_encryption.encrypt_token(
creds_dict["client_secret"] creds_dict["client_secret"]
) )
# Mark that credentials are encrypted for backward compatibility # Mark that credentials are encrypted for backward compatibility
creds_dict["_token_encrypted"] = True creds_dict["_token_encrypted"] = True

View file

@ -173,13 +173,9 @@ async def gmail_callback(
# Validate required parameters for successful flow # Validate required parameters for successful flow
if not code: if not code:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing authorization code")
status_code=400, detail="Missing authorization code"
)
if not state: if not state:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing state parameter")
status_code=400, detail="Missing state parameter"
)
# Validate and decode state with signature verification # Validate and decode state with signature verification
state_manager = get_state_manager() state_manager = get_state_manager()
@ -209,7 +205,7 @@ async def gmail_callback(
# Encrypt sensitive credentials before storing # Encrypt sensitive credentials before storing
token_encryption = get_token_encryption() token_encryption = get_token_encryption()
# Encrypt sensitive fields: token, refresh_token, client_secret # Encrypt sensitive fields: token, refresh_token, client_secret
if creds_dict.get("token"): if creds_dict.get("token"):
creds_dict["token"] = token_encryption.encrypt_token(creds_dict["token"]) creds_dict["token"] = token_encryption.encrypt_token(creds_dict["token"])
@ -221,7 +217,7 @@ async def gmail_callback(
creds_dict["client_secret"] = token_encryption.encrypt_token( creds_dict["client_secret"] = token_encryption.encrypt_token(
creds_dict["client_secret"] creds_dict["client_secret"]
) )
# Mark that credentials are encrypted for backward compatibility # Mark that credentials are encrypted for backward compatibility
creds_dict["_token_encrypted"] = True creds_dict["_token_encrypted"] = True

View file

@ -4,7 +4,6 @@ Linear Connector OAuth Routes.
Handles OAuth 2.0 authentication flow for Linear connector. Handles OAuth 2.0 authentication flow for Linear connector.
""" """
import json
import logging import logging
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from uuid import UUID from uuid import UUID
@ -89,9 +88,7 @@ async def connect_linear(space_id: int, user: User = Depends(current_active_user
raise HTTPException(status_code=400, detail="space_id is required") raise HTTPException(status_code=400, detail="space_id is required")
if not config.LINEAR_CLIENT_ID: if not config.LINEAR_CLIENT_ID:
raise HTTPException( raise HTTPException(status_code=500, detail="Linear OAuth not configured.")
status_code=500, detail="Linear OAuth not configured."
)
if not config.SECRET_KEY: if not config.SECRET_KEY:
raise HTTPException( raise HTTPException(
@ -115,9 +112,7 @@ async def connect_linear(space_id: int, user: User = Depends(current_active_user
auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}"
logger.info( logger.info(f"Generated Linear OAuth URL for user {user.id}, space {space_id}")
f"Generated Linear OAuth URL for user {user.id}, space {space_id}"
)
return {"auth_url": auth_url} return {"auth_url": auth_url}
except Exception as e: except Exception as e:
@ -162,7 +157,7 @@ async def linear_callback(
except Exception: except Exception:
# If state is invalid, we'll redirect without space_id # If state is invalid, we'll redirect without space_id
logger.warning("Failed to validate state in error handler") logger.warning("Failed to validate state in error handler")
# Redirect to frontend with error parameter # Redirect to frontend with error parameter
if space_id: if space_id:
return RedirectResponse( return RedirectResponse(
@ -172,16 +167,12 @@ async def linear_callback(
return RedirectResponse( return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=linear_oauth_denied" url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=linear_oauth_denied"
) )
# Validate required parameters for successful flow # Validate required parameters for successful flow
if not code: if not code:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing authorization code")
status_code=400, detail="Missing authorization code"
)
if not state: if not state:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing state parameter")
status_code=400, detail="Missing state parameter"
)
# Validate and decode state with signature verification # Validate and decode state with signature verification
state_manager = get_state_manager() state_manager = get_state_manager()
@ -242,7 +233,7 @@ async def linear_callback(
token_encryption = get_token_encryption() token_encryption = get_token_encryption()
access_token = token_json.get("access_token") access_token = token_json.get("access_token")
refresh_token = token_json.get("refresh_token") refresh_token = token_json.get("refresh_token")
if not access_token: if not access_token:
raise HTTPException( raise HTTPException(
status_code=400, detail="No access token received from Linear" status_code=400, detail="No access token received from Linear"
@ -337,4 +328,3 @@ async def linear_callback(
raise HTTPException( raise HTTPException(
status_code=500, detail=f"Failed to complete Linear OAuth: {e!s}" status_code=500, detail=f"Failed to complete Linear OAuth: {e!s}"
) from e ) from e

View file

@ -4,7 +4,6 @@ Notion Connector OAuth Routes.
Handles OAuth 2.0 authentication flow for Notion connector. Handles OAuth 2.0 authentication flow for Notion connector.
""" """
import json
import logging import logging
from uuid import UUID from uuid import UUID
@ -85,9 +84,7 @@ async def connect_notion(space_id: int, user: User = Depends(current_active_user
raise HTTPException(status_code=400, detail="space_id is required") raise HTTPException(status_code=400, detail="space_id is required")
if not config.NOTION_CLIENT_ID: if not config.NOTION_CLIENT_ID:
raise HTTPException( raise HTTPException(status_code=500, detail="Notion OAuth not configured.")
status_code=500, detail="Notion OAuth not configured."
)
if not config.SECRET_KEY: if not config.SECRET_KEY:
raise HTTPException( raise HTTPException(
@ -111,9 +108,7 @@ async def connect_notion(space_id: int, user: User = Depends(current_active_user
auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}"
logger.info( logger.info(f"Generated Notion OAuth URL for user {user.id}, space {space_id}")
f"Generated Notion OAuth URL for user {user.id}, space {space_id}"
)
return {"auth_url": auth_url} return {"auth_url": auth_url}
except Exception as e: except Exception as e:
@ -158,7 +153,7 @@ async def notion_callback(
except Exception: except Exception:
# If state is invalid, we'll redirect without space_id # If state is invalid, we'll redirect without space_id
logger.warning("Failed to validate state in error handler") logger.warning("Failed to validate state in error handler")
# Redirect to frontend with error parameter # Redirect to frontend with error parameter
if space_id: if space_id:
return RedirectResponse( return RedirectResponse(
@ -168,16 +163,12 @@ async def notion_callback(
return RedirectResponse( return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=notion_oauth_denied" url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=notion_oauth_denied"
) )
# Validate required parameters for successful flow # Validate required parameters for successful flow
if not code: if not code:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing authorization code")
status_code=400, detail="Missing authorization code"
)
if not state: if not state:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing state parameter")
status_code=400, detail="Missing state parameter"
)
# Validate and decode state with signature verification # Validate and decode state with signature verification
state_manager = get_state_manager() state_manager = get_state_manager()
@ -325,4 +316,3 @@ async def notion_callback(
raise HTTPException( raise HTTPException(
status_code=500, detail=f"Failed to complete Notion OAuth: {e!s}" status_code=500, detail=f"Failed to complete Notion OAuth: {e!s}"
) from e ) from e

View file

@ -12,13 +12,13 @@ from app.routes.airtable_add_connector_route import refresh_airtable_token
from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase
from app.services.llm_service import get_user_long_context_llm from app.services.llm_service import get_user_long_context_llm
from app.services.task_logging_service import TaskLoggingService from app.services.task_logging_service import TaskLoggingService
from app.utils.oauth_security import TokenEncryption
from app.utils.document_converters import ( from app.utils.document_converters import (
create_document_chunks, create_document_chunks,
generate_content_hash, generate_content_hash,
generate_document_summary, generate_document_summary,
generate_unique_identifier_hash, generate_unique_identifier_hash,
) )
from app.utils.oauth_security import TokenEncryption
from .base import ( from .base import (
calculate_date_range, calculate_date_range,
@ -86,29 +86,35 @@ async def index_airtable_records(
return 0, f"Connector with ID {connector_id} not found" return 0, f"Connector with ID {connector_id} not found"
# Create credentials from connector config # Create credentials from connector config
config_data = connector.config.copy() # Work with a copy to avoid modifying original config_data = (
connector.config.copy()
) # Work with a copy to avoid modifying original
# Decrypt tokens if they are encrypted (for backward compatibility) # Decrypt tokens if they are encrypted (for backward compatibility)
token_encrypted = config_data.get("_token_encrypted", False) token_encrypted = config_data.get("_token_encrypted", False)
if token_encrypted and config.SECRET_KEY: if token_encrypted and config.SECRET_KEY:
try: try:
token_encryption = TokenEncryption(config.SECRET_KEY) token_encryption = TokenEncryption(config.SECRET_KEY)
# Decrypt access_token # Decrypt access_token
if config_data.get("access_token"): if config_data.get("access_token"):
if token_encryption.is_encrypted(config_data["access_token"]): if token_encryption.is_encrypted(config_data["access_token"]):
config_data["access_token"] = token_encryption.decrypt_token( config_data["access_token"] = token_encryption.decrypt_token(
config_data["access_token"] config_data["access_token"]
) )
logger.info(f"Decrypted Airtable access token for connector {connector_id}") logger.info(
f"Decrypted Airtable access token for connector {connector_id}"
)
# Decrypt refresh_token if present # Decrypt refresh_token if present
if config_data.get("refresh_token"): if config_data.get("refresh_token"):
if token_encryption.is_encrypted(config_data["refresh_token"]): if token_encryption.is_encrypted(config_data["refresh_token"]):
config_data["refresh_token"] = token_encryption.decrypt_token( config_data["refresh_token"] = token_encryption.decrypt_token(
config_data["refresh_token"] config_data["refresh_token"]
) )
logger.info(f"Decrypted Airtable refresh token for connector {connector_id}") logger.info(
f"Decrypted Airtable refresh token for connector {connector_id}"
)
except Exception as e: except Exception as e:
await task_logger.log_task_failure( await task_logger.log_task_failure(
log_entry, log_entry,
@ -117,7 +123,7 @@ async def index_airtable_records(
{"error_type": "TokenDecryptionError"}, {"error_type": "TokenDecryptionError"},
) )
return 0, f"Failed to decrypt Airtable tokens: {e!s}" return 0, f"Failed to decrypt Airtable tokens: {e!s}"
try: try:
credentials = AirtableAuthCredentialsBase.from_dict(config_data) credentials = AirtableAuthCredentialsBase.from_dict(config_data)
except Exception as e: except Exception as e:

View file

@ -8,7 +8,6 @@ from google.oauth2.credentials import Credentials
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import config
from app.connectors.google_calendar_connector import GoogleCalendarConnector from app.connectors.google_calendar_connector import GoogleCalendarConnector
from app.db import Document, DocumentType, SearchSourceConnectorType from app.db import Document, DocumentType, SearchSourceConnectorType
from app.services.llm_service import get_user_long_context_llm from app.services.llm_service import get_user_long_context_llm
@ -85,7 +84,7 @@ async def index_google_calendar_events(
# Get the Google Calendar credentials from the connector config # Get the Google Calendar credentials from the connector config
config_data = connector.config config_data = connector.config
# Decrypt sensitive credentials if encrypted (for backward compatibility) # Decrypt sensitive credentials if encrypted (for backward compatibility)
from app.config import config from app.config import config
from app.utils.oauth_security import TokenEncryption from app.utils.oauth_security import TokenEncryption
@ -94,7 +93,7 @@ async def index_google_calendar_events(
if token_encrypted and config.SECRET_KEY: if token_encrypted and config.SECRET_KEY:
try: try:
token_encryption = TokenEncryption(config.SECRET_KEY) token_encryption = TokenEncryption(config.SECRET_KEY)
# Decrypt sensitive fields # Decrypt sensitive fields
if config_data.get("token"): if config_data.get("token"):
config_data["token"] = token_encryption.decrypt_token( config_data["token"] = token_encryption.decrypt_token(
@ -108,7 +107,7 @@ async def index_google_calendar_events(
config_data["client_secret"] = token_encryption.decrypt_token( config_data["client_secret"] = token_encryption.decrypt_token(
config_data["client_secret"] config_data["client_secret"]
) )
logger.info( logger.info(
f"Decrypted Google Calendar credentials for connector {connector_id}" f"Decrypted Google Calendar credentials for connector {connector_id}"
) )

View file

@ -8,7 +8,6 @@ from google.oauth2.credentials import Credentials
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import config
from app.connectors.google_gmail_connector import GoogleGmailConnector from app.connectors.google_gmail_connector import GoogleGmailConnector
from app.db import ( from app.db import (
Document, Document,
@ -90,7 +89,7 @@ async def index_google_gmail_messages(
# Get the Google Gmail credentials from the connector config # Get the Google Gmail credentials from the connector config
config_data = connector.config config_data = connector.config
# Decrypt sensitive credentials if encrypted (for backward compatibility) # Decrypt sensitive credentials if encrypted (for backward compatibility)
from app.config import config from app.config import config
from app.utils.oauth_security import TokenEncryption from app.utils.oauth_security import TokenEncryption
@ -99,7 +98,7 @@ async def index_google_gmail_messages(
if token_encrypted and config.SECRET_KEY: if token_encrypted and config.SECRET_KEY:
try: try:
token_encryption = TokenEncryption(config.SECRET_KEY) token_encryption = TokenEncryption(config.SECRET_KEY)
# Decrypt sensitive fields # Decrypt sensitive fields
if config_data.get("token"): if config_data.get("token"):
config_data["token"] = token_encryption.decrypt_token( config_data["token"] = token_encryption.decrypt_token(
@ -113,7 +112,7 @@ async def index_google_gmail_messages(
config_data["client_secret"] = token_encryption.decrypt_token( config_data["client_secret"] = token_encryption.decrypt_token(
config_data["client_secret"] config_data["client_secret"]
) )
logger.info( logger.info(
f"Decrypted Google Gmail credentials for connector {connector_id}" f"Decrypted Google Gmail credentials for connector {connector_id}"
) )

View file

@ -7,7 +7,6 @@ from datetime import datetime
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import config
from app.connectors.linear_connector import LinearConnector from app.connectors.linear_connector import LinearConnector
from app.db import Document, DocumentType, SearchSourceConnectorType from app.db import Document, DocumentType, SearchSourceConnectorType
from app.services.llm_service import get_user_long_context_llm from app.services.llm_service import get_user_long_context_llm
@ -114,7 +113,9 @@ async def index_linear_issues(
): ):
try: try:
token_encryption = TokenEncryption(config.SECRET_KEY) token_encryption = TokenEncryption(config.SECRET_KEY)
linear_access_token = token_encryption.decrypt_token(linear_access_token) linear_access_token = token_encryption.decrypt_token(
linear_access_token
)
logger.info( logger.info(
f"Decrypted Linear access token for connector {connector_id}" f"Decrypted Linear access token for connector {connector_id}"
) )

View file

@ -107,11 +107,16 @@ async def index_notion_pages(
# Decrypt token if it's encrypted (for backward compatibility) # Decrypt token if it's encrypted (for backward compatibility)
token_encrypted = connector.config.get("_token_encrypted", False) token_encrypted = connector.config.get("_token_encrypted", False)
if token_encrypted or (config.SECRET_KEY and TokenEncryption(config.SECRET_KEY).is_encrypted(notion_token)): if token_encrypted or (
config.SECRET_KEY
and TokenEncryption(config.SECRET_KEY).is_encrypted(notion_token)
):
try: try:
token_encryption = TokenEncryption(config.SECRET_KEY) token_encryption = TokenEncryption(config.SECRET_KEY)
notion_token = token_encryption.decrypt_token(notion_token) notion_token = token_encryption.decrypt_token(notion_token)
logger.info(f"Decrypted Notion access token for connector {connector_id}") logger.info(
f"Decrypted Notion access token for connector {connector_id}"
)
except Exception as e: except Exception as e:
await task_logger.log_task_failure( await task_logger.log_task_failure(
log_entry, log_entry,

View file

@ -35,7 +35,9 @@ class OAuthStateManager:
self.secret_key = secret_key self.secret_key = secret_key
self.max_age_seconds = max_age_seconds self.max_age_seconds = max_age_seconds
def generate_secure_state(self, space_id: int, user_id: UUID, **extra_fields) -> str: def generate_secure_state(
self, space_id: int, user_id: UUID, **extra_fields
) -> str:
""" """
Generate cryptographically signed state parameter. Generate cryptographically signed state parameter.
@ -53,7 +55,7 @@ class OAuthStateManager:
"user_id": str(user_id), "user_id": str(user_id),
"timestamp": timestamp, "timestamp": timestamp,
} }
# Add any extra fields (e.g., code_verifier for PKCE) # Add any extra fields (e.g., code_verifier for PKCE)
state_payload.update(extra_fields) state_payload.update(extra_fields)
@ -97,9 +99,7 @@ class OAuthStateManager:
# Verify signature exists # Verify signature exists
signature = data.pop("signature", None) signature = data.pop("signature", None)
if not signature: if not signature:
raise HTTPException( raise HTTPException(status_code=400, detail="Missing state signature")
status_code=400, detail="Missing state signature"
)
# Verify signature # Verify signature
payload_str = json.dumps(data, sort_keys=True) payload_str = json.dumps(data, sort_keys=True)
@ -120,9 +120,7 @@ class OAuthStateManager:
age = current_time - timestamp age = current_time - timestamp
if age < 0: if age < 0:
raise HTTPException( raise HTTPException(status_code=400, detail="Invalid state timestamp")
status_code=400, detail="Invalid state timestamp"
)
if age > self.max_age_seconds: if age > self.max_age_seconds:
raise HTTPException( raise HTTPException(
@ -147,15 +145,11 @@ class TokenEncryption:
raise ValueError("secret_key is required for token encryption") raise ValueError("secret_key is required for token encryption")
# Derive Fernet key from secret using SHA256 # Derive Fernet key from secret using SHA256
# Note: In production, consider using HKDF for key derivation # Note: In production, consider using HKDF for key derivation
key = base64.urlsafe_b64encode( key = base64.urlsafe_b64encode(hashlib.sha256(secret_key.encode()).digest())
hashlib.sha256(secret_key.encode()).digest()
)
try: try:
self.cipher = Fernet(key) self.cipher = Fernet(key)
except Exception as e: except Exception as e:
raise ValueError( raise ValueError(f"Failed to initialize encryption cipher: {e!s}") from e
f"Failed to initialize encryption cipher: {e!s}"
) from e
def encrypt_token(self, token: str) -> str: def encrypt_token(self, token: str) -> str:
""" """

View file

@ -1,13 +1,7 @@
"use client"; "use client";
import { IconBrandYoutube } from "@tabler/icons-react"; import { IconBrandYoutube } from "@tabler/icons-react";
import { import { differenceInDays, differenceInMinutes, format, isToday, isYesterday } from "date-fns";
differenceInDays,
differenceInMinutes,
format,
isToday,
isYesterday,
} from "date-fns";
import { FileText, Loader2 } from "lucide-react"; import { FileText, Loader2 } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";

View file

@ -46,7 +46,7 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
}) => { }) => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const isFromOAuth = searchParams.get("view") === "configure"; const isFromOAuth = searchParams.get("view") === "configure";
// Get connector-specific config component // Get connector-specific config component
const ConnectorConfigComponent = useMemo( const ConnectorConfigComponent = useMemo(
() => (connector ? getConnectorConfigComponent(connector.connector_type) : null), () => (connector ? getConnectorConfigComponent(connector.connector_type) : null),

View file

@ -1,12 +1,6 @@
"use client"; "use client";
import { import { differenceInDays, differenceInMinutes, format, isToday, isYesterday } from "date-fns";
differenceInDays,
differenceInMinutes,
format,
isToday,
isYesterday,
} from "date-fns";
import { ArrowRight, Cable, Loader2 } from "lucide-react"; import { ArrowRight, Cable, Loader2 } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { FC } from "react"; import type { FC } from "react";

View file

@ -99,7 +99,7 @@ const DocumentUploadPopupContent: FC<{
<Dialog open={isOpen} onOpenChange={onOpenChange}> <Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl w-[95vw] sm:w-full max-h-[calc(100vh-2rem)] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-3 sm:[&>button]:right-12 [&>button]:top-4 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button]:z-[100] [&>button_svg]:size-4 sm:[&>button_svg]:size-5"> <DialogContent className="max-w-4xl w-[95vw] sm:w-full max-h-[calc(100vh-2rem)] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-3 sm:[&>button]:right-12 [&>button]:top-4 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button]:z-[100] [&>button_svg]:size-4 sm:[&>button_svg]:size-5">
<DialogTitle className="sr-only">Upload Document</DialogTitle> <DialogTitle className="sr-only">Upload Document</DialogTitle>
{/* Fixed Header */} {/* Fixed Header */}
<div className="flex-shrink-0 px-4 sm:px-12 pt-6 sm:pt-10 transition-shadow duration-200 relative z-10"> <div className="flex-shrink-0 px-4 sm:px-12 pt-6 sm:pt-10 transition-shadow duration-200 relative z-10">
{/* Upload header */} {/* Upload header */}
@ -120,8 +120,8 @@ const DocumentUploadPopupContent: FC<{
<div className="flex-1 min-h-0 relative overflow-hidden"> <div className="flex-1 min-h-0 relative overflow-hidden">
<div className={`h-full ${isAccordionExpanded ? "overflow-y-auto" : ""}`}> <div className={`h-full ${isAccordionExpanded ? "overflow-y-auto" : ""}`}>
<div className="px-6 sm:px-12 pb-5 sm:pb-16"> <div className="px-6 sm:px-12 pb-5 sm:pb-16">
<DocumentUploadTab <DocumentUploadTab
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
onSuccess={handleSuccess} onSuccess={handleSuccess}
onAccordionStateChange={setIsAccordionExpanded} onAccordionStateChange={setIsAccordionExpanded}
/> />

View file

@ -110,7 +110,11 @@ const FILE_TYPE_CONFIG: Record<string, Record<string, string[]>> = {
const cardClass = "border border-border bg-slate-400/5 dark:bg-white/5"; const cardClass = "border border-border bg-slate-400/5 dark:bg-white/5";
export function DocumentUploadTab({ searchSpaceId, onSuccess, onAccordionStateChange }: DocumentUploadTabProps) { export function DocumentUploadTab({
searchSpaceId,
onSuccess,
onAccordionStateChange,
}: DocumentUploadTabProps) {
const t = useTranslations("upload_documents"); const t = useTranslations("upload_documents");
const router = useRouter(); const router = useRouter();
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
@ -157,10 +161,13 @@ export function DocumentUploadTab({ searchSpaceId, onSuccess, onAccordionStateCh
const totalFileSize = files.reduce((total, file) => total + file.size, 0); const totalFileSize = files.reduce((total, file) => total + file.size, 0);
// Track accordion state changes // Track accordion state changes
const handleAccordionChange = useCallback((value: string) => { const handleAccordionChange = useCallback(
setAccordionValue(value); (value: string) => {
onAccordionStateChange?.(value === "supported-file-types"); setAccordionValue(value);
}, [onAccordionStateChange]); onAccordionStateChange?.(value === "supported-file-types");
},
[onAccordionStateChange]
);
const handleUpload = async () => { const handleUpload = async () => {
setUploadProgress(0); setUploadProgress(0);
@ -202,7 +209,9 @@ export function DocumentUploadTab({ searchSpaceId, onSuccess, onAccordionStateCh
> >
<Alert className="border border-border bg-slate-400/5 dark:bg-white/5 flex items-start gap-3 [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg~*]:pl-0"> <Alert className="border border-border bg-slate-400/5 dark:bg-white/5 flex items-start gap-3 [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg~*]:pl-0">
<Info className="h-4 w-4 shrink-0 mt-0.5" /> <Info className="h-4 w-4 shrink-0 mt-0.5" />
<AlertDescription className="text-xs sm:text-sm leading-relaxed pt-0.5">{t("file_size_limit")}</AlertDescription> <AlertDescription className="text-xs sm:text-sm leading-relaxed pt-0.5">
{t("file_size_limit")}
</AlertDescription>
</Alert> </Alert>
<Card className={`relative overflow-hidden ${cardClass}`}> <Card className={`relative overflow-hidden ${cardClass}`}>