diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index caa1a2546..fed21913e 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -61,7 +61,7 @@ from .rbac_routes import router as rbac_router from .reports_routes import router as reports_router from .sandbox_routes import router as sandbox_router from .search_source_connectors_routes import router as search_source_connectors_router -from .search_spaces_routes import router as search_spaces_router +from .workspaces_routes import router as workspaces_router from .slack_add_connector_route import router as slack_add_connector_router from .stripe_routes import router as stripe_router from .team_memory_routes import router as team_memory_router @@ -71,7 +71,7 @@ from .youtube_routes import router as youtube_router router = APIRouter() -router.include_router(search_spaces_router) +router.include_router(workspaces_router) router.include_router(rbac_router) # RBAC routes for roles, members, invites router.include_router(editor_router) router.include_router(export_router) @@ -92,7 +92,7 @@ router.include_router(agent_revert_router) # POST /threads/{id}/revert/{action_ router.include_router(agent_action_log_router) # GET /threads/{id}/actions router.include_router( agent_permissions_router -) # CRUD for /searchspaces/{id}/agent/permissions/rules +) # CRUD for /workspaces/{id}/agent/permissions/rules router.include_router(agent_flags_router) # GET /agent/flags router.include_router(sandbox_router) # Sandbox file downloads (Daytona) router.include_router(chat_comments_router) @@ -135,6 +135,6 @@ router.include_router(stripe_router) # Stripe checkout for additional page pack router.include_router(youtube_router) # YouTube playlist resolution router.include_router(prompts_router) router.include_router(memory_router) # User personal memory (memory.md style) -router.include_router(team_memory_router) # Search-space team memory +router.include_router(team_memory_router) # Workspace team memory router.include_router(automations_router) # Automations CRUD + run history router.include_router(file_storage_router) # Original file metadata + download diff --git a/surfsense_backend/app/routes/agent_action_log_route.py b/surfsense_backend/app/routes/agent_action_log_route.py index 72086b8ae..582927d76 100644 --- a/surfsense_backend/app/routes/agent_action_log_route.py +++ b/surfsense_backend/app/routes/agent_action_log_route.py @@ -55,7 +55,7 @@ class AgentActionRead(BaseModel): id: int thread_id: int user_id: str | None - search_space_id: int + workspace_id: int tool_name: str args: dict[str, Any] | None result_id: str | None @@ -116,7 +116,7 @@ async def list_thread_actions( """List agent actions for a thread, newest first. Authorization: - * Caller must be a member of the thread's search space with + * Caller must be a member of the thread's workspace with ``CHATS_READ`` permission. Pagination: @@ -133,7 +133,7 @@ async def list_thread_actions( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_READ.value, "You don't have permission to view this thread's action log.", ) @@ -169,7 +169,7 @@ async def list_thread_actions( id=row.id, thread_id=row.thread_id, user_id=str(row.user_id) if row.user_id is not None else None, - search_space_id=row.search_space_id, + workspace_id=row.workspace_id, tool_name=row.tool_name, args=row.args, result_id=row.result_id, diff --git a/surfsense_backend/app/routes/agent_permissions_route.py b/surfsense_backend/app/routes/agent_permissions_route.py index 1eb8b1a37..d9136100d 100644 --- a/surfsense_backend/app/routes/agent_permissions_route.py +++ b/surfsense_backend/app/routes/agent_permissions_route.py @@ -3,12 +3,12 @@ Surfaces the permission rules consumed by :class:`PermissionMiddleware`. Rules are scoped at one of three levels: -* **Search-space wide** — both ``user_id`` and ``thread_id`` are NULL. +* **Workspace wide** — both ``user_id`` and ``thread_id`` are NULL. * **Per-user** — ``user_id`` set, ``thread_id`` NULL. * **Per-thread** — ``thread_id`` set (``user_id`` typically NULL). The middleware reads these rows at agent build time (see -``chat_deepagent.py``). UI lets a search-space owner curate them so +``chat_deepagent.py``). UI lets a workspace owner curate them so the agent can ask for approval / auto-deny / auto-allow specific tool patterns. @@ -36,7 +36,7 @@ from app.db import ( AgentPermissionRule, NewChatThread, Permission, - SearchSpace, + Workspace, get_async_session, ) from app.users import get_auth_context @@ -58,7 +58,7 @@ _PERMISSION_PATTERN = re.compile(r"^[a-zA-Z0-9_:.\-*]+$") class AgentPermissionRuleRead(BaseModel): id: int - search_space_id: int + workspace_id: int user_id: str | None thread_id: int | None permission: str @@ -122,7 +122,7 @@ def _validate_permission_string(value: str) -> str: def _to_read(row: AgentPermissionRule) -> AgentPermissionRuleRead: return AgentPermissionRuleRead( id=row.id, - search_space_id=row.search_space_id, + workspace_id=row.workspace_id, user_id=str(row.user_id) if row.user_id is not None else None, thread_id=row.thread_id, permission=row.permission, @@ -132,17 +132,17 @@ def _to_read(row: AgentPermissionRule) -> AgentPermissionRuleRead: ) -async def _ensure_search_space_membership_admin( - session: AsyncSession, auth: AuthContext, search_space_id: int +async def _ensure_workspace_membership_admin( + session: AsyncSession, auth: AuthContext, workspace_id: int ) -> None: """Curating agent rules == "settings" administration on the space.""" - space = await session.get(SearchSpace, search_space_id) + space = await session.get(Workspace, workspace_id) if space is None: - raise HTTPException(status_code=404, detail="Search space not found.") + raise HTTPException(status_code=404, detail="Workspace not found.") await check_permission( session, auth, - search_space_id, + workspace_id, Permission.SETTINGS_UPDATE.value, "You don't have permission to manage agent permission rules in this space.", ) @@ -154,21 +154,21 @@ async def _ensure_search_space_membership_admin( @router.get( - "/searchspaces/{search_space_id}/agent/permissions/rules", + "/workspaces/{workspace_id}/agent/permissions/rules", response_model=list[AgentPermissionRuleRead], ) async def list_rules( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ) -> list[AgentPermissionRuleRead]: user = auth.user _flag_guard() - await _ensure_search_space_membership_admin(session, user, search_space_id) + await _ensure_workspace_membership_admin(session, user, workspace_id) stmt = ( select(AgentPermissionRule) - .where(AgentPermissionRule.search_space_id == search_space_id) + .where(AgentPermissionRule.workspace_id == workspace_id) .order_by(AgentPermissionRule.created_at.desc(), AgentPermissionRule.id.desc()) ) rows = (await session.execute(stmt)).scalars().all() @@ -176,33 +176,33 @@ async def list_rules( @router.post( - "/searchspaces/{search_space_id}/agent/permissions/rules", + "/workspaces/{workspace_id}/agent/permissions/rules", response_model=AgentPermissionRuleRead, status_code=201, ) async def create_rule( - search_space_id: int, + workspace_id: int, payload: AgentPermissionRuleCreate, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ) -> AgentPermissionRuleRead: user = auth.user _flag_guard() - await _ensure_search_space_membership_admin(session, user, search_space_id) + await _ensure_workspace_membership_admin(session, user, workspace_id) permission = _validate_permission_string(payload.permission.strip()) pattern = payload.pattern.strip() or "*" if payload.thread_id is not None: thread = await session.get(NewChatThread, payload.thread_id) - if thread is None or thread.search_space_id != search_space_id: + if thread is None or thread.workspace_id != workspace_id: raise HTTPException( status_code=404, - detail="Thread not found in this search space.", + detail="Thread not found in this workspace.", ) row = AgentPermissionRule( - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=payload.user_id, thread_id=payload.thread_id, permission=permission, @@ -226,11 +226,11 @@ async def create_rule( @router.patch( - "/searchspaces/{search_space_id}/agent/permissions/rules/{rule_id}", + "/workspaces/{workspace_id}/agent/permissions/rules/{rule_id}", response_model=AgentPermissionRuleRead, ) async def update_rule( - search_space_id: int, + workspace_id: int, rule_id: int, payload: AgentPermissionRuleUpdate, session: AsyncSession = Depends(get_async_session), @@ -238,10 +238,10 @@ async def update_rule( ) -> AgentPermissionRuleRead: user = auth.user _flag_guard() - await _ensure_search_space_membership_admin(session, user, search_space_id) + await _ensure_workspace_membership_admin(session, user, workspace_id) row = await session.get(AgentPermissionRule, rule_id) - if row is None or row.search_space_id != search_space_id: + if row is None or row.workspace_id != workspace_id: raise HTTPException(status_code=404, detail="Rule not found.") if payload.pattern is not None: @@ -262,21 +262,21 @@ async def update_rule( @router.delete( - "/searchspaces/{search_space_id}/agent/permissions/rules/{rule_id}", + "/workspaces/{workspace_id}/agent/permissions/rules/{rule_id}", status_code=204, ) async def delete_rule( - search_space_id: int, + workspace_id: int, rule_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ) -> None: user = auth.user _flag_guard() - await _ensure_search_space_membership_admin(session, user, search_space_id) + await _ensure_workspace_membership_admin(session, user, workspace_id) row = await session.get(AgentPermissionRule, rule_id) - if row is None or row.search_space_id != search_space_id: + if row is None or row.workspace_id != workspace_id: raise HTTPException(status_code=404, detail="Rule not found.") await session.delete(row) diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index d5cbc2498..c34c7d2fb 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -86,7 +86,7 @@ async def connect_airtable( Initiate Airtable OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -317,7 +317,7 @@ async def airtable_callback( connector_type=SearchSourceConnectorType.AIRTABLE_CONNECTOR, is_indexable=False, config=credentials_dict, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) diff --git a/surfsense_backend/app/routes/chat_comments_routes.py b/surfsense_backend/app/routes/chat_comments_routes.py index 2e1eb1d27..528fbaf38 100644 --- a/surfsense_backend/app/routes/chat_comments_routes.py +++ b/surfsense_backend/app/routes/chat_comments_routes.py @@ -101,9 +101,9 @@ async def remove_comment( @router.get("/mentions", response_model=MentionListResponse) async def list_mentions( - search_space_id: int | None = None, + workspace_id: int | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(require_session_context), ): """List mentions for the current user.""" - return await get_user_mentions(session, auth, search_space_id) + return await get_user_mentions(session, auth, workspace_id) diff --git a/surfsense_backend/app/routes/circleback_webhook_route.py b/surfsense_backend/app/routes/circleback_webhook_route.py index 4a5823645..11ddc3f95 100644 --- a/surfsense_backend/app/routes/circleback_webhook_route.py +++ b/surfsense_backend/app/routes/circleback_webhook_route.py @@ -2,7 +2,7 @@ Circleback Webhook Route This module provides a webhook endpoint for receiving meeting data from Circleback. -It processes the incoming webhook payload and saves it as a document in the specified search space. +It processes the incoming webhook payload and saves it as a document in the specified workspace. """ import logging @@ -212,9 +212,9 @@ def format_circleback_meeting_to_markdown(payload: CirclebackWebhookPayload) -> return "\n".join(lines) -@router.post("/webhooks/circleback/{search_space_id}") +@router.post("/webhooks/circleback/{workspace_id}") async def receive_circleback_webhook( - search_space_id: int, + workspace_id: int, payload: CirclebackWebhookPayload, session: AsyncSession = Depends(get_async_session), ): @@ -222,11 +222,11 @@ async def receive_circleback_webhook( Receive and process a Circleback webhook. This endpoint receives meeting data from Circleback and saves it as a document - in the specified search space. The meeting data is converted to Markdown format + in the specified workspace. The meeting data is converted to Markdown format and processed asynchronously. Args: - search_space_id: The ID of the search space to save the document to + workspace_id: The ID of the workspace to save the document to payload: The Circleback webhook payload containing meeting data session: Database session for looking up the connector @@ -239,13 +239,13 @@ async def receive_circleback_webhook( """ try: logger.info( - f"Received Circleback webhook for meeting {payload.id} in search space {search_space_id}" + f"Received Circleback webhook for meeting {payload.id} in workspace {workspace_id}" ) - # Look up the Circleback connector for this search space + # Look up the Circleback connector for this workspace connector_result = await session.execute( select(SearchSourceConnector.id).where( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CIRCLEBACK_CONNECTOR, ) @@ -254,11 +254,11 @@ async def receive_circleback_webhook( if connector_id: logger.info( - f"Found Circleback connector {connector_id} for search space {search_space_id}" + f"Found Circleback connector {connector_id} for workspace {workspace_id}" ) else: logger.warning( - f"No Circleback connector found for search space {search_space_id}. " + f"No Circleback connector found for workspace {workspace_id}. " "Document will be created without connector_id." ) @@ -289,19 +289,19 @@ async def receive_circleback_webhook( meeting_name=payload.name, markdown_content=markdown_content, metadata=meeting_metadata, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, ) logger.info( - f"Queued Circleback meeting {payload.id} for processing in search space {search_space_id}" + f"Queued Circleback meeting {payload.id} for processing in workspace {workspace_id}" ) return { "status": "accepted", "message": f"Meeting '{payload.name}' queued for processing", "meeting_id": payload.id, - "search_space_id": search_space_id, + "workspace_id": workspace_id, } except Exception as e: @@ -312,9 +312,9 @@ async def receive_circleback_webhook( ) from e -@router.get("/webhooks/circleback/{search_space_id}/info") +@router.get("/webhooks/circleback/{workspace_id}/info") async def get_circleback_webhook_info( - search_space_id: int, + workspace_id: int, ): """ Get information about the Circleback webhook endpoint. @@ -323,7 +323,7 @@ async def get_circleback_webhook_info( webhook integration. Args: - search_space_id: The ID of the search space + workspace_id: The ID of the workspace Returns: Webhook configuration information @@ -332,11 +332,11 @@ async def get_circleback_webhook_info( # Construct the webhook URL base_url = getattr(config, "API_BASE_URL", "http://localhost:8000") - webhook_url = f"{base_url}/api/v1/webhooks/circleback/{search_space_id}" + webhook_url = f"{base_url}/api/v1/webhooks/circleback/{workspace_id}" return { "webhook_url": webhook_url, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "method": "POST", "content_type": "application/json", "description": "Use this URL in your Circleback automation to send meeting data to SurfSense", diff --git a/surfsense_backend/app/routes/clickup_add_connector_route.py b/surfsense_backend/app/routes/clickup_add_connector_route.py index 3be32b217..e6e23d57f 100644 --- a/surfsense_backend/app/routes/clickup_add_connector_route.py +++ b/surfsense_backend/app/routes/clickup_add_connector_route.py @@ -69,7 +69,7 @@ async def connect_clickup( Initiate ClickUp OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -290,10 +290,10 @@ async def clickup_callback( "_token_encrypted": True, } - # Check if connector already exists for this search space and user + # Check if connector already exists for this workspace and user existing_connector_result = await session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CLICKUP_CONNECTOR, @@ -316,7 +316,7 @@ async def clickup_callback( connector_type=SearchSourceConnectorType.CLICKUP_CONNECTOR, is_indexable=False, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) diff --git a/surfsense_backend/app/routes/composio_routes.py b/surfsense_backend/app/routes/composio_routes.py index 410f90256..bdbbe5d38 100644 --- a/surfsense_backend/app/routes/composio_routes.py +++ b/surfsense_backend/app/routes/composio_routes.py @@ -41,7 +41,7 @@ from app.utils.connector_naming import ( get_base_name_for_type, ) from app.utils.oauth_security import OAuthStateManager -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access logger = logging.getLogger(__name__) @@ -108,7 +108,7 @@ async def initiate_composio_auth( Initiate Composio OAuth flow for a specific toolkit. Query params: - space_id: Search space ID to add connector to + space_id: Workspace ID to add connector to toolkit_id: Composio toolkit ID (e.g., "googledrive", "gmail", "googlecalendar") Returns: @@ -334,7 +334,7 @@ async def composio_callback( existing_connector_result = await session.execute( select(SearchSourceConnector).where( SearchSourceConnector.connector_type == connector_type, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.name == connector_name, ) @@ -395,7 +395,7 @@ async def composio_callback( name=connector_name, connector_type=connector_type, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, is_indexable=toolkit_id in INDEXABLE_TOOLKITS, ) @@ -461,7 +461,7 @@ async def reauth_composio_connector( after the user completes the OAuth flow again. Query params: - space_id: Search space ID the connector belongs to + space_id: Workspace ID the connector belongs to connector_id: ID of the existing Composio connector to re-authenticate return_url: Optional frontend path to redirect to after completion """ @@ -481,7 +481,7 @@ async def reauth_composio_connector( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type.in_(COMPOSIO_CONNECTOR_TYPES), ) ) @@ -594,7 +594,7 @@ async def composio_reauth_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, ) ) connector = result.scalars().first() @@ -683,7 +683,7 @@ async def list_composio_drive_folders( detail="Composio Google Drive connector not found or access denied", ) - await check_search_space_access(session, auth, connector.search_space_id) + await check_workspace_access(session, auth, connector.workspace_id) composio_connected_account_id = connector.config.get( "composio_connected_account_id" diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py index cc9e681bf..d6aaaf268 100644 --- a/surfsense_backend/app/routes/confluence_add_connector_route.py +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -85,7 +85,7 @@ async def connect_confluence( Initiate Confluence OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -309,7 +309,7 @@ async def confluence_callback( sa_select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, ) @@ -375,7 +375,7 @@ async def confluence_callback( connector_type=SearchSourceConnectorType.CONFLUENCE_CONNECTOR, is_indexable=True, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) @@ -437,7 +437,7 @@ async def reauth_confluence( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, ) diff --git a/surfsense_backend/app/routes/discord_add_connector_route.py b/surfsense_backend/app/routes/discord_add_connector_route.py index 1da0987b0..6b335a885 100644 --- a/surfsense_backend/app/routes/discord_add_connector_route.py +++ b/surfsense_backend/app/routes/discord_add_connector_route.py @@ -30,7 +30,7 @@ from app.utils.connector_naming import ( generate_unique_connector_name, ) from app.utils.oauth_security import OAuthStateManager, TokenEncryption -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access logger = logging.getLogger(__name__) @@ -86,7 +86,7 @@ async def connect_discord( Initiate Discord OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -333,7 +333,7 @@ async def discord_callback( connector_type=SearchSourceConnectorType.DISCORD_CONNECTOR, is_indexable=False, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) @@ -652,7 +652,7 @@ async def get_discord_channels( detail="Discord connector not found or access denied", ) - await check_search_space_access(session, auth, connector.search_space_id) + await check_workspace_access(session, auth, connector.workspace_id) # Get credentials and decrypt bot token credentials = DiscordAuthCredentialsBase.from_dict(connector.config) diff --git a/surfsense_backend/app/routes/documents_routes.py b/surfsense_backend/app/routes/documents_routes.py index accf3b18f..ade17a59f 100644 --- a/surfsense_backend/app/routes/documents_routes.py +++ b/surfsense_backend/app/routes/documents_routes.py @@ -16,8 +16,8 @@ from app.db import ( DocumentVersion, Folder, Permission, - SearchSpace, - SearchSpaceMembership, + Workspace, + WorkspaceMembership, get_async_session, ) from app.schemas import ( @@ -72,9 +72,9 @@ async def create_documents( await check_permission( session, auth, - request.search_space_id, + request.workspace_id, Permission.DOCUMENTS_CREATE.value, - "You don't have permission to create documents in this search space", + "You don't have permission to create documents in this workspace", ) if request.document_type == DocumentType.EXTENSION: @@ -96,14 +96,14 @@ async def create_documents( "pageContent": individual_document.pageContent, } process_extension_document_task.delay( - document_dict, request.search_space_id, str(user.id) + document_dict, request.workspace_id, str(user.id) ) elif request.document_type == DocumentType.YOUTUBE_VIDEO: from app.tasks.celery_tasks.document_tasks import process_youtube_video_task for url in request.content: process_youtube_video_task.delay( - url, request.search_space_id, str(user.id) + url, request.workspace_id, str(user.id) ) else: raise HTTPException(status_code=400, detail="Invalid document type") @@ -125,7 +125,7 @@ async def create_documents( @router.post("/documents/fileupload") async def create_documents_file_upload( files: list[UploadFile], - search_space_id: int = Form(...), + workspace_id: int = Form(...), use_vision_llm: bool = Form(False), processing_mode: str = Form("basic"), session: AsyncSession = Depends(get_async_session), @@ -162,9 +162,9 @@ async def create_documents_file_upload( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_CREATE.value, - "You don't have permission to create documents in this search space", + "You don't have permission to create documents in this workspace", ) if not files: @@ -216,7 +216,7 @@ async def create_documents_file_upload( for temp_path, filename, file_size, content_type in saved_files: try: unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.FILE, filename, search_space_id + DocumentType.FILE, filename, workspace_id ) existing = await check_document_by_unique_identifier( @@ -244,7 +244,7 @@ async def create_documents_file_upload( continue document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=filename if filename != "unknown" else "Uploaded File", document_type=DocumentType.FILE, document_metadata={ @@ -288,7 +288,7 @@ async def create_documents_file_upload( await store_document_file( session, document_id=document.id, - search_space_id=search_space_id, + workspace_id=workspace_id, data=original_bytes, filename=filename, mime_type=content_type, @@ -308,7 +308,7 @@ async def create_documents_file_upload( document_id=document.id, temp_path=temp_path, filename=filename, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=str(user.id), use_vision_llm=use_vision_llm, processing_mode=validated_mode.value, @@ -336,7 +336,7 @@ async def read_documents( skip: int | None = None, page: int | None = None, page_size: int = 50, - search_space_id: int | None = None, + workspace_id: int | None = None, document_types: str | None = None, folder_id: int | str | None = None, sort_by: str = "created_at", @@ -347,13 +347,13 @@ async def read_documents( user = auth.user """ List documents the user has access to, with optional filtering and pagination. - Requires DOCUMENTS_READ permission for the search space(s). + Requires DOCUMENTS_READ permission for the workspace(s). Args: skip: Absolute number of items to skip from the beginning. If provided, it takes precedence over 'page'. page: Zero-based page index used when 'skip' is not provided. page_size: Number of items per page (default: 50). Use -1 to return all remaining items after the offset. - search_space_id: If provided, restrict results to a specific search space. + workspace_id: If provided, restrict results to a specific workspace. document_types: Comma-separated list of document types to filter by (e.g., "EXTENSION,FILE,SLACK_CONNECTOR"). session: Database session (injected). user: Current authenticated user (injected). @@ -363,45 +363,45 @@ async def read_documents( Notes: - If both 'skip' and 'page' are provided, 'skip' is used. - - Results are scoped to documents in search spaces the user has membership in. + - Results are scoped to documents in workspaces the user has membership in. """ try: from sqlalchemy import func - # If specific search_space_id, check permission - if search_space_id is not None: + # If specific workspace_id, check permission + if workspace_id is not None: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) query = ( select(Document) .options(selectinload(Document.created_by)) - .filter(Document.search_space_id == search_space_id) + .filter(Document.workspace_id == workspace_id) ) count_query = ( select(func.count()) .select_from(Document) - .filter(Document.search_space_id == search_space_id) + .filter(Document.workspace_id == workspace_id) ) else: - # Get documents from all search spaces user has membership in + # Get documents from all workspaces user has membership in query = ( select(Document) .options(selectinload(Document.created_by)) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) ) count_query = ( select(func.count()) .select_from(Document) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) ) # Filter by document_types if provided @@ -483,7 +483,7 @@ async def read_documents( unique_identifier_hash=doc.unique_identifier_hash, created_at=doc.created_at, updated_at=doc.updated_at, - search_space_id=doc.search_space_id, + workspace_id=doc.workspace_id, folder_id=doc.folder_id, created_by_id=doc.created_by_id, created_by_name=created_by_name, @@ -519,22 +519,22 @@ async def search_documents( skip: int | None = None, page: int | None = None, page_size: int = 50, - search_space_id: int | None = None, + workspace_id: int | None = None, document_types: str | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ - Search documents by title substring, optionally filtered by search_space_id and document_types. - Requires DOCUMENTS_READ permission for the search space(s). + Search documents by title substring, optionally filtered by workspace_id and document_types. + Requires DOCUMENTS_READ permission for the workspace(s). Args: title: Case-insensitive substring to match against document titles. Required. skip: Absolute number of items to skip from the beginning. If provided, it takes precedence over 'page'. Default: None. page: Zero-based page index used when 'skip' is not provided. Default: None. page_size: Number of items per page. Use -1 to return all remaining items after the offset. Default: 50. - search_space_id: Filter results to a specific search space. Default: None. + workspace_id: Filter results to a specific workspace. Default: None. document_types: Comma-separated list of document types to filter by (e.g., "EXTENSION,FILE,SLACK_CONNECTOR"). session: Database session (injected). user: Current authenticated user (injected). @@ -549,40 +549,40 @@ async def search_documents( try: from sqlalchemy import func - # If specific search_space_id, check permission - if search_space_id is not None: + # If specific workspace_id, check permission + if workspace_id is not None: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) query = ( select(Document) .options(selectinload(Document.created_by)) - .filter(Document.search_space_id == search_space_id) + .filter(Document.workspace_id == workspace_id) ) count_query = ( select(func.count()) .select_from(Document) - .filter(Document.search_space_id == search_space_id) + .filter(Document.workspace_id == workspace_id) ) else: - # Get documents from all search spaces user has membership in + # Get documents from all workspaces user has membership in query = ( select(Document) .options(selectinload(Document.created_by)) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) ) count_query = ( select(func.count()) .select_from(Document) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) ) # Only search by title (case-insensitive) @@ -644,7 +644,7 @@ async def search_documents( unique_identifier_hash=doc.unique_identifier_hash, created_at=doc.created_at, updated_at=doc.updated_at, - search_space_id=doc.search_space_id, + workspace_id=doc.workspace_id, folder_id=doc.folder_id, created_by_id=doc.created_by_id, created_by_name=created_by_name, @@ -676,7 +676,7 @@ async def search_documents( @router.get("/documents/search/titles", response_model=DocumentTitleSearchResponse) async def search_document_titles( - search_space_id: int, + workspace_id: int, title: str = "", page: int = 0, page_size: int = 20, @@ -691,7 +691,7 @@ async def search_document_titles( Results are ordered by relevance using trigram similarity scores. Args: - search_space_id: The search space to search in. Required. + workspace_id: The workspace to search in. Required. title: Search query (case-insensitive). If empty or < 2 chars, returns recent documents. page: Zero-based page index. Default: 0. page_size: Number of items per page. Default: 20. @@ -704,13 +704,13 @@ async def search_document_titles( from sqlalchemy import desc, func, or_ try: - # Check permission for the search space + # Check permission for the workspace await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) # Base query - only select lightweight fields @@ -718,7 +718,7 @@ async def search_document_titles( Document.id, Document.title, Document.document_type, - ).filter(Document.search_space_id == search_space_id) + ).filter(Document.workspace_id == workspace_id) # If query is too short, return recent documents ordered by updated_at if len(title.strip()) < 2: @@ -782,7 +782,7 @@ async def search_document_titles( @router.get("/documents/by-virtual-path", response_model=DocumentTitleRead) async def get_document_by_virtual_path( - search_space_id: int, + workspace_id: int, virtual_path: str, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -809,14 +809,14 @@ async def get_document_by_virtual_path( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) document = await virtual_path_to_doc( session, - search_space_id=search_space_id, + workspace_id=workspace_id, virtual_path=virtual_path, ) if document is None: @@ -839,13 +839,13 @@ async def get_document_by_virtual_path( @router.get("/documents/status", response_model=DocumentStatusBatchResponse) async def get_documents_status( - search_space_id: int, + workspace_id: int, document_ids: str, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - Batch status endpoint for documents in a search space. + Batch status endpoint for documents in a workspace. Returns lightweight status info for the provided document IDs, intended for polling async ETL progress in chat upload flows. @@ -854,9 +854,9 @@ async def get_documents_status( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) # Parse comma-separated IDs (e.g. "1,2,3") @@ -878,7 +878,7 @@ async def get_documents_status( result = await session.execute( select(Document).filter( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.id.in_(parsed_ids), ) ) @@ -907,17 +907,17 @@ async def get_documents_status( @router.get("/documents/type-counts") async def get_document_type_counts( - search_space_id: int | None = None, + workspace_id: int | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ - Get counts of documents by type for search spaces the user has access to. - Requires DOCUMENTS_READ permission for the search space(s). + Get counts of documents by type for workspaces the user has access to. + Requires DOCUMENTS_READ permission for the workspace(s). Args: - search_space_id: If provided, restrict counts to a specific search space. + workspace_id: If provided, restrict counts to a specific workspace. session: Database session (injected). user: Current authenticated user (injected). @@ -927,27 +927,27 @@ async def get_document_type_counts( try: from sqlalchemy import func - if search_space_id is not None: - # Check permission for specific search space + if workspace_id is not None: + # Check permission for specific workspace await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) query = ( select(Document.document_type, func.count(Document.id)) - .filter(Document.search_space_id == search_space_id) + .filter(Document.workspace_id == workspace_id) .group_by(Document.document_type) ) else: - # Get counts from all search spaces user has membership in + # Get counts from all workspaces user has membership in query = ( select(Document.document_type, func.count(Document.id)) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) .group_by(Document.document_type) ) @@ -1001,9 +1001,9 @@ async def get_document_by_chunk_id( await check_permission( session, auth, - document.search_space_id, + document.workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) total_result = await session.execute( @@ -1048,7 +1048,7 @@ async def get_document_by_chunk_id( unique_identifier_hash=document.unique_identifier_hash, created_at=document.created_at, updated_at=document.updated_at, - search_space_id=document.search_space_id, + workspace_id=document.workspace_id, chunks=windowed_chunks, total_chunks=total_chunks, chunk_start_index=start, @@ -1063,7 +1063,7 @@ async def get_document_by_chunk_id( @router.get("/documents/watched-folders", response_model=list[FolderRead]) async def get_watched_folders( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): @@ -1071,16 +1071,16 @@ async def get_watched_folders( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) folders = ( ( await session.execute( select(Folder).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.parent_id.is_(None), Folder.folder_metadata.isnot(None), Folder.folder_metadata["watched"].astext == "true", @@ -1126,9 +1126,9 @@ async def get_document_chunks_paginated( await check_permission( session, auth, - document.search_space_id, + document.workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) total_result = await session.execute( @@ -1171,7 +1171,7 @@ async def read_document( ): """ Get a specific document by ID. - Requires DOCUMENTS_READ permission for the search space. + Requires DOCUMENTS_READ permission for the workspace. """ try: result = await session.execute( @@ -1184,13 +1184,13 @@ async def read_document( status_code=404, detail=f"Document with id {document_id} not found" ) - # Check permission for the search space + # Check permission for the workspace await check_permission( session, auth, - document.search_space_id, + document.workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) raw_content = document.content or "" @@ -1205,7 +1205,7 @@ async def read_document( unique_identifier_hash=document.unique_identifier_hash, created_at=document.created_at, updated_at=document.updated_at, - search_space_id=document.search_space_id, + workspace_id=document.workspace_id, folder_id=document.folder_id, ) except HTTPException: @@ -1225,7 +1225,7 @@ async def update_document( ): """ Update a document. - Requires DOCUMENTS_UPDATE permission for the search space. + Requires DOCUMENTS_UPDATE permission for the workspace. """ try: result = await session.execute( @@ -1238,13 +1238,13 @@ async def update_document( status_code=404, detail=f"Document with id {document_id} not found" ) - # Check permission for the search space + # Check permission for the workspace await check_permission( session, auth, - db_document.search_space_id, + db_document.workspace_id, Permission.DOCUMENTS_UPDATE.value, - "You don't have permission to update documents in this search space", + "You don't have permission to update documents in this workspace", ) update_data = document_update.model_dump(exclude_unset=True) @@ -1264,7 +1264,7 @@ async def update_document( unique_identifier_hash=db_document.unique_identifier_hash, created_at=db_document.created_at, updated_at=db_document.updated_at, - search_space_id=db_document.search_space_id, + workspace_id=db_document.workspace_id, folder_id=db_document.folder_id, ) except HTTPException: @@ -1284,7 +1284,7 @@ async def delete_document( ): """ Delete a document. - Requires DOCUMENTS_DELETE permission for the search space. + Requires DOCUMENTS_DELETE permission for the workspace. Documents in "processing" state cannot be deleted. Heavy cascade deletion runs asynchronously via Celery so the API @@ -1313,13 +1313,13 @@ async def delete_document( detail="Document is already being deleted.", ) - # Check permission for the search space + # Check permission for the workspace await check_permission( session, auth, - document.search_space_id, + document.workspace_id, Permission.DOCUMENTS_DELETE.value, - "You don't have permission to delete documents in this search space", + "You don't have permission to delete documents in this workspace", ) # Mark the document as "deleting" so it's excluded from searches, @@ -1371,7 +1371,7 @@ async def list_document_versions( raise HTTPException(status_code=404, detail="Document not found") await check_permission( - session, user, document.search_space_id, Permission.DOCUMENTS_READ.value + session, user, document.workspace_id, Permission.DOCUMENTS_READ.value ) versions = ( @@ -1413,7 +1413,7 @@ async def get_document_version( raise HTTPException(status_code=404, detail="Document not found") await check_permission( - session, user, document.search_space_id, Permission.DOCUMENTS_READ.value + session, user, document.workspace_id, Permission.DOCUMENTS_READ.value ) version = ( @@ -1452,7 +1452,7 @@ async def restore_document_version( raise HTTPException(status_code=404, detail="Document not found") await check_permission( - session, user, document.search_space_id, Permission.DOCUMENTS_UPDATE.value + session, user, document.workspace_id, Permission.DOCUMENTS_UPDATE.value ) version = ( @@ -1503,20 +1503,20 @@ _MAX_MTIME_CHECK_FILES = 10_000 class FolderMtimeCheckRequest(PydanticBaseModel): folder_name: str - search_space_id: int + workspace_id: int files: list[FolderMtimeCheckFile] = Field(max_length=_MAX_MTIME_CHECK_FILES) class FolderUnlinkRequest(PydanticBaseModel): folder_name: str - search_space_id: int + workspace_id: int root_folder_id: int | None = None relative_paths: list[str] class FolderSyncFinalizeRequest(PydanticBaseModel): folder_name: str - search_space_id: int + workspace_id: int root_folder_id: int | None = None all_relative_paths: list[str] @@ -1537,16 +1537,16 @@ async def folder_mtime_check( await check_permission( session, auth, - request.search_space_id, + request.workspace_id, Permission.DOCUMENTS_CREATE.value, - "You don't have permission to create documents in this search space", + "You don't have permission to create documents in this workspace", ) uid_hashes = {} for f in request.files: uid = f"{request.folder_name}:{f.relative_path}" uid_hash = compute_identifier_hash( - DocumentType.LOCAL_FOLDER_FILE.value, uid, request.search_space_id + DocumentType.LOCAL_FOLDER_FILE.value, uid, request.workspace_id ) uid_hashes[uid_hash] = f @@ -1589,7 +1589,7 @@ async def folder_mtime_check( async def folder_upload( files: list[UploadFile], folder_name: str = Form(...), - search_space_id: int = Form(...), + workspace_id: int = Form(...), relative_paths: str = Form(...), root_folder_id: int | None = Form(None), use_vision_llm: bool = Form(False), @@ -1613,9 +1613,9 @@ async def folder_upload( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_CREATE.value, - "You don't have permission to create documents in this search space", + "You don't have permission to create documents in this workspace", ) if not files: @@ -1655,9 +1655,9 @@ async def folder_upload( if root_folder_id: root_folder = await session.get(Folder, root_folder_id) - if not root_folder or root_folder.search_space_id != search_space_id: + if not root_folder or root_folder.workspace_id != workspace_id: raise HTTPException( - status_code=404, detail="Root folder not found in this search space" + status_code=404, detail="Root folder not found in this workspace" ) if not root_folder_id: @@ -1671,7 +1671,7 @@ async def folder_upload( select(Folder).where( Folder.name == folder_name, Folder.parent_id.is_(None), - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, ) ) ).scalar_one_or_none() @@ -1682,7 +1682,7 @@ async def folder_upload( else: root_folder = Folder( name=folder_name, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=str(user.id), position="a0", folder_metadata=watched_metadata, @@ -1721,7 +1721,7 @@ async def folder_upload( ) index_uploaded_folder_files_task.delay( - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=str(user.id), folder_name=folder_name, root_folder_id=root_folder_id, @@ -1756,9 +1756,9 @@ async def folder_unlink( await check_permission( session, auth, - request.search_space_id, + request.workspace_id, Permission.DOCUMENTS_DELETE.value, - "You don't have permission to delete documents in this search space", + "You don't have permission to delete documents in this workspace", ) deleted_count = 0 @@ -1768,7 +1768,7 @@ async def folder_unlink( uid_hash = compute_identifier_hash( DocumentType.LOCAL_FOLDER_FILE.value, unique_id, - request.search_space_id, + request.workspace_id, ) existing = ( @@ -1813,9 +1813,9 @@ async def folder_sync_finalize( await check_permission( session, auth, - request.search_space_id, + request.workspace_id, Permission.DOCUMENTS_DELETE.value, - "You don't have permission to delete documents in this search space", + "You don't have permission to delete documents in this workspace", ) if not request.root_folder_id: @@ -1829,7 +1829,7 @@ async def folder_sync_finalize( uid_hash = compute_identifier_hash( DocumentType.LOCAL_FOLDER_FILE.value, unique_id, - request.search_space_id, + request.workspace_id, ) seen_hashes.add(uid_hash) @@ -1838,7 +1838,7 @@ async def folder_sync_finalize( await session.execute( select(Document).where( Document.document_type == DocumentType.LOCAL_FOLDER_FILE, - Document.search_space_id == request.search_space_id, + Document.workspace_id == request.workspace_id, Document.folder_id.in_(subtree_ids), ) ) @@ -1866,7 +1866,7 @@ async def folder_sync_finalize( await _cleanup_empty_folders( session, request.root_folder_id, - request.search_space_id, + request.workspace_id, existing_dirs, folder_mapping, subtree_ids=subtree_ids, diff --git a/surfsense_backend/app/routes/dropbox_add_connector_route.py b/surfsense_backend/app/routes/dropbox_add_connector_route.py index 6a9284371..d8b6d2072 100644 --- a/surfsense_backend/app/routes/dropbox_add_connector_route.py +++ b/surfsense_backend/app/routes/dropbox_add_connector_route.py @@ -36,7 +36,7 @@ from app.utils.connector_naming import ( generate_unique_connector_name, ) from app.utils.oauth_security import OAuthStateManager, TokenEncryption -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access logger = logging.getLogger(__name__) router = APIRouter() @@ -124,7 +124,7 @@ async def reauth_dropbox( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.DROPBOX_CONNECTOR, ) @@ -304,7 +304,7 @@ async def dropbox_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.DROPBOX_CONNECTOR, ) @@ -372,7 +372,7 @@ async def dropbox_callback( connector_type=SearchSourceConnectorType.DROPBOX_CONNECTOR, is_indexable=True, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) @@ -431,7 +431,7 @@ async def list_dropbox_folders( status_code=404, detail="Dropbox connector not found or access denied" ) - await check_search_space_access(session, auth, connector.search_space_id) + await check_workspace_access(session, auth, connector.workspace_id) dropbox_client = DropboxClient(session, connector_id) items, error = await list_folder_contents(dropbox_client, path=parent_path) diff --git a/surfsense_backend/app/routes/editor_routes.py b/surfsense_backend/app/routes/editor_routes.py index 0bc1dd45f..189e9b433 100644 --- a/surfsense_backend/app/routes/editor_routes.py +++ b/surfsense_backend/app/routes/editor_routes.py @@ -43,9 +43,9 @@ EDITOR_PLATE_MAX_BYTES = 1 * 1024 * 1024 EDITOR_PLATE_MAX_LINES = 5000 -@router.get("/search-spaces/{search_space_id}/documents/{document_id}/editor-content") +@router.get("/workspaces/{workspace_id}/documents/{document_id}/editor-content") async def get_editor_content( - search_space_id: int, + workspace_id: int, document_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -62,15 +62,15 @@ async def get_editor_content( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) result = await session.execute( select(Document).filter( Document.id == document_id, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) document = result.scalars().first() @@ -173,10 +173,10 @@ async def get_editor_content( @router.get( - "/search-spaces/{search_space_id}/documents/{document_id}/download-markdown" + "/workspaces/{workspace_id}/documents/{document_id}/download-markdown" ) async def download_document_markdown( - search_space_id: int, + workspace_id: int, document_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -188,15 +188,15 @@ async def download_document_markdown( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) result = await session.execute( select(Document).filter( Document.id == document_id, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) document = result.scalars().first() @@ -239,9 +239,9 @@ async def download_document_markdown( ) -@router.post("/search-spaces/{search_space_id}/documents/{document_id}/save") +@router.post("/workspaces/{workspace_id}/documents/{document_id}/save") async def save_document( - search_space_id: int, + workspace_id: int, document_id: int, data: dict[str, Any], session: AsyncSession = Depends(get_async_session), @@ -262,15 +262,15 @@ async def save_document( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_UPDATE.value, - "You don't have permission to update documents in this search space", + "You don't have permission to update documents in this workspace", ) result = await session.execute( select(Document).filter( Document.id == document_id, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) document = result.scalars().first() @@ -324,9 +324,9 @@ async def save_document( } -@router.get("/search-spaces/{search_space_id}/documents/{document_id}/export") +@router.get("/workspaces/{workspace_id}/documents/{document_id}/export") async def export_document( - search_space_id: int, + workspace_id: int, document_id: int, format: ExportFormat = Query( ExportFormat.PDF, @@ -339,15 +339,15 @@ async def export_document( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) result = await session.execute( select(Document).filter( Document.id == document_id, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) document = result.scalars().first() diff --git a/surfsense_backend/app/routes/export_routes.py b/surfsense_backend/app/routes/export_routes.py index 70df33b2e..19d4d301e 100644 --- a/surfsense_backend/app/routes/export_routes.py +++ b/surfsense_backend/app/routes/export_routes.py @@ -18,9 +18,9 @@ logger = logging.getLogger(__name__) router = APIRouter() -@router.get("/search-spaces/{search_space_id}/export") +@router.get("/workspaces/{workspace_id}/export") async def export_knowledge_base( - search_space_id: int, + workspace_id: int, folder_id: int | None = Query( None, description="Export only this folder's subtree" ), @@ -31,13 +31,13 @@ async def export_knowledge_base( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to export documents in this search space", + "You don't have permission to export documents in this workspace", ) try: - result = await build_export_zip(session, search_space_id, folder_id) + result = await build_export_zip(session, workspace_id, folder_id) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) from None diff --git a/surfsense_backend/app/routes/folders_routes.py b/surfsense_backend/app/routes/folders_routes.py index 1da0c9b0e..af4dda737 100644 --- a/surfsense_backend/app/routes/folders_routes.py +++ b/surfsense_backend/app/routes/folders_routes.py @@ -42,32 +42,32 @@ async def create_folder( await check_permission( session, auth, - request.search_space_id, + request.workspace_id, Permission.DOCUMENTS_CREATE.value, - "You don't have permission to create folders in this search space", + "You don't have permission to create folders in this workspace", ) if request.parent_id is not None: parent = await session.get(Folder, request.parent_id) if not parent: raise HTTPException(status_code=404, detail="Parent folder not found") - if parent.search_space_id != request.search_space_id: + if parent.workspace_id != request.workspace_id: raise HTTPException( status_code=400, - detail="Parent folder belongs to a different search space", + detail="Parent folder belongs to a different workspace", ) await validate_folder_depth(session, request.parent_id) position = await generate_folder_position( - session, request.search_space_id, request.parent_id + session, request.workspace_id, request.parent_id ) folder = Folder( name=request.name, position=position, parent_id=request.parent_id, - search_space_id=request.search_space_id, + workspace_id=request.workspace_id, created_by_id=user.id, ) session.add(folder) @@ -91,23 +91,23 @@ async def create_folder( @router.get("/folders", response_model=list[FolderRead]) async def list_folders( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): - """List all folders in a search space (flat). Requires DOCUMENTS_READ permission.""" + """List all folders in a workspace (flat). Requires DOCUMENTS_READ permission.""" try: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read folders in this search space", + "You don't have permission to read folders in this workspace", ) result = await session.execute( select(Folder) - .where(Folder.search_space_id == search_space_id) + .where(Folder.workspace_id == workspace_id) .order_by(Folder.position) ) return result.scalars().all() @@ -135,9 +135,9 @@ async def get_folder( await check_permission( session, auth, - folder.search_space_id, + folder.workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read folders in this search space", + "You don't have permission to read folders in this workspace", ) return folder @@ -165,9 +165,9 @@ async def get_folder_breadcrumb( await check_permission( session, auth, - folder.search_space_id, + folder.workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read folders in this search space", + "You don't have permission to read folders in this workspace", ) result = await session.execute( @@ -208,9 +208,9 @@ async def stop_watching_folder( await check_permission( session, auth, - folder.search_space_id, + folder.workspace_id, Permission.DOCUMENTS_UPDATE.value, - "You don't have permission to update folders in this search space", + "You don't have permission to update folders in this workspace", ) if folder.folder_metadata and isinstance(folder.folder_metadata, dict): @@ -237,9 +237,9 @@ async def update_folder( await check_permission( session, auth, - folder.search_space_id, + folder.workspace_id, Permission.DOCUMENTS_UPDATE.value, - "You don't have permission to update folders in this search space", + "You don't have permission to update folders in this workspace", ) folder.name = request.name @@ -277,9 +277,9 @@ async def move_folder( await check_permission( session, auth, - folder.search_space_id, + folder.workspace_id, Permission.DOCUMENTS_UPDATE.value, - "You don't have permission to move folders in this search space", + "You don't have permission to move folders in this workspace", ) if request.new_parent_id is not None: @@ -288,10 +288,10 @@ async def move_folder( raise HTTPException( status_code=404, detail="Target parent folder not found" ) - if new_parent.search_space_id != folder.search_space_id: + if new_parent.workspace_id != folder.workspace_id: raise HTTPException( status_code=400, - detail="Cannot move folder to a different search space", + detail="Cannot move folder to a different workspace", ) await check_no_circular_reference(session, folder_id, request.new_parent_id) @@ -299,7 +299,7 @@ async def move_folder( await validate_folder_depth(session, request.new_parent_id, subtree_depth) position = await generate_folder_position( - session, folder.search_space_id, request.new_parent_id + session, folder.workspace_id, request.new_parent_id ) folder.parent_id = request.new_parent_id folder.position = position @@ -337,14 +337,14 @@ async def reorder_folder( await check_permission( session, auth, - folder.search_space_id, + folder.workspace_id, Permission.DOCUMENTS_UPDATE.value, - "You don't have permission to reorder folders in this search space", + "You don't have permission to reorder folders in this workspace", ) position = await generate_folder_position( session, - folder.search_space_id, + folder.workspace_id, folder.parent_id, before_position=request.before_position, after_position=request.after_position, @@ -378,9 +378,9 @@ async def delete_folder( await check_permission( session, auth, - folder.search_space_id, + folder.workspace_id, Permission.DOCUMENTS_DELETE.value, - "You don't have permission to delete folders in this search space", + "You don't have permission to delete folders in this workspace", ) subtree_ids = await get_folder_subtree_ids(session, folder_id) @@ -455,19 +455,19 @@ async def move_document( await check_permission( session, auth, - document.search_space_id, + document.workspace_id, Permission.DOCUMENTS_UPDATE.value, - "You don't have permission to move documents in this search space", + "You don't have permission to move documents in this workspace", ) if request.folder_id is not None: target = await session.get(Folder, request.folder_id) if not target: raise HTTPException(status_code=404, detail="Target folder not found") - if target.search_space_id != document.search_space_id: + if target.workspace_id != document.workspace_id: raise HTTPException( status_code=400, - detail="Cannot move document to a folder in a different search space", + detail="Cannot move document to a folder in a different workspace", ) document.folder_id = request.folder_id @@ -502,14 +502,14 @@ async def bulk_move_documents( if not documents: raise HTTPException(status_code=404, detail="No documents found") - search_space_ids = {doc.search_space_id for doc in documents} - for ss_id in search_space_ids: + workspace_ids = {doc.workspace_id for doc in documents} + for ss_id in workspace_ids: await check_permission( session, auth, ss_id, Permission.DOCUMENTS_UPDATE.value, - "You don't have permission to move documents in this search space", + "You don't have permission to move documents in this workspace", ) if request.folder_id is not None: @@ -519,12 +519,12 @@ async def bulk_move_documents( mismatched = [ doc.id for doc in documents - if doc.search_space_id != target.search_space_id + if doc.workspace_id != target.workspace_id ] if mismatched: raise HTTPException( status_code=400, - detail="Cannot move documents to a folder in a different search space", + detail="Cannot move documents to a folder in a different workspace", ) for doc in documents: diff --git a/surfsense_backend/app/routes/gateway_webhook_routes.py b/surfsense_backend/app/routes/gateway_webhook_routes.py index 931794059..f09186295 100644 --- a/surfsense_backend/app/routes/gateway_webhook_routes.py +++ b/surfsense_backend/app/routes/gateway_webhook_routes.py @@ -53,7 +53,7 @@ from app.observability.metrics import ( ) from app.users import get_auth_context from app.utils.oauth_security import OAuthStateManager, TokenEncryption -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access router = APIRouter(prefix="/gateway", tags=["gateway"]) config_router = APIRouter(prefix="/gateway", tags=["gateway"]) @@ -170,7 +170,7 @@ def _slack_event_kind(payload: dict[str, Any]) -> str: class StartBindingRequest(BaseModel): platform: ExternalChatPlatform = ExternalChatPlatform.TELEGRAM - search_space_id: int + workspace_id: int class StartBindingResponse(BaseModel): @@ -180,12 +180,12 @@ class StartBindingResponse(BaseModel): expires_at: datetime -class UpdateBindingSearchSpaceRequest(BaseModel): - search_space_id: int +class UpdateBindingWorkspaceRequest(BaseModel): + workspace_id: int -class UpdateAccountSearchSpaceRequest(BaseModel): - search_space_id: int +class UpdateAccountWorkspaceRequest(BaseModel): + workspace_id: int def _active_whatsapp_account_mode() -> ExternalChatAccountMode | None: @@ -249,7 +249,7 @@ def _telegram_message(payload: dict[str, Any]) -> dict[str, Any] | None: @router.get("/slack/install") async def install_slack_gateway( - search_space_id: int, + workspace_id: int, auth: AuthContext = Depends(get_auth_context), session: AsyncSession = Depends(get_async_session), ) -> dict[str, str]: @@ -258,8 +258,8 @@ async def install_slack_gateway( raise HTTPException( status_code=500, detail="Slack gateway OAuth is not configured" ) - await check_search_space_access(session, auth, search_space_id) - state = _get_state_manager().generate_secure_state(search_space_id, user.id) + await check_workspace_access(session, auth, workspace_id) + state = _get_state_manager().generate_secure_state(workspace_id, user.id) auth_params = { "client_id": config.GATEWAY_SLACK_CLIENT_ID, "scope": ",".join(SLACK_BOT_SCOPES), @@ -382,7 +382,7 @@ async def slack_gateway_callback( ExternalChatBinding( account_id=account.id, user_id=user_id, - search_space_id=space_id, + workspace_id=space_id, state=ExternalChatBindingState.BOUND, external_peer_id=peer_id, external_peer_kind=ExternalChatPeerKind.DIRECT, @@ -395,7 +395,7 @@ async def slack_gateway_callback( ) ) elif binding.user_id == user_id: - binding.search_space_id = space_id + binding.workspace_id = space_id binding.external_metadata = { **(binding.external_metadata or {}), "kind": "slack_user", @@ -409,7 +409,7 @@ async def slack_gateway_callback( @router.get("/discord/install") async def install_discord_gateway( - search_space_id: int, + workspace_id: int, auth: AuthContext = Depends(get_auth_context), session: AsyncSession = Depends(get_async_session), ) -> dict[str, str]: @@ -418,8 +418,8 @@ async def install_discord_gateway( raise HTTPException( status_code=500, detail="Discord gateway OAuth is not configured" ) - await check_search_space_access(session, auth, search_space_id) - state = _get_state_manager().generate_secure_state(search_space_id, user.id) + await check_workspace_access(session, auth, workspace_id) + state = _get_state_manager().generate_secure_state(workspace_id, user.id) auth_params = { "client_id": config.DISCORD_CLIENT_ID, "scope": " ".join(DISCORD_GATEWAY_SCOPES), @@ -559,7 +559,7 @@ async def discord_gateway_callback( ExternalChatBinding( account_id=account.id, user_id=user_id, - search_space_id=space_id, + workspace_id=space_id, state=ExternalChatBindingState.BOUND, external_peer_id=peer_id, external_peer_kind=ExternalChatPeerKind.DIRECT, @@ -568,7 +568,7 @@ async def discord_gateway_callback( ) ) elif binding.user_id == user_id: - binding.search_space_id = space_id + binding.workspace_id = space_id binding.external_username = discord_username or binding.external_username binding.external_metadata = { **(binding.external_metadata or {}), @@ -718,7 +718,7 @@ async def start_binding( session: AsyncSession = Depends(get_async_session), ) -> StartBindingResponse: user = auth.user - await check_search_space_access(session, auth, body.search_space_id) + await check_workspace_access(session, auth, body.workspace_id) code = generate_pairing_code() if body.platform == ExternalChatPlatform.TELEGRAM: if not _telegram_gateway_enabled(): @@ -758,7 +758,7 @@ async def start_binding( binding = ExternalChatBinding( account_id=account.id, user_id=user.id, - search_space_id=body.search_space_id, + workspace_id=body.workspace_id, state=ExternalChatBindingState.PENDING, pairing_code=code, pairing_code_expires_at=expires_at, @@ -794,7 +794,7 @@ async def list_bindings( "id": binding.id, "platform": account.platform.value, "state": binding.state.value, - "search_space_id": binding.search_space_id, + "workspace_id": binding.workspace_id, "external_display_name": binding.external_display_name, "external_username": binding.external_username, "external_metadata": binding.external_metadata, @@ -869,7 +869,7 @@ async def list_connections( workspace_id = None route_type = "binding" connection_id = binding.id - search_space_id = binding.search_space_id + workspace_id = binding.workspace_id display_name = binding.external_display_name or binding.external_username if account.platform == ExternalChatPlatform.SLACK: workspace_name = account_state.get("team_name") @@ -886,8 +886,8 @@ async def list_connections( baileys_account_ids.add(int(account.id)) route_type = "account" connection_id = account.id - search_space_id = ( - account.owner_search_space_id or binding.search_space_id + workspace_id = ( + account.owner_workspace_id or binding.workspace_id ) display_name = "WhatsApp Bridge" @@ -899,7 +899,7 @@ async def list_connections( "platform": account.platform.value, "mode": account.mode.value, "state": binding.state.value, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "display_name": display_name or workspace_name, "external_username": ( None @@ -921,7 +921,7 @@ async def list_connections( ExternalChatAccount.owner_user_id == user.id, ExternalChatAccount.platform == ExternalChatPlatform.WHATSAPP, ExternalChatAccount.mode == ExternalChatAccountMode.SELF_HOST_BYO, - ExternalChatAccount.owner_search_space_id.is_not(None), + ExternalChatAccount.owner_workspace_id.is_not(None), ) ) for account in account_result.scalars(): @@ -936,7 +936,7 @@ async def list_connections( "platform": account.platform.value, "mode": account.mode.value, "state": "bound", - "search_space_id": account.owner_search_space_id, + "workspace_id": account.owner_workspace_id, "display_name": "WhatsApp Bridge", "external_username": None, "workspace_name": account_state.get("display_phone_number"), @@ -995,10 +995,10 @@ async def get_gateway_config( } -@router.patch("/bindings/{binding_id}/search-space") -async def update_binding_search_space( +@router.patch("/bindings/{binding_id}/workspace") +async def update_binding_workspace( binding_id: int, - body: UpdateBindingSearchSpaceRequest, + body: UpdateBindingWorkspaceRequest, auth: AuthContext = Depends(get_auth_context), session: AsyncSession = Depends(get_async_session), ) -> dict[str, bool]: @@ -1017,19 +1017,19 @@ async def update_binding_search_space( if account is None or _is_inactive_whatsapp_account(account): raise HTTPException(status_code=404, detail="Binding not found") - await check_search_space_access(session, auth, body.search_space_id) - if binding.search_space_id != body.search_space_id: - binding.search_space_id = body.search_space_id + await check_workspace_access(session, auth, body.workspace_id) + if binding.workspace_id != body.workspace_id: + binding.workspace_id = body.workspace_id binding.new_chat_thread_id = None binding.updated_at = datetime.now(UTC) await session.commit() return {"ok": True} -@router.patch("/accounts/{account_id}/search-space") -async def update_gateway_account_search_space( +@router.patch("/accounts/{account_id}/workspace") +async def update_gateway_account_workspace( account_id: int, - body: UpdateAccountSearchSpaceRequest, + body: UpdateAccountWorkspaceRequest, auth: AuthContext = Depends(get_auth_context), session: AsyncSession = Depends(get_async_session), ) -> dict[str, bool]: @@ -1044,8 +1044,8 @@ async def update_gateway_account_search_space( ): raise HTTPException(status_code=404, detail="Gateway account not found") - await check_search_space_access(session, auth, body.search_space_id) - account.owner_search_space_id = body.search_space_id + await check_workspace_access(session, auth, body.workspace_id) + account.owner_workspace_id = body.workspace_id account.updated_at = datetime.now(UTC) result = await session.execute( @@ -1058,7 +1058,7 @@ async def update_gateway_account_search_space( ) ) for binding in result.scalars(): - binding.search_space_id = body.search_space_id + binding.workspace_id = body.workspace_id binding.new_chat_thread_id = None binding.updated_at = datetime.now(UTC) @@ -1113,7 +1113,7 @@ async def delete_gateway_account( for binding in result.scalars(): revoke_binding(binding) - account.owner_search_space_id = None + account.owner_workspace_id = None account.suspended_at = datetime.now(UTC) account.suspended_reason = "disconnected" account.updated_at = datetime.now(UTC) diff --git a/surfsense_backend/app/routes/gateway_whatsapp_baileys_routes.py b/surfsense_backend/app/routes/gateway_whatsapp_baileys_routes.py index 95c8fe12b..eb2c60d50 100644 --- a/surfsense_backend/app/routes/gateway_whatsapp_baileys_routes.py +++ b/surfsense_backend/app/routes/gateway_whatsapp_baileys_routes.py @@ -22,13 +22,13 @@ from app.db import ( ) from app.gateway.whatsapp.adapter_baileys import WhatsAppBaileysAdapter from app.users import get_auth_context -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access router = APIRouter(prefix="/gateway/whatsapp/baileys", tags=["gateway"]) class BaileysPairRequest(BaseModel): - search_space_id: int + workspace_id: int phone_number: str @@ -66,7 +66,7 @@ async def request_pairing_code( ) -> dict[str, Any]: user = auth.user _ensure_baileys_enabled() - await check_search_space_access(session, auth, body.search_space_id) + await check_workspace_access(session, auth, body.workspace_id) adapter = WhatsAppBaileysAdapter() try: pairing = await adapter.request_pairing_code(phone_number=body.phone_number) @@ -79,7 +79,7 @@ async def request_pairing_code( platform=ExternalChatPlatform.WHATSAPP, mode=ExternalChatAccountMode.SELF_HOST_BYO, owner_user_id=user.id, - owner_search_space_id=body.search_space_id, + owner_workspace_id=body.workspace_id, is_system_account=False, cursor_state={}, health_status=ExternalChatHealthStatus.UNKNOWN, @@ -87,7 +87,7 @@ async def request_pairing_code( session.add(account) else: account.mode = ExternalChatAccountMode.SELF_HOST_BYO - account.owner_search_space_id = body.search_space_id + account.owner_workspace_id = body.workspace_id account.health_status = ExternalChatHealthStatus.UNKNOWN account.suspended_at = None account.suspended_reason = None 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 8789287b8..793a255cb 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -141,7 +141,7 @@ async def reauth_calendar( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, ) @@ -286,7 +286,7 @@ async def calendar_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, ) @@ -343,7 +343,7 @@ async def calendar_callback( name=connector_name, connector_type=SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, config=creds_dict, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, is_indexable=False, ) 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 c97c82eb0..a82a41ae1 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -46,7 +46,7 @@ from app.utils.oauth_security import ( TokenEncryption, generate_code_verifier, ) -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access # Relax token scope validation for Google OAuth os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1" @@ -119,7 +119,7 @@ async def connect_drive( Initiate Google Drive OAuth flow. Query params: - space_id: Search space ID to add connector to + space_id: Workspace ID to add connector to Returns: JSON with auth_url to redirect user to Google authorization @@ -177,7 +177,7 @@ async def reauth_drive( Initiate Google Drive re-authentication to upgrade OAuth scopes. Query params: - space_id: Search space ID the connector belongs to + space_id: Workspace ID the connector belongs to connector_id: ID of the existing connector to re-authenticate Returns: @@ -189,7 +189,7 @@ async def reauth_drive( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, ) @@ -349,7 +349,7 @@ async def drive_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, ) @@ -415,7 +415,7 @@ async def drive_callback( **creds_dict, "start_page_token": None, # Will be set on first index }, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, is_indexable=True, ) @@ -517,7 +517,7 @@ async def list_google_drive_folders( detail="Google Drive connector not found or access denied", ) - await check_search_space_access(session, auth, connector.search_space_id) + await check_workspace_access(session, auth, connector.workspace_id) # Initialize Drive client (credentials will be loaded on first API call) drive_client = GoogleDriveClient(session, connector_id) 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 82475c792..c7b0fef9c 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -100,7 +100,7 @@ async def connect_gmail( Initiate Google Gmail OAuth flow. Query params: - space_id: Search space ID to add connector to + space_id: Workspace ID to add connector to Returns: JSON with auth_url to redirect user to Google authorization @@ -159,7 +159,7 @@ async def reauth_gmail( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, ) @@ -317,7 +317,7 @@ async def gmail_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, ) @@ -374,7 +374,7 @@ async def gmail_callback( name=connector_name, connector_type=SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, config=creds_dict, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, is_indexable=False, ) diff --git a/surfsense_backend/app/routes/image_generation_routes.py b/surfsense_backend/app/routes/image_generation_routes.py index 96cb3825c..5a6c71127 100644 --- a/surfsense_backend/app/routes/image_generation_routes.py +++ b/surfsense_backend/app/routes/image_generation_routes.py @@ -22,8 +22,8 @@ from app.db import ( ImageGeneration, Model, Permission, - SearchSpace, - SearchSpaceMembership, + Workspace, + WorkspaceMembership, get_async_session, ) from app.schemas import ( @@ -68,7 +68,7 @@ def _get_global_connection(connection_id: int) -> dict | None: async def _resolve_billing_for_image_gen( session: AsyncSession, config_id: int | None, - search_space: SearchSpace, + workspace: Workspace, ) -> tuple[str, str, int]: """Resolve ``(billing_tier, base_model, reserve_micros)`` for a request. @@ -84,18 +84,18 @@ async def _resolve_billing_for_image_gen( """ resolved_id = config_id if resolved_id is None: - resolved_id = search_space.image_gen_model_id or IMAGE_GEN_AUTO_MODE_ID + resolved_id = workspace.image_gen_model_id or IMAGE_GEN_AUTO_MODE_ID if is_image_gen_auto_mode(resolved_id): candidates = await auto_model_candidates( session, - search_space_id=search_space.id, - user_id=search_space.user_id, + workspace_id=workspace.id, + user_id=workspace.user_id, capability="image_gen", ) if not candidates: return ("free", "auto", DEFAULT_IMAGE_RESERVE_MICROS) - selected = choose_auto_model_candidate(candidates, search_space.id) + selected = choose_auto_model_candidate(candidates, workspace.id) resolved_id = int(selected["id"]) if resolved_id < 0: @@ -119,19 +119,19 @@ async def _resolve_billing_for_image_gen( async def _execute_image_generation( session: AsyncSession, image_gen: ImageGeneration, - search_space: SearchSpace, + workspace: Workspace, ) -> None: """ Call litellm.aimage_generation() with the appropriate config. Resolution order: 1. Explicit image_gen_model_id on the request - 2. Search space's image_gen_model_id preference + 2. Workspace's image_gen_model_id preference 3. Falls back to Auto mode if available """ config_id = image_gen.image_gen_model_id if config_id is None: - config_id = search_space.image_gen_model_id or IMAGE_GEN_AUTO_MODE_ID + config_id = workspace.image_gen_model_id or IMAGE_GEN_AUTO_MODE_ID image_gen.image_gen_model_id = config_id # Build kwargs @@ -150,13 +150,13 @@ async def _execute_image_generation( if is_image_gen_auto_mode(config_id): candidates = await auto_model_candidates( session, - search_space_id=search_space.id, - user_id=search_space.user_id, + workspace_id=workspace.id, + user_id=workspace.user_id, capability="image_gen", ) if not candidates: raise ValueError("No image-generation models are available for Auto mode") - config_id = int(choose_auto_model_candidate(candidates, search_space.id)["id"]) + config_id = int(choose_auto_model_candidate(candidates, workspace.id)["id"]) image_gen.image_gen_model_id = config_id if config_id < 0: @@ -191,9 +191,9 @@ async def _execute_image_generation( if not db_model or not db_model.connection or not db_model.connection.enabled: raise ValueError(f"Image generation model {config_id} not found") conn = db_model.connection - if conn.search_space_id is not None and conn.search_space_id != search_space.id: + if conn.workspace_id is not None and conn.workspace_id != workspace.id: raise ValueError(f"Image generation model {config_id} not found") - if conn.user_id is not None and conn.user_id != search_space.user_id: + if conn.user_id is not None and conn.user_id != workspace.user_id: raise ValueError(f"Image generation model {config_id} not found") if not has_capability(db_model, "image_gen"): raise ValueError(f"Model {config_id} is not image-generation capable") @@ -254,7 +254,7 @@ async def create_image_generation( Premium configs are gated by the user's shared premium credit pool. The flow is: - 1. Permission check + load the search space (cheap, no provider call). + 1. Permission check + load the workspace (cheap, no provider call). 2. Resolve which config will run so we know its billing tier and the worst-case reservation size *before* opening any DB rows. 3. Wrap the entire ImageGeneration row insert + provider call in @@ -273,20 +273,20 @@ async def create_image_generation( await check_permission( session, auth, - data.search_space_id, + data.workspace_id, Permission.IMAGE_GENERATIONS_CREATE.value, - "You don't have permission to create image generations in this search space", + "You don't have permission to create image generations in this workspace", ) result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == data.search_space_id) + select(Workspace).filter(Workspace.id == data.workspace_id) ) - search_space = result.scalars().first() - if not search_space: - raise HTTPException(status_code=404, detail="Search space not found") + workspace = result.scalars().first() + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") billing_tier, base_model, reserve_micros = await _resolve_billing_for_image_gen( - session, data.image_gen_model_id, search_space + session, data.image_gen_model_id, workspace ) # billable_call runs OUTSIDE the inner try/except so QuotaInsufficientError @@ -296,8 +296,8 @@ async def create_image_generation( # exists when none does, and (2) return HTTP 200 to a client # whose request was actively *denied* (issue K). async with billable_call( - user_id=search_space.user_id, - search_space_id=data.search_space_id, + user_id=workspace.user_id, + workspace_id=data.workspace_id, billing_tier=billing_tier, base_model=base_model, quota_reserve_micros_override=reserve_micros, @@ -313,14 +313,14 @@ async def create_image_generation( style=data.style, response_format=data.response_format, image_gen_model_id=data.image_gen_model_id, - search_space_id=data.search_space_id, + workspace_id=data.workspace_id, created_by_id=user.id, ) session.add(db_image_gen) await session.flush() try: - await _execute_image_generation(session, db_image_gen, search_space) + await _execute_image_generation(session, db_image_gen, workspace) except Exception as e: logger.exception("Image generation call failed") db_image_gen.error_message = str(e) @@ -363,7 +363,7 @@ async def create_image_generation( @router.get("/image-generations", response_model=list[ImageGenerationListRead]) async def list_image_generations( - search_space_id: int | None = None, + workspace_id: int | None = None, skip: int = 0, limit: int = 50, session: AsyncSession = Depends(get_async_session), @@ -377,17 +377,17 @@ async def list_image_generations( limit = 100 try: - if search_space_id is not None: + if workspace_id is not None: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.IMAGE_GENERATIONS_READ.value, - "You don't have permission to read image generations in this search space", + "You don't have permission to read image generations in this workspace", ) result = await session.execute( select(ImageGeneration) - .filter(ImageGeneration.search_space_id == search_space_id) + .filter(ImageGeneration.workspace_id == workspace_id) .order_by(ImageGeneration.created_at.desc()) .offset(skip) .limit(limit) @@ -395,9 +395,9 @@ async def list_image_generations( else: result = await session.execute( select(ImageGeneration) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) .order_by(ImageGeneration.created_at.desc()) .offset(skip) .limit(limit) @@ -434,9 +434,9 @@ async def get_image_generation( await check_permission( session, auth, - image_gen.search_space_id, + image_gen.workspace_id, Permission.IMAGE_GENERATIONS_READ.value, - "You don't have permission to read image generations in this search space", + "You don't have permission to read image generations in this workspace", ) return image_gen @@ -466,9 +466,9 @@ async def delete_image_generation( await check_permission( session, auth, - db_image_gen.search_space_id, + db_image_gen.workspace_id, Permission.IMAGE_GENERATIONS_DELETE.value, - "You don't have permission to delete image generations in this search space", + "You don't have permission to delete image generations in this workspace", ) await session.delete(db_image_gen) @@ -500,8 +500,8 @@ async def serve_generated_image( Serve a generated image by ID, protected by a signed token. The token is generated when the image URL is created by the generate_image - tool and encodes the image_gen_id, search_space_id, and an expiry timestamp. - This ensures only users with access to the search space can view images, + tool and encodes the image_gen_id, workspace_id, and an expiry timestamp. + This ensures only users with access to the workspace can view images, without requiring auth headers (which tags cannot pass). Args: diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index c29d0609b..c82363daa 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -83,7 +83,7 @@ async def connect_jira( Initiate Jira OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -326,7 +326,7 @@ async def jira_callback( sa_select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR, ) @@ -392,7 +392,7 @@ async def jira_callback( connector_type=SearchSourceConnectorType.JIRA_CONNECTOR, is_indexable=False, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) @@ -454,7 +454,7 @@ async def reauth_jira( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR, ) diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index 1d7cc172f..f29d8fcae 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -87,7 +87,7 @@ async def connect_linear( Initiate Linear OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -148,7 +148,7 @@ async def reauth_linear( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR, ) @@ -346,7 +346,7 @@ async def linear_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR, ) @@ -406,7 +406,7 @@ async def linear_callback( connector_type=SearchSourceConnectorType.LINEAR_CONNECTOR, is_indexable=False, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) diff --git a/surfsense_backend/app/routes/logs_routes.py b/surfsense_backend/app/routes/logs_routes.py index 28c3e4fd1..355afece2 100644 --- a/surfsense_backend/app/routes/logs_routes.py +++ b/surfsense_backend/app/routes/logs_routes.py @@ -11,8 +11,8 @@ from app.db import ( LogLevel, LogStatus, Permission, - SearchSpace, - SearchSpaceMembership, + Workspace, + WorkspaceMembership, get_async_session, ) from app.schemas import LogCreate, LogRead, LogUpdate @@ -33,13 +33,13 @@ async def create_log( Note: This is typically called internally. Requires LOGS_READ permission (since logs are usually system-generated). """ try: - # Check if the user has access to the search space + # Check if the user has access to the workspace await check_permission( session, auth, - log.search_space_id, + log.workspace_id, Permission.LOGS_READ.value, - "You don't have permission to access logs in this search space", + "You don't have permission to access logs in this workspace", ) db_log = Log(**log.model_dump()) @@ -60,7 +60,7 @@ async def create_log( async def read_logs( skip: int = 0, limit: int = 100, - search_space_id: int | None = None, + workspace_id: int | None = None, level: LogLevel | None = None, status: LogStatus | None = None, source: str | None = None, @@ -72,34 +72,34 @@ async def read_logs( user = auth.user """ Get logs with optional filtering. - Requires LOGS_READ permission for the search space(s). + Requires LOGS_READ permission for the workspace(s). """ try: # Apply filters filters = [] - if search_space_id is not None: - # Check permission for specific search space + if workspace_id is not None: + # Check permission for specific workspace await check_permission( session, auth, - search_space_id, + workspace_id, Permission.LOGS_READ.value, - "You don't have permission to read logs in this search space", + "You don't have permission to read logs in this workspace", ) - # Build query for specific search space + # Build query for specific workspace query = ( select(Log) - .filter(Log.search_space_id == search_space_id) + .filter(Log.workspace_id == workspace_id) .order_by(desc(Log.created_at)) ) else: - # Build base query - logs from search spaces user has membership in + # Build base query - logs from workspaces user has membership in query = ( select(Log) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) .order_by(desc(Log.created_at)) ) @@ -141,7 +141,7 @@ async def read_log( ): """ Get a specific log by ID. - Requires LOGS_READ permission for the search space. + Requires LOGS_READ permission for the workspace. """ try: result = await session.execute(select(Log).filter(Log.id == log_id)) @@ -150,13 +150,13 @@ async def read_log( if not log: raise HTTPException(status_code=404, detail="Log not found") - # Check permission for the search space + # Check permission for the workspace await check_permission( session, auth, - log.search_space_id, + log.workspace_id, Permission.LOGS_READ.value, - "You don't have permission to read logs in this search space", + "You don't have permission to read logs in this workspace", ) return log @@ -186,13 +186,13 @@ async def update_log( if not db_log: raise HTTPException(status_code=404, detail="Log not found") - # Check permission for the search space + # Check permission for the workspace await check_permission( session, auth, - db_log.search_space_id, + db_log.workspace_id, Permission.LOGS_READ.value, - "You don't have permission to access logs in this search space", + "You don't have permission to access logs in this workspace", ) # Update only provided fields @@ -220,7 +220,7 @@ async def delete_log( ): """ Delete a log entry. - Requires LOGS_DELETE permission for the search space. + Requires LOGS_DELETE permission for the workspace. """ try: result = await session.execute(select(Log).filter(Log.id == log_id)) @@ -229,13 +229,13 @@ async def delete_log( if not db_log: raise HTTPException(status_code=404, detail="Log not found") - # Check permission for the search space + # Check permission for the workspace await check_permission( session, auth, - db_log.search_space_id, + db_log.workspace_id, Permission.LOGS_DELETE.value, - "You don't have permission to delete logs in this search space", + "You don't have permission to delete logs in this workspace", ) await session.delete(db_log) @@ -250,25 +250,25 @@ async def delete_log( ) from e -@router.get("/logs/search-space/{search_space_id}/summary") +@router.get("/logs/workspaces/{workspace_id}/summary") async def get_logs_summary( - search_space_id: int, + workspace_id: int, hours: int = 24, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - Get a summary of logs for a search space in the last X hours. - Requires LOGS_READ permission for the search space. + Get a summary of logs for a workspace in the last X hours. + Requires LOGS_READ permission for the workspace. """ try: # Check permission await check_permission( session, auth, - search_space_id, + workspace_id, Permission.LOGS_READ.value, - "You don't have permission to read logs in this search space", + "You don't have permission to read logs in this workspace", ) # Calculate time window @@ -278,7 +278,7 @@ async def get_logs_summary( result = await session.execute( select(Log) .filter( - and_(Log.search_space_id == search_space_id, Log.created_at >= since) + and_(Log.workspace_id == workspace_id, Log.created_at >= since) ) .order_by(desc(Log.created_at)) ) diff --git a/surfsense_backend/app/routes/luma_add_connector_route.py b/surfsense_backend/app/routes/luma_add_connector_route.py index 9a6f18940..84bfa41ee 100644 --- a/surfsense_backend/app/routes/luma_add_connector_route.py +++ b/surfsense_backend/app/routes/luma_add_connector_route.py @@ -23,7 +23,7 @@ class AddLumaConnectorRequest(BaseModel): """Request model for adding a Luma connector.""" api_key: str = Field(..., description="Luma API key") - space_id: int = Field(..., description="Search space ID") + space_id: int = Field(..., description="Workspace ID") @router.post("/connectors/luma/add") @@ -48,10 +48,10 @@ async def add_luma_connector( """ user = auth.user try: - # Check if a Luma connector already exists for this search space and user + # Check if a Luma connector already exists for this workspace and user result = await session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == request.space_id, + SearchSourceConnector.workspace_id == request.space_id, SearchSourceConnector.user_id == user.id, SearchSourceConnector.connector_type == SearchSourceConnectorType.LUMA_CONNECTOR, @@ -81,7 +81,7 @@ async def add_luma_connector( name="Luma Event Connector", connector_type=SearchSourceConnectorType.LUMA_CONNECTOR, config={"api_key": request.api_key}, - search_space_id=request.space_id, + workspace_id=request.space_id, user_id=user.id, is_indexable=False, ) @@ -123,10 +123,10 @@ async def delete_luma_connector( session: AsyncSession = Depends(get_async_session), ): """ - Delete the Luma connector for the authenticated user in a specific search space. + Delete the Luma connector for the authenticated user in a specific workspace. Args: - space_id: Search space ID + space_id: Workspace ID user: Current authenticated user session: Database session @@ -140,7 +140,7 @@ async def delete_luma_connector( try: result = await session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.user_id == user.id, SearchSourceConnector.connector_type == SearchSourceConnectorType.LUMA_CONNECTOR, @@ -179,10 +179,10 @@ async def test_luma_connector( session: AsyncSession = Depends(get_async_session), ): """ - Test the Luma connector for the authenticated user in a specific search space. + Test the Luma connector for the authenticated user in a specific workspace. Args: - space_id: Search space ID + space_id: Workspace ID user: Current authenticated user session: Database session @@ -194,10 +194,10 @@ async def test_luma_connector( """ user = auth.user try: - # Get the Luma connector for this search space and user + # Get the Luma connector for this workspace and user result = await session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.user_id == user.id, SearchSourceConnector.connector_type == SearchSourceConnectorType.LUMA_CONNECTOR, diff --git a/surfsense_backend/app/routes/mcp_oauth_route.py b/surfsense_backend/app/routes/mcp_oauth_route.py index dbeb8738c..b894b0703 100644 --- a/surfsense_backend/app/routes/mcp_oauth_route.py +++ b/surfsense_backend/app/routes/mcp_oauth_route.py @@ -413,7 +413,7 @@ async def mcp_oauth_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == db_connector_type, ) ) @@ -468,7 +468,7 @@ async def mcp_oauth_callback( connector_type=db_connector_type, is_indexable=False, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) @@ -539,7 +539,7 @@ async def reauth_mcp_service( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == db_connector_type, ) ) diff --git a/surfsense_backend/app/routes/model_connections_routes.py b/surfsense_backend/app/routes/model_connections_routes.py index 84e9b830d..0623340b7 100644 --- a/surfsense_backend/app/routes/model_connections_routes.py +++ b/surfsense_backend/app/routes/model_connections_routes.py @@ -14,7 +14,7 @@ from app.db import ( ModelSource, NewChatThread, Permission, - SearchSpace, + Workspace, get_async_session, ) from app.schemas import ( @@ -88,7 +88,7 @@ def _connection_read( api_key=conn.api_key, extra=conn.extra or {}, scope=conn.scope, - search_space_id=conn.search_space_id, + workspace_id=conn.workspace_id, user_id=conn.user_id, enabled=conn.enabled, has_api_key=bool(conn.api_key), @@ -142,7 +142,7 @@ def _default_model_for(models: list[Model], capability: str) -> int | None: async def _load_role_model( session: AsyncSession, - search_space_id: int, + workspace_id: int, model_id: int, ) -> Model | dict | None: if model_id < 0: @@ -157,7 +157,7 @@ async def _load_role_model( .where(Model.id == model_id) ) model = result.scalars().first() - if model is None or model.connection.search_space_id != search_space_id: + if model is None or model.connection.workspace_id != workspace_id: return None return model @@ -171,14 +171,14 @@ def _role_model_enabled(model: Model | dict) -> bool: async def _validate_role_model_id( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, model_id: int | None, capability: str, ) -> int: if model_id is None or model_id == 0: return 0 - model = await _load_role_model(session, search_space_id, model_id) + model = await _load_role_model(session, workspace_id, model_id) if model and _role_model_enabled(model) and has_capability(model, capability): return model_id @@ -191,14 +191,14 @@ async def _validate_role_model_id( async def _resolve_role_model_id( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, model_id: int | None, capability: str, ) -> int: try: return await _validate_role_model_id( session, - search_space_id=search_space_id, + workspace_id=workspace_id, model_id=model_id, capability=capability, ) @@ -207,28 +207,28 @@ async def _resolve_role_model_id( async def _clear_invalid_roles( - session: AsyncSession, search_space_id: int -) -> SearchSpace: - search_space = await _get_search_space(session, search_space_id) - search_space.chat_model_id = await _resolve_role_model_id( + session: AsyncSession, workspace_id: int +) -> Workspace: + workspace = await _get_workspace(session, workspace_id) + workspace.chat_model_id = await _resolve_role_model_id( session, - search_space_id=search_space_id, - model_id=search_space.chat_model_id, + workspace_id=workspace_id, + model_id=workspace.chat_model_id, capability="chat", ) - search_space.vision_model_id = await _resolve_role_model_id( + workspace.vision_model_id = await _resolve_role_model_id( session, - search_space_id=search_space_id, - model_id=search_space.vision_model_id, + workspace_id=workspace_id, + model_id=workspace.vision_model_id, capability="vision", ) - search_space.image_gen_model_id = await _resolve_role_model_id( + workspace.image_gen_model_id = await _resolve_role_model_id( session, - search_space_id=search_space_id, - model_id=search_space.image_gen_model_id, + workspace_id=workspace_id, + model_id=workspace.image_gen_model_id, capability="image_gen", ) - return search_space + return workspace async def _default_unset_roles( @@ -236,24 +236,24 @@ async def _default_unset_roles( conn: Connection, models: list[Model], ) -> None: - if conn.scope != ConnectionScope.SEARCH_SPACE or conn.search_space_id is None: + if conn.scope != ConnectionScope.SEARCH_SPACE or conn.workspace_id is None: return - search_space = await _get_search_space(session, conn.search_space_id) - if search_space.chat_model_id is None: - search_space.chat_model_id = _default_model_for(models, "chat") - if search_space.vision_model_id is None: + workspace = await _get_workspace(session, conn.workspace_id) + if workspace.chat_model_id is None: + workspace.chat_model_id = _default_model_for(models, "chat") + if workspace.vision_model_id is None: vision_default = None - if search_space.chat_model_id: + if workspace.chat_model_id: chat_model = next( - (m for m in models if m.id == search_space.chat_model_id), None + (m for m in models if m.id == workspace.chat_model_id), None ) if chat_model and has_capability(chat_model, "vision"): vision_default = chat_model.id - search_space.vision_model_id = vision_default or _default_model_for( + workspace.vision_model_id = vision_default or _default_model_for( models, "vision" ) - if search_space.image_gen_model_id is None: - search_space.image_gen_model_id = _default_model_for(models, "image_gen") + if workspace.image_gen_model_id is None: + workspace.image_gen_model_id = _default_model_for(models, "image_gen") @router.get("/model-providers", response_model=list[ModelProviderRead]) @@ -274,14 +274,14 @@ async def list_model_providers(auth: AuthContext = Depends(require_session_conte ] -async def _get_search_space(session: AsyncSession, search_space_id: int) -> SearchSpace: +async def _get_workspace(session: AsyncSession, workspace_id: int) -> Workspace: result = await session.execute( - select(SearchSpace).where(SearchSpace.id == search_space_id) + select(Workspace).where(Workspace.id == workspace_id) ) - search_space = result.scalars().first() - if not search_space: - raise HTTPException(status_code=404, detail="Search space not found") - return search_space + workspace = result.scalars().first() + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + return workspace async def _load_connection(session: AsyncSession, connection_id: int) -> Connection: @@ -304,13 +304,13 @@ async def _assert_connection_access( allow_spaceless_pat: bool = False, ) -> None: user = auth.user - if conn.search_space_id: + if conn.workspace_id: await check_permission( session, auth, - conn.search_space_id, + conn.workspace_id, permission, - "You don't have permission to manage model connections in this search space", + "You don't have permission to manage model connections in this workspace", ) return if conn.user_id != user.id: @@ -346,21 +346,21 @@ async def list_global_connections(auth: AuthContext = Depends(require_session_co @router.get("/model-connections", response_model=list[ConnectionRead]) async def list_connections( - search_space_id: int | None = None, + workspace_id: int | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user stmt = select(Connection).options(selectinload(Connection.models)) - if search_space_id is not None: + if workspace_id is not None: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.LLM_CONFIGS_CREATE.value, - "You don't have permission to view model connections in this search space", + "You don't have permission to view model connections in this workspace", ) - stmt = stmt.where(Connection.search_space_id == search_space_id) + stmt = stmt.where(Connection.workspace_id == workspace_id) else: stmt = stmt.where(Connection.user_id == user.id) result = await session.execute(stmt.order_by(Connection.id)) @@ -379,25 +379,25 @@ async def create_connection( if data.scope == ConnectionScope.GLOBAL: raise HTTPException(status_code=400, detail="GLOBAL connections are YAML-only") if data.scope == ConnectionScope.SEARCH_SPACE: - if data.search_space_id is None: - raise HTTPException(status_code=400, detail="search_space_id is required") + if data.workspace_id is None: + raise HTTPException(status_code=400, detail="workspace_id is required") await check_permission( session, auth, - data.search_space_id, + data.workspace_id, Permission.LLM_CONFIGS_CREATE.value, - "You don't have permission to create model connections in this search space", + "You don't have permission to create model connections in this workspace", ) elif auth.is_gated: raise HTTPException( status_code=403, detail="Managing personal model connections requires an interactive session", ) - payload = data.model_dump(exclude={"search_space_id", "models"}) + payload = data.model_dump(exclude={"workspace_id", "models"}) conn = Connection( **payload, - search_space_id=data.search_space_id + workspace_id=data.workspace_id if data.scope == ConnectionScope.SEARCH_SPACE else None, user_id=user.id, @@ -430,13 +430,13 @@ async def preview_connection_models( auth: AuthContext = Depends(get_auth_context), ): user = auth.user - if data.scope == ConnectionScope.SEARCH_SPACE and data.search_space_id is not None: + if data.scope == ConnectionScope.SEARCH_SPACE and data.workspace_id is not None: await check_permission( session, auth, - data.search_space_id, + data.workspace_id, Permission.LLM_CONFIGS_CREATE.value, - "You don't have permission to create model connections in this search space", + "You don't have permission to create model connections in this workspace", ) elif auth.is_gated: raise HTTPException( @@ -451,7 +451,7 @@ async def preview_connection_models( extra=data.extra or {}, scope=data.scope, enabled=data.enabled, - search_space_id=data.search_space_id + workspace_id=data.workspace_id if data.scope == ConnectionScope.SEARCH_SPACE else None, user_id=user.id, @@ -470,13 +470,13 @@ async def test_preview_connection_model( auth: AuthContext = Depends(get_auth_context), ): user = auth.user - if data.scope == ConnectionScope.SEARCH_SPACE and data.search_space_id is not None: + if data.scope == ConnectionScope.SEARCH_SPACE and data.workspace_id is not None: await check_permission( session, auth, - data.search_space_id, + data.workspace_id, Permission.LLM_CONFIGS_CREATE.value, - "You don't have permission to create model connections in this search space", + "You don't have permission to create model connections in this workspace", ) elif auth.is_gated: raise HTTPException( @@ -495,7 +495,7 @@ async def test_preview_connection_model( extra=data.extra or {}, scope=data.scope, enabled=data.enabled, - search_space_id=data.search_space_id + workspace_id=data.workspace_id if data.scope == ConnectionScope.SEARCH_SPACE else None, user_id=user.id, @@ -525,12 +525,12 @@ async def update_connection( await _assert_connection_access( session, auth, conn, Permission.LLM_CONFIGS_UPDATE.value ) - search_space_id = conn.search_space_id + workspace_id = conn.workspace_id for key, value in data.model_dump(exclude_unset=True).items(): setattr(conn, key, value) await session.commit() - if search_space_id is not None: - await _clear_invalid_roles(session, search_space_id) + if workspace_id is not None: + await _clear_invalid_roles(session, workspace_id) await session.commit() conn = await _load_connection(session, connection_id) return _connection_read(conn, list(conn.models)) @@ -546,11 +546,11 @@ async def delete_connection( await _assert_connection_access( session, auth, conn, Permission.LLM_CONFIGS_DELETE.value ) - search_space_id = conn.search_space_id + workspace_id = conn.workspace_id await session.delete(conn) await session.commit() - if search_space_id is not None: - await _clear_invalid_roles(session, search_space_id) + if workspace_id is not None: + await _clear_invalid_roles(session, workspace_id) await session.commit() return {"status": "deleted"} @@ -611,8 +611,8 @@ async def discover_connection_models( await session.commit() conn = await _load_connection(session, connection_id) await _default_unset_roles(session, conn, list(conn.models)) - if conn.search_space_id is not None: - await _clear_invalid_roles(session, conn.search_space_id) + if conn.workspace_id is not None: + await _clear_invalid_roles(session, conn.workspace_id) await session.commit() conn = await _load_connection(session, connection_id) return [_model_read(model) for model in conn.models] @@ -654,8 +654,8 @@ async def add_manual_model( await session.refresh(model) conn = await _load_connection(session, connection_id) await _default_unset_roles(session, conn, list(conn.models)) - if conn.search_space_id is not None: - await _clear_invalid_roles(session, conn.search_space_id) + if conn.workspace_id is not None: + await _clear_invalid_roles(session, conn.workspace_id) await session.commit() await session.refresh(model) return _model_read(model) @@ -674,7 +674,7 @@ async def bulk_update_models( await _assert_connection_access( session, auth, conn, Permission.LLM_CONFIGS_UPDATE.value ) - search_space_id = conn.search_space_id + workspace_id = conn.workspace_id model_ids = set(data.model_ids) await session.execute( @@ -684,8 +684,8 @@ async def bulk_update_models( ) await session.commit() session.expire_all() - if search_space_id is not None: - await _clear_invalid_roles(session, search_space_id) + if workspace_id is not None: + await _clear_invalid_roles(session, workspace_id) await session.commit() session.expire_all() @@ -715,14 +715,14 @@ async def update_model( await _assert_connection_access( session, auth, model.connection, Permission.LLM_CONFIGS_UPDATE.value ) - search_space_id = model.connection.search_space_id + workspace_id = model.connection.workspace_id update = data.model_dump(exclude_unset=True) for key, value in update.items(): setattr(model, key, value) await session.commit() await session.refresh(model) - if search_space_id is not None: - await _clear_invalid_roles(session, search_space_id) + if workspace_id is not None: + await _clear_invalid_roles(session, workspace_id) await session.commit() await session.refresh(model) return _model_read(model) @@ -753,35 +753,35 @@ async def test_connection_model( @router.get( - "/search-spaces/{search_space_id}/model-roles", response_model=ModelRolesRead + "/workspaces/{workspace_id}/model-roles", response_model=ModelRolesRead ) async def get_model_roles( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): await check_permission( session, auth, - search_space_id, + workspace_id, Permission.LLM_CONFIGS_CREATE.value, - "You don't have permission to view model roles in this search space", + "You don't have permission to view model roles in this workspace", ) - search_space = await _clear_invalid_roles(session, search_space_id) + workspace = await _clear_invalid_roles(session, workspace_id) await session.commit() - await session.refresh(search_space) + await session.refresh(workspace) return ModelRolesRead( - chat_model_id=search_space.chat_model_id, - vision_model_id=search_space.vision_model_id, - image_gen_model_id=search_space.image_gen_model_id, + chat_model_id=workspace.chat_model_id, + vision_model_id=workspace.vision_model_id, + image_gen_model_id=workspace.image_gen_model_id, ) @router.put( - "/search-spaces/{search_space_id}/model-roles", response_model=ModelRolesRead + "/workspaces/{workspace_id}/model-roles", response_model=ModelRolesRead ) async def update_model_roles( - search_space_id: int, + workspace_id: int, data: ModelRolesUpdate, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -789,51 +789,51 @@ async def update_model_roles( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.LLM_CONFIGS_UPDATE.value, - "You don't have permission to update model roles in this search space", + "You don't have permission to update model roles in this workspace", ) - search_space = await _get_search_space(session, search_space_id) + workspace = await _get_workspace(session, workspace_id) updates = data.model_dump(exclude_unset=True) if "chat_model_id" in updates: - previous_chat_model_id = search_space.chat_model_id + previous_chat_model_id = workspace.chat_model_id next_chat_model_id = await _validate_role_model_id( session, - search_space_id=search_space_id, + workspace_id=workspace_id, model_id=updates["chat_model_id"], capability="chat", ) - search_space.chat_model_id = next_chat_model_id + workspace.chat_model_id = next_chat_model_id if next_chat_model_id != previous_chat_model_id: await session.execute( update(NewChatThread) - .where(NewChatThread.search_space_id == search_space_id) + .where(NewChatThread.workspace_id == workspace_id) .values(pinned_llm_config_id=None) ) logger.info( - "Cleared auto model pins for search_space_id=%s after chat_model_id change (%s -> %s)", - search_space_id, + "Cleared auto model pins for workspace_id=%s after chat_model_id change (%s -> %s)", + workspace_id, previous_chat_model_id, next_chat_model_id, ) if "vision_model_id" in updates: - search_space.vision_model_id = await _validate_role_model_id( + workspace.vision_model_id = await _validate_role_model_id( session, - search_space_id=search_space_id, + workspace_id=workspace_id, model_id=updates["vision_model_id"], capability="vision", ) if "image_gen_model_id" in updates: - search_space.image_gen_model_id = await _validate_role_model_id( + workspace.image_gen_model_id = await _validate_role_model_id( session, - search_space_id=search_space_id, + workspace_id=workspace_id, model_id=updates["image_gen_model_id"], capability="image_gen", ) await session.commit() - await session.refresh(search_space) + await session.refresh(workspace) return ModelRolesRead( - chat_model_id=search_space.chat_model_id, - vision_model_id=search_space.vision_model_id, - image_gen_model_id=search_space.image_gen_model_id, + chat_model_id=workspace.chat_model_id, + vision_model_id=workspace.vision_model_id, + image_gen_model_id=workspace.image_gen_model_id, ) diff --git a/surfsense_backend/app/routes/new_chat_routes.py b/surfsense_backend/app/routes/new_chat_routes.py index 951682e47..6307264f9 100644 --- a/surfsense_backend/app/routes/new_chat_routes.py +++ b/surfsense_backend/app/routes/new_chat_routes.py @@ -45,7 +45,7 @@ from app.db import ( NewChatMessageRole, NewChatThread, Permission, - SearchSpace, + Workspace, TokenUsage, User, get_async_session, @@ -525,7 +525,7 @@ async def check_thread_access( Access is granted if: - User is the creator of the thread - Thread visibility is SEARCH_SPACE (any member can access) - for read/update operations only - - Thread is a legacy thread (created_by_id is NULL) - only if user is search space owner + - Thread is a legacy thread (created_by_id is NULL) - only if user is workspace owner Args: session: Database session @@ -558,16 +558,16 @@ async def check_thread_access( return True # For legacy threads (created before visibility feature), - # only the search space owner can access + # only the workspace owner can access if is_legacy: - search_space_query = select(SearchSpace).filter( - SearchSpace.id == thread.search_space_id + workspace_query = select(Workspace).filter( + Workspace.id == thread.workspace_id ) - search_space_result = await session.execute(search_space_query) - search_space = search_space_result.scalar_one_or_none() - is_search_space_owner = search_space and search_space.user_id == user.id + workspace_result = await session.execute(workspace_query) + workspace = workspace_result.scalar_one_or_none() + is_workspace_owner = workspace and workspace.user_id == user.id - if is_search_space_owner: + if is_workspace_owner: return True # Legacy threads are not accessible to non-owners raise HTTPException( @@ -593,23 +593,23 @@ async def check_thread_access( @router.get("/threads", response_model=ThreadListResponse) async def list_threads( - search_space_id: int, + workspace_id: int, limit: int | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ - List all accessible threads for the current user in a search space. + List all accessible threads for the current user in a workspace. Returns threads and archived_threads for ThreadListPrimitive. A user can see threads that are: - Created by them (regardless of visibility) - - Shared with the search space (visibility = SEARCH_SPACE) - - Legacy threads with no creator (created_by_id is NULL) - only if user is search space owner + - Shared with the workspace (visibility = SEARCH_SPACE) + - Legacy threads with no creator (created_by_id is NULL) - only if user is workspace owner Args: - search_space_id: The search space to list threads for + workspace_id: The workspace to list threads for limit: Optional limit on number of threads to return (applies to active threads only) Requires CHATS_READ permission. @@ -618,36 +618,36 @@ async def list_threads( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.CHATS_READ.value, - "You don't have permission to read chats in this search space", + "You don't have permission to read chats in this workspace", ) - # Check if user is the search space owner (for legacy thread visibility) - search_space_query = select(SearchSpace).filter( - SearchSpace.id == search_space_id + # Check if user is the workspace owner (for legacy thread visibility) + workspace_query = select(Workspace).filter( + Workspace.id == workspace_id ) - search_space_result = await session.execute(search_space_query) - search_space = search_space_result.scalar_one_or_none() - is_search_space_owner = search_space and search_space.user_id == user.id + workspace_result = await session.execute(workspace_query) + workspace = workspace_result.scalar_one_or_none() + is_workspace_owner = workspace and workspace.user_id == user.id # Build filter conditions: # 1. Created by the current user (any visibility) - # 2. Shared with the search space (visibility = SEARCH_SPACE) - # 3. Legacy threads (created_by_id is NULL) - only visible to search space owner + # 2. Shared with the workspace (visibility = SEARCH_SPACE) + # 3. Legacy threads (created_by_id is NULL) - only visible to workspace owner filter_conditions = [ NewChatThread.created_by_id == user.id, NewChatThread.visibility == ChatVisibility.SEARCH_SPACE, ] - # Only include legacy threads for the search space owner - if is_search_space_owner: + # Only include legacy threads for the workspace owner + if is_workspace_owner: filter_conditions.append(NewChatThread.created_by_id.is_(None)) query = ( select(NewChatThread) .filter( - NewChatThread.search_space_id == search_space_id, + NewChatThread.workspace_id == workspace_id, or_(*filter_conditions), ) .order_by(NewChatThread.updated_at.desc()) @@ -663,7 +663,7 @@ async def list_threads( for thread in all_threads: # Legacy threads (no creator) are treated as own threads for owner is_own_thread = thread.created_by_id == user.id or ( - thread.created_by_id is None and is_search_space_owner + thread.created_by_id is None and is_workspace_owner ) item = ThreadListItem( id=thread.id, @@ -701,22 +701,22 @@ async def list_threads( @router.get("/threads/search", response_model=list[ThreadListItem]) async def search_threads( - search_space_id: int, + workspace_id: int, title: str, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ - Search accessible threads by title in a search space. + Search accessible threads by title in a workspace. A user can search threads that are: - Created by them (regardless of visibility) - - Shared with the search space (visibility = SEARCH_SPACE) - - Legacy threads with no creator (created_by_id is NULL) - only if user is search space owner + - Shared with the workspace (visibility = SEARCH_SPACE) + - Legacy threads with no creator (created_by_id is NULL) - only if user is workspace owner Args: - search_space_id: The search space to search in + workspace_id: The workspace to search in title: The search query (case-insensitive partial match) Requires CHATS_READ permission. @@ -725,18 +725,18 @@ async def search_threads( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.CHATS_READ.value, - "You don't have permission to read chats in this search space", + "You don't have permission to read chats in this workspace", ) - # Check if user is the search space owner (for legacy thread visibility) - search_space_query = select(SearchSpace).filter( - SearchSpace.id == search_space_id + # Check if user is the workspace owner (for legacy thread visibility) + workspace_query = select(Workspace).filter( + Workspace.id == workspace_id ) - search_space_result = await session.execute(search_space_query) - search_space = search_space_result.scalar_one_or_none() - is_search_space_owner = search_space and search_space.user_id == user.id + workspace_result = await session.execute(workspace_query) + workspace = workspace_result.scalar_one_or_none() + is_workspace_owner = workspace and workspace.user_id == user.id # Build filter conditions filter_conditions = [ @@ -744,15 +744,15 @@ async def search_threads( NewChatThread.visibility == ChatVisibility.SEARCH_SPACE, ] - # Only include legacy threads for the search space owner - if is_search_space_owner: + # Only include legacy threads for the workspace owner + if is_workspace_owner: filter_conditions.append(NewChatThread.created_by_id.is_(None)) # Search accessible threads by title (case-insensitive) query = ( select(NewChatThread) .filter( - NewChatThread.search_space_id == search_space_id, + NewChatThread.workspace_id == workspace_id, NewChatThread.title.ilike(f"%{title}%"), or_(*filter_conditions), ) @@ -772,7 +772,7 @@ async def search_threads( # Legacy threads (no creator) are treated as own threads for owner is_own_thread=( thread.created_by_id == user.id - or (thread.created_by_id is None and is_search_space_owner) + or (thread.created_by_id is None and is_workspace_owner) ), created_at=thread.created_at, updated_at=thread.updated_at, @@ -812,9 +812,9 @@ async def create_thread( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_CREATE.value, - "You don't have permission to create chats in this search space", + "You don't have permission to create chats in this workspace", ) now = datetime.now(UTC) @@ -822,7 +822,7 @@ async def create_thread( title=thread.title, archived=thread.archived, visibility=thread.visibility, - search_space_id=thread.search_space_id, + workspace_id=thread.workspace_id, created_by_id=user.id, updated_at=now, ) @@ -879,13 +879,13 @@ async def get_thread_messages( if not thread: raise HTTPException(status_code=404, detail="Thread not found") - # Check permission to read chats in this search space + # Check permission to read chats in this workspace await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_READ.value, - "You don't have permission to read chats in this search space", + "You don't have permission to read chats in this workspace", ) # Check thread-level access based on visibility @@ -971,9 +971,9 @@ async def get_thread_full( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_READ.value, - "You don't have permission to read chats in this search space", + "You don't have permission to read chats in this workspace", ) # Check thread-level access based on visibility @@ -1035,9 +1035,9 @@ async def update_thread( await check_permission( session, auth, - db_thread.search_space_id, + db_thread.workspace_id, Permission.CHATS_UPDATE.value, - "You don't have permission to update chats in this search space", + "You don't have permission to update chats in this workspace", ) # For PRIVATE threads, only the creator can update @@ -1104,16 +1104,16 @@ async def delete_thread( await check_permission( session, auth, - db_thread.search_space_id, + db_thread.workspace_id, Permission.CHATS_DELETE.value, - "You don't have permission to delete chats in this search space", + "You don't have permission to delete chats in this workspace", ) # For PRIVATE threads, only the creator can delete # For SEARCH_SPACE threads, any member with permission can delete # Legacy threads (created_by_id is NULL) have no recorded creator, # so we skip strict ownership and fall through to legacy handling - # which allows the search space owner to delete them + # which allows the workspace owner to delete them if db_thread.visibility == ChatVisibility.PRIVATE: await check_thread_access( session, @@ -1162,7 +1162,7 @@ async def update_thread_visibility( Only the creator of the thread can change its visibility. - PRIVATE: Only the creator can access the thread (default) - - SEARCH_SPACE: All members of the search space can access the thread + - SEARCH_SPACE: All members of the workspace can access the thread Requires CHATS_UPDATE permission. """ @@ -1178,9 +1178,9 @@ async def update_thread_visibility( await check_permission( session, auth, - db_thread.search_space_id, + db_thread.workspace_id, Permission.CHATS_UPDATE.value, - "You don't have permission to update chats in this search space", + "You don't have permission to update chats in this workspace", ) # Only the creator can change visibility @@ -1381,9 +1381,9 @@ async def append_message( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_UPDATE.value, - "You don't have permission to update chats in this search space", + "You don't have permission to update chats in this workspace", ) # Check thread-level access based on visibility @@ -1552,7 +1552,7 @@ async def append_message( call_details=token_usage_data.get("call_details"), thread_id=thread_id, message_id=db_message.id, - search_space_id=thread.search_space_id, + workspace_id=thread.workspace_id, user_id=user_uuid, ) .on_conflict_do_nothing( @@ -1632,9 +1632,9 @@ async def list_messages( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_READ.value, - "You don't have permission to read chats in this search space", + "You don't have permission to read chats in this workspace", ) # Check thread-level access based on visibility @@ -1730,9 +1730,9 @@ async def handle_new_chat( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_CREATE.value, - "You don't have permission to chat in this search space", + "You don't have permission to chat in this workspace", ) # Check thread-level access based on visibility @@ -1744,24 +1744,24 @@ async def handle_new_chat( local_mounts=request.local_filesystem_mounts, ) - # Get search space to check LLM config preferences - search_space_result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == request.search_space_id) + # Get workspace to check LLM config preferences + workspace_result = await session.execute( + select(Workspace).filter(Workspace.id == request.workspace_id) ) - search_space = search_space_result.scalars().first() + workspace = workspace_result.scalars().first() - if not search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") # Use the converged model-connections role for chat operations. # Positive IDs load Model + Connection rows; negative IDs load # virtual GLOBAL models; 0 means Auto. llm_config_id = ( - search_space.chat_model_id if search_space.chat_model_id is not None else 0 + workspace.chat_model_id if workspace.chat_model_id is not None else 0 ) # Release the read-transaction so we don't hold ACCESS SHARE locks - # on searchspaces/documents for the entire duration of the stream. + # on workspaces/documents for the entire duration of the stream. # expire_on_commit=False keeps loaded ORM attrs usable. await session.commit() # Close the dependency session now so its connection returns to @@ -1791,7 +1791,7 @@ async def handle_new_chat( return StreamingResponse( stream_new_chat( user_query=request.user_query, - search_space_id=request.search_space_id, + workspace_id=request.workspace_id, chat_id=request.chat_id, user_id=str(user.id), llm_config_id=llm_config_id, @@ -1849,9 +1849,9 @@ async def cancel_active_turn( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_UPDATE.value, - "You don't have permission to update chats in this search space", + "You don't have permission to update chats in this workspace", ) await check_thread_access(session, thread, user) @@ -1901,9 +1901,9 @@ async def get_turn_status( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_READ.value, - "You don't have permission to view chats in this search space", + "You don't have permission to view chats in this workspace", ) await check_thread_access(session, thread, user) @@ -1965,9 +1965,9 @@ async def regenerate_response( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_UPDATE.value, - "You don't have permission to update chats in this search space", + "You don't have permission to update chats in this workspace", ) # Check thread-level access based on visibility @@ -2234,21 +2234,21 @@ async def regenerate_response( seen_turns.add(tid) revert_turn_ids.append(tid) - # Get search space for LLM config - search_space_result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == request.search_space_id) + # Get workspace for LLM config + workspace_result = await session.execute( + select(Workspace).filter(Workspace.id == request.workspace_id) ) - search_space = search_space_result.scalars().first() + workspace = workspace_result.scalars().first() - if not search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") llm_config_id = ( - search_space.chat_model_id if search_space.chat_model_id is not None else 0 + workspace.chat_model_id if workspace.chat_model_id is not None else 0 ) # Release the read-transaction so we don't hold ACCESS SHARE locks - # on searchspaces/documents for the entire duration of the stream. + # on workspaces/documents for the entire duration of the stream. # expire_on_commit=False keeps loaded ORM attrs (including messages_to_delete PKs) usable. await session.commit() await session.close() @@ -2288,7 +2288,7 @@ async def regenerate_response( try: async for chunk in stream_new_chat( user_query=str(user_query_to_use), - search_space_id=request.search_space_id, + workspace_id=request.workspace_id, chat_id=thread_id, user_id=str(user.id), llm_config_id=llm_config_id, @@ -2390,9 +2390,9 @@ async def resume_chat( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_CREATE.value, - "You don't have permission to chat in this search space", + "You don't have permission to chat in this workspace", ) await check_thread_access(session, thread, user) @@ -2403,29 +2403,29 @@ async def resume_chat( local_mounts=request.local_filesystem_mounts, ) - search_space_result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == request.search_space_id) + workspace_result = await session.execute( + select(Workspace).filter(Workspace.id == request.workspace_id) ) - search_space = search_space_result.scalars().first() + workspace = workspace_result.scalars().first() - if not search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") llm_config_id = ( - search_space.chat_model_id if search_space.chat_model_id is not None else 0 + workspace.chat_model_id if workspace.chat_model_id is not None else 0 ) decisions = [d.model_dump() for d in request.decisions] # Release the read-transaction so we don't hold ACCESS SHARE locks - # on searchspaces/documents for the entire duration of the stream. + # on workspaces/documents for the entire duration of the stream. await session.commit() await session.close() return StreamingResponse( stream_resume_chat( chat_id=thread_id, - search_space_id=request.search_space_id, + workspace_id=request.workspace_id, decisions=decisions, user_id=str(user.id), llm_config_id=llm_config_id, diff --git a/surfsense_backend/app/routes/notes_routes.py b/surfsense_backend/app/routes/notes_routes.py index eb3c66b5f..e421ceff4 100644 --- a/surfsense_backend/app/routes/notes_routes.py +++ b/surfsense_backend/app/routes/notes_routes.py @@ -23,9 +23,9 @@ class CreateNoteRequest(BaseModel): source_markdown: str | None = None -@router.post("/search-spaces/{search_space_id}/notes", response_model=DocumentRead) +@router.post("/workspaces/{workspace_id}/notes", response_model=DocumentRead) async def create_note( - search_space_id: int, + workspace_id: int, request: CreateNoteRequest, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -40,9 +40,9 @@ async def create_note( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_CREATE.value, - "You don't have permission to create notes in this search space", + "You don't have permission to create notes in this workspace", ) if not request.title or not request.title.strip(): @@ -58,7 +58,7 @@ async def create_note( # Create document with NOTE type document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=request.title.strip(), document_type=DocumentType.NOTE, content="", # Empty initially, will be populated on first save/reindex @@ -83,7 +83,7 @@ async def create_note( content_hash=document.content_hash, unique_identifier_hash=document.unique_identifier_hash, document_metadata=document.document_metadata, - search_space_id=document.search_space_id, + workspace_id=document.workspace_id, created_at=document.created_at, updated_at=document.updated_at, created_by_id=document.created_by_id, @@ -91,11 +91,11 @@ async def create_note( @router.get( - "/search-spaces/{search_space_id}/notes", + "/workspaces/{workspace_id}/notes", response_model=PaginatedResponse[DocumentRead], ) async def list_notes( - search_space_id: int, + workspace_id: int, skip: int | None = None, page: int | None = None, page_size: int = 50, @@ -103,7 +103,7 @@ async def list_notes( auth: AuthContext = Depends(get_auth_context), ): """ - List all notes in a search space. + List all notes in a workspace. Requires DOCUMENTS_READ permission. """ @@ -111,16 +111,16 @@ async def list_notes( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read notes in this search space", + "You don't have permission to read notes in this workspace", ) from sqlalchemy import func # Build query query = select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.NOTE, ) @@ -128,7 +128,7 @@ async def list_notes( count_query = select(func.count()).select_from( select(Document) .where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.NOTE, ) .subquery() @@ -164,7 +164,7 @@ async def list_notes( content_hash=doc.content_hash, unique_identifier_hash=doc.unique_identifier_hash, document_metadata=doc.document_metadata, - search_space_id=doc.search_space_id, + workspace_id=doc.workspace_id, created_at=doc.created_at, updated_at=doc.updated_at, ) @@ -188,9 +188,9 @@ async def list_notes( ) -@router.delete("/search-spaces/{search_space_id}/notes/{note_id}") +@router.delete("/workspaces/{workspace_id}/notes/{note_id}") async def delete_note( - search_space_id: int, + workspace_id: int, note_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -204,16 +204,16 @@ async def delete_note( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.DOCUMENTS_DELETE.value, - "You don't have permission to delete notes in this search space", + "You don't have permission to delete notes in this workspace", ) # Get document result = await session.execute( select(Document).where( Document.id == note_id, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.NOTE, ) ) diff --git a/surfsense_backend/app/routes/notion_add_connector_route.py b/surfsense_backend/app/routes/notion_add_connector_route.py index b0fafb242..830e1c641 100644 --- a/surfsense_backend/app/routes/notion_add_connector_route.py +++ b/surfsense_backend/app/routes/notion_add_connector_route.py @@ -84,7 +84,7 @@ async def connect_notion( Initiate Notion OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -145,7 +145,7 @@ async def reauth_notion( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR, ) @@ -345,7 +345,7 @@ async def notion_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR, ) @@ -408,7 +408,7 @@ async def notion_callback( connector_type=SearchSourceConnectorType.NOTION_CONNECTOR, is_indexable=True, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) diff --git a/surfsense_backend/app/routes/oauth_connector_base.py b/surfsense_backend/app/routes/oauth_connector_base.py index 483caa6c2..42e99c262 100644 --- a/surfsense_backend/app/routes/oauth_connector_base.py +++ b/surfsense_backend/app/routes/oauth_connector_base.py @@ -415,7 +415,7 @@ class OAuthConnectorRoute: select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == oauth.connector_type, ) ) @@ -530,7 +530,7 @@ class OAuthConnectorRoute: select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == oauth.connector_type, ) ) @@ -597,7 +597,7 @@ class OAuthConnectorRoute: connector_type=oauth.connector_type, is_indexable=oauth.is_indexable, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) diff --git a/surfsense_backend/app/routes/obsidian_plugin_routes.py b/surfsense_backend/app/routes/obsidian_plugin_routes.py index 56623d61a..5aa7cf8fa 100644 --- a/surfsense_backend/app/routes/obsidian_plugin_routes.py +++ b/surfsense_backend/app/routes/obsidian_plugin_routes.py @@ -22,7 +22,7 @@ from app.db import ( DocumentType, SearchSourceConnector, SearchSourceConnectorType, - SearchSpace, + Workspace, User, get_async_session, ) @@ -54,7 +54,7 @@ from app.services.obsidian_plugin_indexer import ( ) from app.tasks.celery_tasks.obsidian_tasks import index_obsidian_attachment_task from app.users import allow_any_principal, get_auth_context -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access logger = logging.getLogger(__name__) @@ -102,7 +102,7 @@ async def _start_obsidian_sync_notification( operation_id=operation_id, title=f"Syncing: {connector_name}", message="Syncing from Obsidian plugin", - search_space_id=connector.search_space_id, + workspace_id=connector.workspace_id, initial_metadata={ "connector_id": connector.id, "connector_name": connector_name, @@ -195,7 +195,7 @@ async def _resolve_vault_connector( connector = (await session.execute(stmt)).scalars().first() if connector is not None: - await check_search_space_access(session, auth, connector.search_space_id) + await check_workspace_access(session, auth, connector.workspace_id) return connector raise HTTPException( @@ -222,17 +222,17 @@ def _queue_obsidian_attachment( ) -async def _ensure_search_space_access( +async def _ensure_workspace_access( session: AsyncSession, *, auth: AuthContext, - search_space_id: int, -) -> SearchSpace: - """Owner-only access to the search space (shared spaces are a follow-up).""" + workspace_id: int, +) -> Workspace: + """Owner-only access to the workspace (shared spaces are a follow-up).""" user = auth.user result = await session.execute( - select(SearchSpace).where( - and_(SearchSpace.id == search_space_id, SearchSpace.user_id == user.id) + select(Workspace).where( + and_(Workspace.id == workspace_id, Workspace.user_id == user.id) ) ) space = result.scalars().first() @@ -241,10 +241,10 @@ async def _ensure_search_space_access( status_code=status.HTTP_403_FORBIDDEN, detail={ "code": "SEARCH_SPACE_FORBIDDEN", - "message": "You don't own that search space.", + "message": "You don't own that workspace.", }, ) - await check_search_space_access(session, auth, search_space_id) + await check_workspace_access(session, auth, workspace_id) return space @@ -326,8 +326,8 @@ async def obsidian_connect( Fingerprint collisions on (1) trigger ``merge_obsidian_connectors`` so the partial unique index can never produce two live rows for one vault. """ - await _ensure_search_space_access( - session, auth=auth, search_space_id=payload.search_space_id + await _ensure_workspace_access( + session, auth=auth, workspace_id=payload.workspace_id ) user = auth.user @@ -354,7 +354,7 @@ async def obsidian_connect( response = ConnectResponse( connector_id=collision.id, vault_id=collision_cfg["vault_id"], - search_space_id=collision.search_space_id, + workspace_id=collision.workspace_id, server_time_utc=datetime.now(UTC), **_build_handshake(), ) @@ -363,12 +363,12 @@ async def obsidian_connect( existing_by_vid.name = display_name existing_by_vid.config = cfg - existing_by_vid.search_space_id = payload.search_space_id + existing_by_vid.workspace_id = payload.workspace_id existing_by_vid.is_indexable = False response = ConnectResponse( connector_id=existing_by_vid.id, vault_id=payload.vault_id, - search_space_id=existing_by_vid.search_space_id, + workspace_id=existing_by_vid.workspace_id, server_time_utc=datetime.now(UTC), **_build_handshake(), ) @@ -387,7 +387,7 @@ async def obsidian_connect( response = ConnectResponse( connector_id=existing_by_fp.id, vault_id=survivor_cfg["vault_id"], - search_space_id=existing_by_fp.search_space_id, + workspace_id=existing_by_fp.workspace_id, server_time_utc=datetime.now(UTC), **_build_handshake(), ) @@ -406,12 +406,12 @@ async def obsidian_connect( is_indexable=False, config=cfg, user_id=user.id, - search_space_id=payload.search_space_id, + workspace_id=payload.workspace_id, ) .on_conflict_do_nothing() .returning( SearchSourceConnector.id, - SearchSourceConnector.search_space_id, + SearchSourceConnector.workspace_id, ) ) inserted = (await session.execute(insert_stmt)).first() @@ -419,7 +419,7 @@ async def obsidian_connect( response = ConnectResponse( connector_id=inserted.id, vault_id=payload.vault_id, - search_space_id=inserted.search_space_id, + workspace_id=inserted.workspace_id, server_time_utc=datetime.now(UTC), **_build_handshake(), ) @@ -441,7 +441,7 @@ async def obsidian_connect( response = ConnectResponse( connector_id=winner.id, vault_id=(winner.config or {})["vault_id"], - search_space_id=winner.search_space_id, + workspace_id=winner.workspace_id, server_time_utc=datetime.now(UTC), **_build_handshake(), ) diff --git a/surfsense_backend/app/routes/onedrive_add_connector_route.py b/surfsense_backend/app/routes/onedrive_add_connector_route.py index 9c55d4fe7..9ccd9e80c 100644 --- a/surfsense_backend/app/routes/onedrive_add_connector_route.py +++ b/surfsense_backend/app/routes/onedrive_add_connector_route.py @@ -36,7 +36,7 @@ from app.utils.connector_naming import ( generate_unique_connector_name, ) from app.utils.oauth_security import OAuthStateManager, TokenEncryption -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access logger = logging.getLogger(__name__) router = APIRouter() @@ -134,7 +134,7 @@ async def reauth_onedrive( select(SearchSourceConnector).filter( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user.id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR, ) @@ -312,7 +312,7 @@ async def onedrive_callback( select(SearchSourceConnector).filter( SearchSourceConnector.id == reauth_connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.workspace_id == space_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR, ) @@ -379,7 +379,7 @@ async def onedrive_callback( connector_type=SearchSourceConnectorType.ONEDRIVE_CONNECTOR, is_indexable=True, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) @@ -438,7 +438,7 @@ async def list_onedrive_folders( status_code=404, detail="OneDrive connector not found or access denied" ) - await check_search_space_access(session, auth, connector.search_space_id) + await check_workspace_access(session, auth, connector.workspace_id) onedrive_client = OneDriveClient(session, connector_id) items, error = await list_folder_contents(onedrive_client, parent_id=parent_id) diff --git a/surfsense_backend/app/routes/prompts_routes.py b/surfsense_backend/app/routes/prompts_routes.py index b4cb1466c..b7a989d82 100644 --- a/surfsense_backend/app/routes/prompts_routes.py +++ b/surfsense_backend/app/routes/prompts_routes.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.auth.context import AuthContext -from app.db import Prompt, SearchSpaceMembership, get_async_session +from app.db import Prompt, WorkspaceMembership, get_async_session from app.schemas.prompts import ( PromptCreate, PromptRead, @@ -18,14 +18,14 @@ router = APIRouter(tags=["Prompts"]) @router.get("/prompts", response_model=list[PromptRead]) async def list_prompts( - search_space_id: int | None = None, + workspace_id: int | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(require_session_context), ): user = auth.user query = select(Prompt).where(Prompt.user_id == user.id) - if search_space_id is not None: - query = query.where(Prompt.search_space_id == search_space_id) + if workspace_id is not None: + query = query.where(Prompt.workspace_id == workspace_id) query = query.order_by(Prompt.created_at.desc()) result = await session.execute(query) return result.scalars().all() @@ -38,22 +38,22 @@ async def create_prompt( auth: AuthContext = Depends(require_session_context), ): user = auth.user - if body.search_space_id is not None: + if body.workspace_id is not None: membership = await session.execute( - select(SearchSpaceMembership).where( - SearchSpaceMembership.user_id == user.id, - SearchSpaceMembership.search_space_id == body.search_space_id, + select(WorkspaceMembership).where( + WorkspaceMembership.user_id == user.id, + WorkspaceMembership.workspace_id == body.workspace_id, ) ) if not membership.scalar_one_or_none(): raise HTTPException( status_code=403, - detail="You are not a member of this search space", + detail="You are not a member of this workspace", ) prompt = Prompt( user_id=user.id, - search_space_id=body.search_space_id, + workspace_id=body.workspace_id, name=body.name, prompt=body.prompt, mode=body.mode, diff --git a/surfsense_backend/app/routes/rbac_routes.py b/surfsense_backend/app/routes/rbac_routes.py index e1122b2bb..e9eaf5502 100644 --- a/surfsense_backend/app/routes/rbac_routes.py +++ b/surfsense_backend/app/routes/rbac_routes.py @@ -2,9 +2,9 @@ RBAC (Role-Based Access Control) routes for managing roles, memberships, and invites. Endpoints: -- /searchspaces/{search_space_id}/roles - CRUD for roles -- /searchspaces/{search_space_id}/members - CRUD for memberships -- /searchspaces/{search_space_id}/invites - CRUD for invites +- /workspaces/{workspace_id}/roles - CRUD for roles +- /workspaces/{workspace_id}/members - CRUD for memberships +- /workspaces/{workspace_id}/invites - CRUD for invites - /invites/{invite_code}/info - Get invite info (public) - /invites/accept - Accept an invite - /permissions - List all available permissions @@ -21,10 +21,10 @@ from sqlalchemy.orm import selectinload from app.auth.context import AuthContext from app.db import ( Permission, - SearchSpace, - SearchSpaceInvite, - SearchSpaceMembership, - SearchSpaceRole, + Workspace, + WorkspaceInvite, + WorkspaceMembership, + WorkspaceRole, User, get_async_session, ) @@ -42,12 +42,12 @@ from app.schemas import ( RoleCreate, RoleRead, RoleUpdate, - UserSearchSpaceAccess, + UserWorkspaceAccess, ) from app.users import get_auth_context from app.utils.rbac import ( check_permission, - check_search_space_access, + check_workspace_access, generate_invite_code, get_default_role, get_user_permissions, @@ -63,10 +63,10 @@ router = APIRouter() # Human-readable descriptions for each permission PERMISSION_DESCRIPTIONS = { # Documents - "documents:create": "Add new documents, files, and content to the search space", - "documents:read": "View and search documents in the search space", + "documents:create": "Add new documents, files, and content to the workspace", + "documents:read": "View and search documents in the workspace", "documents:update": "Edit existing documents and their metadata", - "documents:delete": "Remove documents from the search space", + "documents:delete": "Remove documents from the workspace", # Chats "chats:create": "Start new AI chat conversations", "chats:read": "View chat history and conversations", @@ -97,7 +97,7 @@ PERMISSION_DESCRIPTIONS = { # Members "members:invite": "Send invitations to new team members", "members:view": "View the list of team members", - "members:remove": "Remove members from the search space", + "members:remove": "Remove members from the workspace", "members:manage_roles": "Assign and change member roles", # Roles "roles:create": "Create new custom roles", @@ -105,16 +105,16 @@ PERMISSION_DESCRIPTIONS = { "roles:update": "Modify role permissions", "roles:delete": "Remove custom roles", # Settings - "settings:view": "View search space settings", - "settings:update": "Modify search space settings", - "settings:delete": "Delete the entire search space", + "settings:view": "View workspace settings", + "settings:update": "Modify workspace settings", + "settings:delete": "Delete the entire workspace", # API access - "api_access:manage": "Enable or disable programmatic API access for a search space", + "api_access:manage": "Enable or disable programmatic API access for a workspace", # Automations "automations:create": "Create automations from chat or JSON", "automations:read": "View automations, their triggers, and run history", "automations:update": "Edit automations and manage their triggers", - "automations:delete": "Remove automations from the search space", + "automations:delete": "Remove automations from the workspace", "automations:execute": "Manually fire automations", # Full access "*": "Full access to all features and settings", @@ -152,39 +152,39 @@ async def list_all_permissions( @router.post( - "/searchspaces/{search_space_id}/roles", + "/workspaces/{workspace_id}/roles", response_model=RoleRead, ) async def create_role( - search_space_id: int, + workspace_id: int, role_data: RoleCreate, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - Create a new custom role in a search space. + Create a new custom role in a workspace. Requires ROLES_CREATE permission. """ try: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.ROLES_CREATE.value, "You don't have permission to create roles", ) # Check if role with same name already exists result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.search_space_id == search_space_id, - SearchSpaceRole.name == role_data.name, + select(WorkspaceRole).filter( + WorkspaceRole.workspace_id == workspace_id, + WorkspaceRole.name == role_data.name, ) ) if result.scalars().first(): raise HTTPException( status_code=409, - detail=f"A role with name '{role_data.name}' already exists in this search space", + detail=f"A role with name '{role_data.name}' already exists in this workspace", ) # Validate permissions @@ -199,23 +199,23 @@ async def create_role( # If setting is_default to True, unset any existing default if role_data.is_default: await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.search_space_id == search_space_id, - SearchSpaceRole.is_default == True, # noqa: E712 + select(WorkspaceRole).filter( + WorkspaceRole.workspace_id == workspace_id, + WorkspaceRole.is_default == True, # noqa: E712 ) ) existing_defaults = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.search_space_id == search_space_id, - SearchSpaceRole.is_default == True, # noqa: E712 + select(WorkspaceRole).filter( + WorkspaceRole.workspace_id == workspace_id, + WorkspaceRole.is_default == True, # noqa: E712 ) ) for existing in existing_defaults.scalars().all(): existing.is_default = False - db_role = SearchSpaceRole( + db_role = WorkspaceRole( **role_data.model_dump(), - search_space_id=search_space_id, + workspace_id=workspace_id, is_system_role=False, ) session.add(db_role) @@ -234,30 +234,30 @@ async def create_role( @router.get( - "/searchspaces/{search_space_id}/roles", + "/workspaces/{workspace_id}/roles", response_model=list[RoleRead], ) async def list_roles( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - List all roles in a search space. + List all roles in a workspace. Requires ROLES_READ permission. """ try: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.ROLES_READ.value, "You don't have permission to view roles", ) result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.search_space_id == search_space_id + select(WorkspaceRole).filter( + WorkspaceRole.workspace_id == workspace_id ) ) return result.scalars().all() @@ -271,11 +271,11 @@ async def list_roles( @router.get( - "/searchspaces/{search_space_id}/roles/{role_id}", + "/workspaces/{workspace_id}/roles/{role_id}", response_model=RoleRead, ) async def get_role( - search_space_id: int, + workspace_id: int, role_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -288,15 +288,15 @@ async def get_role( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.ROLES_READ.value, "You don't have permission to view roles", ) result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.id == role_id, - SearchSpaceRole.search_space_id == search_space_id, + select(WorkspaceRole).filter( + WorkspaceRole.id == role_id, + WorkspaceRole.workspace_id == workspace_id, ) ) role = result.scalars().first() @@ -315,11 +315,11 @@ async def get_role( @router.put( - "/searchspaces/{search_space_id}/roles/{role_id}", + "/workspaces/{workspace_id}/roles/{role_id}", response_model=RoleRead, ) async def update_role( - search_space_id: int, + workspace_id: int, role_id: int, role_update: RoleUpdate, session: AsyncSession = Depends(get_async_session), @@ -334,15 +334,15 @@ async def update_role( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.ROLES_UPDATE.value, "You don't have permission to update roles", ) result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.id == role_id, - SearchSpaceRole.search_space_id == search_space_id, + select(WorkspaceRole).filter( + WorkspaceRole.id == role_id, + WorkspaceRole.workspace_id == workspace_id, ) ) db_role = result.scalars().first() @@ -365,9 +365,9 @@ async def update_role( # Check for name conflict if updating name if "name" in update_data and update_data["name"] != db_role.name: existing = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.search_space_id == search_space_id, - SearchSpaceRole.name == update_data["name"], + select(WorkspaceRole).filter( + WorkspaceRole.workspace_id == workspace_id, + WorkspaceRole.name == update_data["name"], ) ) if existing.scalars().first(): @@ -390,9 +390,9 @@ async def update_role( if update_data.get("is_default") and not db_role.is_default: # Unset existing default existing_defaults = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.search_space_id == search_space_id, - SearchSpaceRole.is_default == True, # noqa: E712 + select(WorkspaceRole).filter( + WorkspaceRole.workspace_id == workspace_id, + WorkspaceRole.is_default == True, # noqa: E712 ) ) for existing in existing_defaults.scalars().all(): @@ -415,9 +415,9 @@ async def update_role( ) from e -@router.delete("/searchspaces/{search_space_id}/roles/{role_id}") +@router.delete("/workspaces/{workspace_id}/roles/{role_id}") async def delete_role( - search_space_id: int, + workspace_id: int, role_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -431,15 +431,15 @@ async def delete_role( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.ROLES_DELETE.value, "You don't have permission to delete roles", ) result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.id == role_id, - SearchSpaceRole.search_space_id == search_space_id, + select(WorkspaceRole).filter( + WorkspaceRole.id == role_id, + WorkspaceRole.workspace_id == workspace_id, ) ) db_role = result.scalars().first() @@ -471,31 +471,31 @@ async def delete_role( @router.get( - "/searchspaces/{search_space_id}/members", + "/workspaces/{workspace_id}/members", response_model=list[MembershipRead], ) async def list_members( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - List all members of a search space. + List all members of a workspace. Requires MEMBERS_VIEW permission. """ try: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.MEMBERS_VIEW.value, "You don't have permission to view members", ) result = await session.execute( - select(SearchSpaceMembership) - .options(selectinload(SearchSpaceMembership.role)) - .filter(SearchSpaceMembership.search_space_id == search_space_id) + select(WorkspaceMembership) + .options(selectinload(WorkspaceMembership.role)) + .filter(WorkspaceMembership.workspace_id == workspace_id) ) memberships = result.scalars().all() @@ -510,7 +510,7 @@ async def list_members( membership_dict = { "id": membership.id, "user_id": membership.user_id, - "search_space_id": membership.search_space_id, + "workspace_id": membership.workspace_id, "role_id": membership.role_id, "is_owner": membership.is_owner, "joined_at": membership.joined_at, @@ -534,11 +534,11 @@ async def list_members( @router.put( - "/searchspaces/{search_space_id}/members/{membership_id}", + "/workspaces/{workspace_id}/members/{membership_id}", response_model=MembershipRead, ) async def update_member_role( - search_space_id: int, + workspace_id: int, membership_id: int, membership_update: MembershipUpdate, session: AsyncSession = Depends(get_async_session), @@ -553,17 +553,17 @@ async def update_member_role( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.MEMBERS_MANAGE_ROLES.value, "You don't have permission to manage member roles", ) result = await session.execute( - select(SearchSpaceMembership) - .options(selectinload(SearchSpaceMembership.role)) + select(WorkspaceMembership) + .options(selectinload(WorkspaceMembership.role)) .filter( - SearchSpaceMembership.id == membership_id, - SearchSpaceMembership.search_space_id == search_space_id, + WorkspaceMembership.id == membership_id, + WorkspaceMembership.workspace_id == workspace_id, ) ) db_membership = result.scalars().first() @@ -578,18 +578,18 @@ async def update_member_role( detail="Cannot change the owner's role", ) - # Verify the new role exists in this search space + # Verify the new role exists in this workspace if membership_update.role_id: role_result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.id == membership_update.role_id, - SearchSpaceRole.search_space_id == search_space_id, + select(WorkspaceRole).filter( + WorkspaceRole.id == membership_update.role_id, + WorkspaceRole.workspace_id == workspace_id, ) ) if not role_result.scalars().first(): raise HTTPException( status_code=404, - detail="Role not found in this search space", + detail="Role not found in this workspace", ) db_membership.role_id = membership_update.role_id @@ -605,7 +605,7 @@ async def update_member_role( return { "id": db_membership.id, "user_id": db_membership.user_id, - "search_space_id": db_membership.search_space_id, + "workspace_id": db_membership.workspace_id, "role_id": db_membership.role_id, "is_owner": db_membership.is_owner, "joined_at": db_membership.joined_at, @@ -628,22 +628,22 @@ async def update_member_role( # NOTE: /members/me must be defined BEFORE /members/{membership_id} # because FastAPI matches routes in order, and "me" would otherwise # be interpreted as a membership_id (causing a 422 validation error) -@router.delete("/searchspaces/{search_space_id}/members/me") -async def leave_search_space( - search_space_id: int, +@router.delete("/workspaces/{workspace_id}/members/me") +async def leave_workspace( + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ - Leave a search space (remove own membership). - Owners cannot leave their search space. + Leave a workspace (remove own membership). + Owners cannot leave their workspace. """ try: result = await session.execute( - select(SearchSpaceMembership).filter( - SearchSpaceMembership.user_id == user.id, - SearchSpaceMembership.search_space_id == search_space_id, + select(WorkspaceMembership).filter( + WorkspaceMembership.user_id == user.id, + WorkspaceMembership.workspace_id == workspace_id, ) ) db_membership = result.scalars().first() @@ -651,38 +651,38 @@ async def leave_search_space( if not db_membership: raise HTTPException( status_code=404, - detail="You are not a member of this search space", + detail="You are not a member of this workspace", ) if db_membership.is_owner: raise HTTPException( status_code=400, - detail="Owners cannot leave their search space. Transfer ownership first or delete the search space.", + detail="Owners cannot leave their workspace. Transfer ownership first or delete the workspace.", ) await session.delete(db_membership) await session.commit() - return {"message": "Successfully left the search space"} + return {"message": "Successfully left the workspace"} except HTTPException: raise except Exception as e: await session.rollback() - logger.error(f"Failed to leave search space: {e!s}", exc_info=True) + logger.error(f"Failed to leave workspace: {e!s}", exc_info=True) raise HTTPException( - status_code=500, detail=f"Failed to leave search space: {e!s}" + status_code=500, detail=f"Failed to leave workspace: {e!s}" ) from e -@router.delete("/searchspaces/{search_space_id}/members/{membership_id}") +@router.delete("/workspaces/{workspace_id}/members/{membership_id}") async def remove_member( - search_space_id: int, + workspace_id: int, membership_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - Remove a member from a search space. + Remove a member from a workspace. Requires MEMBERS_REMOVE permission. Cannot remove the owner. """ @@ -690,15 +690,15 @@ async def remove_member( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.MEMBERS_REMOVE.value, "You don't have permission to remove members", ) result = await session.execute( - select(SearchSpaceMembership).filter( - SearchSpaceMembership.id == membership_id, - SearchSpaceMembership.search_space_id == search_space_id, + select(WorkspaceMembership).filter( + WorkspaceMembership.id == membership_id, + WorkspaceMembership.workspace_id == workspace_id, ) ) db_membership = result.scalars().first() @@ -709,7 +709,7 @@ async def remove_member( if db_membership.is_owner: raise HTTPException( status_code=400, - detail="Cannot remove the owner from the search space", + detail="Cannot remove the owner from the workspace", ) await session.delete(db_membership) @@ -730,25 +730,25 @@ async def remove_member( @router.post( - "/searchspaces/{search_space_id}/invites", + "/workspaces/{workspace_id}/invites", response_model=InviteRead, ) async def create_invite( - search_space_id: int, + workspace_id: int, invite_data: InviteCreate, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ - Create a new invite link for a search space. + Create a new invite link for a workspace. Requires MEMBERS_INVITE permission. """ try: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.MEMBERS_INVITE.value, "You don't have permission to create invites", ) @@ -756,21 +756,21 @@ async def create_invite( # Verify role exists if specified if invite_data.role_id: role_result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.id == invite_data.role_id, - SearchSpaceRole.search_space_id == search_space_id, + select(WorkspaceRole).filter( + WorkspaceRole.id == invite_data.role_id, + WorkspaceRole.workspace_id == workspace_id, ) ) if not role_result.scalars().first(): raise HTTPException( status_code=404, - detail="Role not found in this search space", + detail="Role not found in this workspace", ) - db_invite = SearchSpaceInvite( + db_invite = WorkspaceInvite( **invite_data.model_dump(), invite_code=generate_invite_code(), - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user.id, ) session.add(db_invite) @@ -778,9 +778,9 @@ async def create_invite( # Reload with role result = await session.execute( - select(SearchSpaceInvite) - .options(selectinload(SearchSpaceInvite.role)) - .filter(SearchSpaceInvite.id == db_invite.id) + select(WorkspaceInvite) + .options(selectinload(WorkspaceInvite.role)) + .filter(WorkspaceInvite.id == db_invite.id) ) db_invite = result.scalars().first() @@ -797,31 +797,31 @@ async def create_invite( @router.get( - "/searchspaces/{search_space_id}/invites", + "/workspaces/{workspace_id}/invites", response_model=list[InviteRead], ) async def list_invites( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - List all invites for a search space. + List all invites for a workspace. Requires MEMBERS_INVITE permission. """ try: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.MEMBERS_INVITE.value, "You don't have permission to view invites", ) result = await session.execute( - select(SearchSpaceInvite) - .options(selectinload(SearchSpaceInvite.role)) - .filter(SearchSpaceInvite.search_space_id == search_space_id) + select(WorkspaceInvite) + .options(selectinload(WorkspaceInvite.role)) + .filter(WorkspaceInvite.workspace_id == workspace_id) ) return result.scalars().all() @@ -834,11 +834,11 @@ async def list_invites( @router.put( - "/searchspaces/{search_space_id}/invites/{invite_id}", + "/workspaces/{workspace_id}/invites/{invite_id}", response_model=InviteRead, ) async def update_invite( - search_space_id: int, + workspace_id: int, invite_id: int, invite_update: InviteUpdate, session: AsyncSession = Depends(get_async_session), @@ -852,17 +852,17 @@ async def update_invite( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.MEMBERS_INVITE.value, "You don't have permission to update invites", ) result = await session.execute( - select(SearchSpaceInvite) - .options(selectinload(SearchSpaceInvite.role)) + select(WorkspaceInvite) + .options(selectinload(WorkspaceInvite.role)) .filter( - SearchSpaceInvite.id == invite_id, - SearchSpaceInvite.search_space_id == search_space_id, + WorkspaceInvite.id == invite_id, + WorkspaceInvite.workspace_id == workspace_id, ) ) db_invite = result.scalars().first() @@ -875,15 +875,15 @@ async def update_invite( # Verify role exists if updating role_id if update_data.get("role_id"): role_result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.id == update_data["role_id"], - SearchSpaceRole.search_space_id == search_space_id, + select(WorkspaceRole).filter( + WorkspaceRole.id == update_data["role_id"], + WorkspaceRole.workspace_id == workspace_id, ) ) if not role_result.scalars().first(): raise HTTPException( status_code=404, - detail="Role not found in this search space", + detail="Role not found in this workspace", ) for key, value in update_data.items(): @@ -903,9 +903,9 @@ async def update_invite( ) from e -@router.delete("/searchspaces/{search_space_id}/invites/{invite_id}") +@router.delete("/workspaces/{workspace_id}/invites/{invite_id}") async def revoke_invite( - search_space_id: int, + workspace_id: int, invite_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -918,15 +918,15 @@ async def revoke_invite( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.MEMBERS_INVITE.value, "You don't have permission to revoke invites", ) result = await session.execute( - select(SearchSpaceInvite).filter( - SearchSpaceInvite.id == invite_id, - SearchSpaceInvite.search_space_id == search_space_id, + select(WorkspaceInvite).filter( + WorkspaceInvite.id == invite_id, + WorkspaceInvite.workspace_id == workspace_id, ) ) db_invite = result.scalars().first() @@ -962,18 +962,18 @@ async def get_invite_info( """ try: result = await session.execute( - select(SearchSpaceInvite) + select(WorkspaceInvite) .options( - selectinload(SearchSpaceInvite.role), - selectinload(SearchSpaceInvite.search_space), + selectinload(WorkspaceInvite.role), + selectinload(WorkspaceInvite.workspace), ) - .filter(SearchSpaceInvite.invite_code == invite_code) + .filter(WorkspaceInvite.invite_code == invite_code) ) invite = result.scalars().first() if not invite: return InviteInfoResponse( - search_space_name="", + workspace_name="", role_name=None, is_valid=False, message="Invite not found", @@ -982,8 +982,8 @@ async def get_invite_info( # Check if invite is still valid if not invite.is_active: return InviteInfoResponse( - search_space_name=invite.search_space.name - if invite.search_space + workspace_name=invite.workspace.name + if invite.workspace else "", role_name=invite.role.name if invite.role else None, is_valid=False, @@ -992,8 +992,8 @@ async def get_invite_info( if invite.expires_at and invite.expires_at < datetime.now(UTC): return InviteInfoResponse( - search_space_name=invite.search_space.name - if invite.search_space + workspace_name=invite.workspace.name + if invite.workspace else "", role_name=invite.role.name if invite.role else None, is_valid=False, @@ -1002,8 +1002,8 @@ async def get_invite_info( if invite.max_uses and invite.uses_count >= invite.max_uses: return InviteInfoResponse( - search_space_name=invite.search_space.name - if invite.search_space + workspace_name=invite.workspace.name + if invite.workspace else "", role_name=invite.role.name if invite.role else None, is_valid=False, @@ -1011,7 +1011,7 @@ async def get_invite_info( ) return InviteInfoResponse( - search_space_name=invite.search_space.name if invite.search_space else "", + workspace_name=invite.workspace.name if invite.workspace else "", role_name=invite.role.name if invite.role else "Default", is_valid=True, ) @@ -1031,16 +1031,16 @@ async def accept_invite( ): user = auth.user """ - Accept an invite and join a search space. + Accept an invite and join a workspace. """ try: result = await session.execute( - select(SearchSpaceInvite) + select(WorkspaceInvite) .options( - selectinload(SearchSpaceInvite.role), - selectinload(SearchSpaceInvite.search_space), + selectinload(WorkspaceInvite.role), + selectinload(WorkspaceInvite.workspace), ) - .filter(SearchSpaceInvite.invite_code == request.invite_code) + .filter(WorkspaceInvite.invite_code == request.invite_code) ) invite = result.scalars().first() @@ -1063,28 +1063,28 @@ async def accept_invite( # Check if user is already a member existing_membership = await session.execute( - select(SearchSpaceMembership).filter( - SearchSpaceMembership.user_id == user.id, - SearchSpaceMembership.search_space_id == invite.search_space_id, + select(WorkspaceMembership).filter( + WorkspaceMembership.user_id == user.id, + WorkspaceMembership.workspace_id == invite.workspace_id, ) ) if existing_membership.scalars().first(): raise HTTPException( status_code=400, - detail="You are already a member of this search space", + detail="You are already a member of this workspace", ) # Determine role to assign role_id = invite.role_id if not role_id: # Use default role - default_role = await get_default_role(session, invite.search_space_id) + default_role = await get_default_role(session, invite.workspace_id) role_id = default_role.id if default_role else None # Create membership - membership = SearchSpaceMembership( + membership = WorkspaceMembership( user_id=user.id, - search_space_id=invite.search_space_id, + workspace_id=invite.workspace_id, role_id=role_id, is_owner=False, invited_by_invite_id=invite.id, @@ -1097,12 +1097,12 @@ async def accept_invite( await session.commit() role_name = invite.role.name if invite.role else "Default" - search_space_name = invite.search_space.name if invite.search_space else "" + workspace_name = invite.workspace.name if invite.workspace else "" return InviteAcceptResponse( - message="Successfully joined the search space", - search_space_id=invite.search_space_id, - search_space_name=search_space_name, + message="Successfully joined the workspace", + workspace_id=invite.workspace_id, + workspace_name=workspace_name, role_name=role_name, ) @@ -1120,33 +1120,33 @@ async def accept_invite( @router.get( - "/searchspaces/{search_space_id}/my-access", - response_model=UserSearchSpaceAccess, + "/workspaces/{workspace_id}/my-access", + response_model=UserWorkspaceAccess, ) async def get_my_access( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ - Get the current user's access info for a search space. + Get the current user's access info for a workspace. """ try: - membership = await check_search_space_access(session, auth, search_space_id) + membership = await check_workspace_access(session, auth, workspace_id) - # Get search space name + # Get workspace name result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - search_space = result.scalars().first() + workspace = result.scalars().first() # Get permissions - permissions = await get_user_permissions(session, user.id, search_space_id) + permissions = await get_user_permissions(session, user.id, workspace_id) - return UserSearchSpaceAccess( - search_space_id=search_space_id, - search_space_name=search_space.name if search_space else "", + return UserWorkspaceAccess( + workspace_id=workspace_id, + workspace_name=workspace.name if workspace else "", is_owner=membership.is_owner, role_name=membership.role.name if membership.role else None, permissions=permissions, diff --git a/surfsense_backend/app/routes/reports_routes.py b/surfsense_backend/app/routes/reports_routes.py index bdcf8a874..2fc917b92 100644 --- a/surfsense_backend/app/routes/reports_routes.py +++ b/surfsense_backend/app/routes/reports_routes.py @@ -8,7 +8,7 @@ Export is on-demand in multiple formats (PDF, DOCX, HTML, LaTeX, EPUB, ODT, plain text). PDF uses pypandoc (Markdown->Typst) + typst-py; the rest use pypandoc directly with format-specific templates and options. -Authorization: lightweight search-space membership checks (no granular RBAC) +Authorization: lightweight workspace membership checks (no granular RBAC) since reports are chat-generated artifacts, not standalone managed resources. """ @@ -31,8 +31,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.auth.context import AuthContext from app.db import ( Report, - SearchSpace, - SearchSpaceMembership, + Workspace, + WorkspaceMembership, get_async_session, ) from app.schemas import ReportContentRead, ReportContentUpdate, ReportRead @@ -43,7 +43,7 @@ from app.templates.export_helpers import ( get_typst_template_path, ) from app.users import get_auth_context -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access logger = logging.getLogger(__name__) @@ -160,7 +160,7 @@ async def _get_report_with_access( session: AsyncSession, auth: AuthContext, ) -> Report: - """Fetch a report and verify the user belongs to its search space. + """Fetch a report and verify the user belongs to its workspace. Raises HTTPException(404) if not found, HTTPException(403) if no access. """ @@ -171,8 +171,8 @@ async def _get_report_with_access( raise HTTPException(status_code=404, detail="Report not found") # Lightweight membership check - no granular RBAC, just "is the user a - # member of the search space this report belongs to?" - await check_search_space_access(session, auth, report.search_space_id) + # member of the workspace this report belongs to?" + await check_workspace_access(session, auth, report.workspace_id) return report @@ -204,23 +204,23 @@ async def _get_version_siblings( async def read_reports( skip: int = Query(default=0, ge=0), limit: int = Query(default=100, ge=1, le=MAX_REPORT_LIST_LIMIT), - search_space_id: int | None = None, + workspace_id: int | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ List reports the user has access to. - Filters by search space membership. + Filters by workspace membership. """ try: - if search_space_id is not None: - # Verify the caller is a member of the requested search space - await check_search_space_access(session, auth, search_space_id) + if workspace_id is not None: + # Verify the caller is a member of the requested workspace + await check_workspace_access(session, auth, workspace_id) result = await session.execute( select(Report) - .filter(Report.search_space_id == search_space_id) + .filter(Report.workspace_id == workspace_id) .order_by(Report.id.desc()) .offset(skip) .limit(limit) @@ -228,9 +228,9 @@ async def read_reports( else: result = await session.execute( select(Report) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) .order_by(Report.id.desc()) .offset(skip) .limit(limit) @@ -307,7 +307,7 @@ async def update_report_content( """ Update the Markdown content of a report. - The caller must be a member of the search space the report belongs to. + The caller must be a member of the workspace the report belongs to. Returns the updated report content including version siblings. """ try: diff --git a/surfsense_backend/app/routes/sandbox_routes.py b/surfsense_backend/app/routes/sandbox_routes.py index c04abe9ee..8ac0a8b90 100644 --- a/surfsense_backend/app/routes/sandbox_routes.py +++ b/surfsense_backend/app/routes/sandbox_routes.py @@ -70,7 +70,7 @@ async def download_sandbox_file( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.CHATS_READ.value, "You don't have permission to access files in this thread", ) diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index 718b4b907..33da72417 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -1,21 +1,21 @@ """ SearchSourceConnector routes for CRUD operations: POST /search-source-connectors/ - Create a new connector -GET /search-source-connectors/ - List all connectors for the current user (optionally filtered by search space) +GET /search-source-connectors/ - List all connectors for the current user (optionally filtered by workspace) GET /search-source-connectors/{connector_id} - Get a specific connector 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 +POST /search-source-connectors/{connector_id}/index - Index content from a connector to a workspace MCP (Model Context Protocol) Connector routes: POST /connectors/mcp - Create a new MCP connector with custom API tools -GET /connectors/mcp - List all MCP connectors for the current user's search space +GET /connectors/mcp - List all MCP connectors for the current user's workspace GET /connectors/mcp/{connector_id} - Get a specific MCP connector with tools config PUT /connectors/mcp/{connector_id} - Update an MCP connector's tools config DELETE /connectors/mcp/{connector_id} - Delete an MCP connector -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. +Note: OAuth connectors (Gmail, Drive, Slack, etc.) support multiple accounts per workspace. +Non-OAuth connectors (BookStack, GitHub, etc.) are limited to one per workspace. """ import asyncio @@ -170,8 +170,8 @@ async def list_github_repositories( @router.post("/search-source-connectors", response_model=SearchSourceConnectorRead) async def create_search_source_connector( connector: SearchSourceConnectorCreate, - search_space_id: int = Query( - ..., description="ID of the search space to associate the connector with" + workspace_id: int = Query( + ..., description="ID of the workspace to associate the connector with" ), session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), @@ -181,7 +181,7 @@ async def create_search_source_connector( Create a new search source connector. Requires CONNECTORS_CREATE permission. - Each search space can have only one connector of each type (based on search_space_id and connector_type). + Each workspace can have only one connector of each type (based on workspace_id and connector_type). The config must contain the appropriate keys for the connector type. """ try: @@ -189,18 +189,18 @@ async def create_search_source_connector( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.CONNECTORS_CREATE.value, - "You don't have permission to create connectors in this search space", + "You don't have permission to create connectors in this workspace", ) - # Check if a connector with the same type already exists for this search space + # Check if a connector with the same type already exists for this workspace # (for non-OAuth connectors that don't support multiple accounts) # Exception: MCP_CONNECTOR can have multiple instances with different names if connector.connector_type != SearchSourceConnectorType.MCP_CONNECTOR: result = await session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.connector_type == connector.connector_type, ) ) @@ -208,7 +208,7 @@ async def create_search_source_connector( if existing_connector: raise HTTPException( status_code=409, - detail=f"A connector with type {connector.connector_type} already exists in this search space.", + detail=f"A connector with type {connector.connector_type} already exists in this workspace.", ) # Prepare connector data @@ -217,7 +217,7 @@ async def create_search_source_connector( # MCP connectors support multiple instances — ensure unique name if connector.connector_type == SearchSourceConnectorType.MCP_CONNECTOR: connector_data["name"] = await ensure_unique_connector_name( - session, connector_data["name"], search_space_id, user.id + session, connector_data["name"], workspace_id, user.id ) # Automatically set next_scheduled_at if periodic indexing is enabled @@ -231,7 +231,7 @@ async def create_search_source_connector( ) db_connector = SearchSourceConnector( - **connector_data, search_space_id=search_space_id, user_id=user.id + **connector_data, workspace_id=workspace_id, user_id=user.id ) session.add(db_connector) await session.commit() @@ -244,7 +244,7 @@ async def create_search_source_connector( ): success = create_periodic_schedule( connector_id=db_connector.id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=str(user.id), connector_type=db_connector.connector_type, frequency_minutes=db_connector.indexing_frequency_minutes, @@ -263,7 +263,7 @@ async def create_search_source_connector( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists in this search space. {e!s}", + detail=f"Integrity error: A connector with this type already exists in this workspace. {e!s}", ) from e except HTTPException: await session.rollback() @@ -281,32 +281,32 @@ async def create_search_source_connector( async def read_search_source_connectors( skip: int = 0, limit: int = 100, - search_space_id: int | None = None, + workspace_id: int | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - List all search source connectors for a search space. + List all search source connectors for a workspace. Requires CONNECTORS_READ permission. """ try: - if search_space_id is None: + if workspace_id is None: raise HTTPException( status_code=400, - detail="search_space_id is required", + detail="workspace_id is required", ) # Check if user has permission to read connectors await check_permission( session, auth, - search_space_id, + workspace_id, Permission.CONNECTORS_READ.value, - "You don't have permission to view connectors in this search space", + "You don't have permission to view connectors in this workspace", ) query = select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id + SearchSourceConnector.workspace_id == workspace_id ) result = await session.execute(query.offset(skip).limit(limit)) @@ -348,7 +348,7 @@ async def read_search_source_connector( await check_permission( session, auth, - connector.search_space_id, + connector.workspace_id, Permission.CONNECTORS_READ.value, "You don't have permission to view this connector", ) @@ -390,7 +390,7 @@ async def update_search_source_connector( await check_permission( session, auth, - db_connector.search_space_id, + db_connector.workspace_id, Permission.CONNECTORS_UPDATE.value, "You don't have permission to update this connector", ) @@ -489,8 +489,8 @@ async def update_search_source_connector( if key == "connector_type" and value != db_connector.connector_type: check_result = await session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id - == db_connector.search_space_id, + SearchSourceConnector.workspace_id + == db_connector.workspace_id, SearchSourceConnector.connector_type == value, SearchSourceConnector.id != connector_id, ) @@ -499,7 +499,7 @@ async def update_search_source_connector( if existing_connector: raise HTTPException( status_code=409, - detail=f"A connector with type {value} already exists in this search space.", + detail=f"A connector with type {value} already exists in this workspace.", ) setattr(db_connector, key, value) @@ -520,7 +520,7 @@ async def update_search_source_connector( # Create or update the periodic schedule success = update_periodic_schedule( connector_id=db_connector.id, - search_space_id=db_connector.search_space_id, + workspace_id=db_connector.workspace_id, user_id=str(user.id), connector_type=db_connector.connector_type, frequency_minutes=db_connector.indexing_frequency_minutes, @@ -592,7 +592,7 @@ async def delete_search_source_connector( await check_permission( session, auth, - db_connector.search_space_id, + db_connector.workspace_id, Permission.CONNECTORS_DELETE.value, "You don't have permission to delete this connector", ) @@ -672,7 +672,7 @@ async def delete_search_source_connector( ) # Delete the connector record - search_space_id = db_connector.search_space_id + workspace_id = db_connector.workspace_id is_mcp = db_connector.connector_type == SearchSourceConnectorType.MCP_CONNECTOR await session.delete(db_connector) await session.commit() @@ -682,7 +682,7 @@ async def delete_search_source_connector( invalidate_mcp_tools_cache, ) - invalidate_mcp_tools_cache(search_space_id) + invalidate_mcp_tools_cache(workspace_id) logger.info( f"Connector {connector_id} ({connector_name}) deleted successfully. " @@ -712,8 +712,8 @@ async def delete_search_source_connector( ) async def index_connector_content( connector_id: int, - search_space_id: int = Query( - ..., description="ID of the search space to store indexed content" + workspace_id: int = Query( + ..., description="ID of the workspace to store indexed content" ), start_date: str = Query( None, @@ -732,7 +732,7 @@ async def index_connector_content( ): user = auth.user """ - Index content from a KB connector to a search space. + Index content from a KB connector to a workspace. Live connectors (Slack, Teams, Linear, Jira, ClickUp, Calendar, Airtable, Gmail, Discord, Luma) use real-time agent tools instead. @@ -749,25 +749,25 @@ async def index_connector_content( if not connector: raise HTTPException(status_code=404, detail="Connector not found") - # Ensure the connector actually belongs to the requested search space. + # Ensure the connector actually belongs to the requested workspace. # Without this, the permission check below would authorize against the - # caller-supplied search_space_id (their own space) while the connector + # caller-supplied workspace_id (their own space) while the connector # lives in another user's space, allowing cross-tenant indexing of a # foreign connector (and use of its stored credentials). Returning 404 # (rather than 403) on a mismatch also avoids disclosing the existence of - # connectors in other search spaces. - if connector.search_space_id != search_space_id: + # connectors in other workspaces. + if connector.workspace_id != workspace_id: raise HTTPException(status_code=404, detail="Connector not found") # Check if user has permission to update connectors (indexing is an update - # operation). Authorize against the connector's OWN search space — matching + # operation). Authorize against the connector's OWN workspace — matching # the read/update/delete handlers — not the client-supplied query param. await check_permission( session, auth, - connector.search_space_id, + connector.workspace_id, Permission.CONNECTORS_UPDATE.value, - "You don't have permission to index content in this search space", + "You don't have permission to index content in this workspace", ) # Handle different connector types @@ -838,7 +838,7 @@ async def index_connector_content( ), "indexing_started": False, "connector_id": connector_id, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "indexing_from": indexing_from, "indexing_to": indexing_to, } @@ -847,10 +847,10 @@ async def index_connector_content( from app.tasks.celery_tasks.connector_tasks import index_notion_pages_task logger.info( - f"Triggering Notion indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}" + f"Triggering Notion indexing for connector {connector_id} into workspace {workspace_id} from {indexing_from} to {indexing_to}" ) index_notion_pages_task.delay( - connector_id, search_space_id, str(user.id), indexing_from, indexing_to + connector_id, workspace_id, str(user.id), indexing_from, indexing_to ) response_message = "Notion indexing started in the background." @@ -858,10 +858,10 @@ async def index_connector_content( from app.tasks.celery_tasks.connector_tasks import index_github_repos_task logger.info( - f"Triggering GitHub indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}" + f"Triggering GitHub indexing for connector {connector_id} into workspace {workspace_id} from {indexing_from} to {indexing_to}" ) index_github_repos_task.delay( - connector_id, search_space_id, str(user.id), indexing_from, indexing_to + connector_id, workspace_id, str(user.id), indexing_from, indexing_to ) response_message = "GitHub indexing started in the background." @@ -871,10 +871,10 @@ async def index_connector_content( ) logger.info( - f"Triggering Confluence indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}" + f"Triggering Confluence indexing for connector {connector_id} into workspace {workspace_id} from {indexing_from} to {indexing_to}" ) index_confluence_pages_task.delay( - connector_id, search_space_id, str(user.id), indexing_from, indexing_to + connector_id, workspace_id, str(user.id), indexing_from, indexing_to ) response_message = "Confluence indexing started in the background." @@ -884,10 +884,10 @@ async def index_connector_content( ) logger.info( - f"Triggering BookStack indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}" + f"Triggering BookStack indexing for connector {connector_id} into workspace {workspace_id} from {indexing_from} to {indexing_to}" ) index_bookstack_pages_task.delay( - connector_id, search_space_id, str(user.id), indexing_from, indexing_to + connector_id, workspace_id, str(user.id), indexing_from, indexing_to ) response_message = "BookStack indexing started in the background." @@ -900,7 +900,7 @@ async def index_connector_content( if drive_items and drive_items.has_items(): logger.info( - f"Triggering Google Drive indexing for connector {connector_id} into search space {search_space_id}, " + f"Triggering Google Drive indexing for connector {connector_id} into workspace {workspace_id}, " f"folders: {len(drive_items.folders)}, files: {len(drive_items.files)}" ) items_dict = drive_items.model_dump() @@ -929,13 +929,13 @@ async def index_connector_content( "indexing_options": indexing_options, } logger.info( - f"Triggering Google Drive indexing for connector {connector_id} into search space {search_space_id} " + f"Triggering Google Drive indexing for connector {connector_id} into workspace {workspace_id} " f"using existing config" ) index_google_drive_files_task.delay( connector_id, - search_space_id, + workspace_id, str(user.id), items_dict, ) @@ -948,7 +948,7 @@ async def index_connector_content( if drive_items and drive_items.has_items(): logger.info( - f"Triggering OneDrive indexing for connector {connector_id} into search space {search_space_id}, " + f"Triggering OneDrive indexing for connector {connector_id} into workspace {workspace_id}, " f"folders: {len(drive_items.folders)}, files: {len(drive_items.files)}" ) items_dict = drive_items.model_dump() @@ -976,13 +976,13 @@ async def index_connector_content( "indexing_options": indexing_options, } logger.info( - f"Triggering OneDrive indexing for connector {connector_id} into search space {search_space_id} " + f"Triggering OneDrive indexing for connector {connector_id} into workspace {workspace_id} " f"using existing config" ) index_onedrive_files_task.delay( connector_id, - search_space_id, + workspace_id, str(user.id), items_dict, ) @@ -995,7 +995,7 @@ async def index_connector_content( if drive_items and drive_items.has_items(): logger.info( - f"Triggering Dropbox indexing for connector {connector_id} into search space {search_space_id}, " + f"Triggering Dropbox indexing for connector {connector_id} into workspace {workspace_id}, " f"folders: {len(drive_items.folders)}, files: {len(drive_items.files)}" ) items_dict = drive_items.model_dump() @@ -1023,13 +1023,13 @@ async def index_connector_content( "indexing_options": indexing_options, } logger.info( - f"Triggering Dropbox indexing for connector {connector_id} into search space {search_space_id} " + f"Triggering Dropbox indexing for connector {connector_id} into workspace {workspace_id} " f"using existing config" ) index_dropbox_files_task.delay( connector_id, - search_space_id, + workspace_id, str(user.id), items_dict, ) @@ -1044,10 +1044,10 @@ async def index_connector_content( ) logger.info( - f"Triggering Elasticsearch indexing for connector {connector_id} into search space {search_space_id}" + f"Triggering Elasticsearch indexing for connector {connector_id} into workspace {workspace_id}" ) index_elasticsearch_documents_task.delay( - connector_id, search_space_id, str(user.id), indexing_from, indexing_to + connector_id, workspace_id, str(user.id), indexing_from, indexing_to ) response_message = "Elasticsearch indexing started in the background." @@ -1068,11 +1068,11 @@ async def index_connector_content( indexing_started = False else: logger.info( - f"Triggering web pages indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}" + f"Triggering web pages indexing for connector {connector_id} into workspace {workspace_id} from {indexing_from} to {indexing_to}" ) index_crawled_urls_task.delay( connector_id, - search_space_id, + workspace_id, str(user.id), indexing_from, indexing_to, @@ -1112,12 +1112,12 @@ async def index_connector_content( await session.refresh(connector) logger.info( - f"Triggering Composio Google Drive indexing for connector {connector_id} into search space {search_space_id}, " + f"Triggering Composio Google Drive indexing for connector {connector_id} into workspace {workspace_id}, " f"folders: {len(drive_items.folders)}, files: {len(drive_items.files)}" ) else: logger.info( - f"Triggering Composio Google Drive indexing for connector {connector_id} into search space {search_space_id} " + f"Triggering Composio Google Drive indexing for connector {connector_id} into workspace {workspace_id} " f"using existing config" ) @@ -1145,7 +1145,7 @@ async def index_connector_content( "indexing_options": indexing_options, } index_google_drive_files_task.delay( - connector_id, search_space_id, str(user.id), items_dict + connector_id, workspace_id, str(user.id), items_dict ) response_message = ( "Composio Google Drive indexing started in the background." @@ -1160,10 +1160,10 @@ async def index_connector_content( ) logger.info( - f"Triggering Composio Gmail indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}" + f"Triggering Composio Gmail indexing for connector {connector_id} into workspace {workspace_id} from {indexing_from} to {indexing_to}" ) index_google_gmail_messages_task.delay( - connector_id, search_space_id, str(user.id), indexing_from, indexing_to + connector_id, workspace_id, str(user.id), indexing_from, indexing_to ) response_message = "Composio Gmail indexing started in the background." @@ -1176,10 +1176,10 @@ async def index_connector_content( ) logger.info( - f"Triggering Composio Google Calendar indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}" + f"Triggering Composio Google Calendar indexing for connector {connector_id} into workspace {workspace_id} from {indexing_from} to {indexing_to}" ) index_google_calendar_events_task.delay( - connector_id, search_space_id, str(user.id), indexing_from, indexing_to + connector_id, workspace_id, str(user.id), indexing_from, indexing_to ) response_message = ( "Composio Google Calendar indexing started in the background." @@ -1195,7 +1195,7 @@ async def index_connector_content( "message": response_message, "indexing_started": indexing_started, "connector_id": connector_id, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "indexing_from": indexing_from, "indexing_to": indexing_to, } @@ -1292,7 +1292,7 @@ async def _persist_auth_expired(session: AsyncSession, connector_id: int) -> Non async def _run_indexing_with_notifications( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -1307,7 +1307,7 @@ async def _run_indexing_with_notifications( Args: session: Database session connector_id: ID of the connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -1359,7 +1359,7 @@ async def _run_indexing_with_notifications( connector_id=connector_id, connector_name=connector.name, connector_type=connector.connector_type.value, - search_space_id=search_space_id, + workspace_id=workspace_id, start_date=start_date, end_date=end_date, ) @@ -1476,7 +1476,7 @@ async def _run_indexing_with_notifications( indexing_kwargs = { "session": session, "connector_id": connector_id, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "user_id": user_id, "start_date": start_date, "end_date": end_date, @@ -1717,7 +1717,7 @@ async def _run_indexing_with_notifications( async def run_notion_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -1732,7 +1732,7 @@ async def run_notion_indexing_with_new_session( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -1746,7 +1746,7 @@ async def run_notion_indexing_with_new_session( async def run_notion_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -1757,7 +1757,7 @@ async def run_notion_indexing( Args: session: Database session connector_id: ID of the Notion connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -1767,7 +1767,7 @@ async def run_notion_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -1781,18 +1781,18 @@ async def run_notion_indexing( # Add new helper functions for GitHub indexing async def run_github_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, ): """Wrapper to run GitHub indexing with its own database session.""" logger.info( - f"Background task started: Indexing GitHub connector {connector_id} into space {search_space_id} from {start_date} to {end_date}" + f"Background task started: Indexing GitHub connector {connector_id} into space {workspace_id} from {start_date} to {end_date}" ) async with async_session_maker() as session: await run_github_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) logger.info(f"Background task finished: Indexing GitHub connector {connector_id}") @@ -1800,7 +1800,7 @@ async def run_github_indexing_with_new_session( async def run_github_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -1811,7 +1811,7 @@ async def run_github_indexing( Args: session: Database session connector_id: ID of the GitHub connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -1821,7 +1821,7 @@ async def run_github_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -1834,18 +1834,18 @@ async def run_github_indexing( # Add new helper functions for Confluence indexing async def run_confluence_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, ): """Wrapper to run Confluence indexing with its own database session.""" logger.info( - f"Background task started: Indexing Confluence connector {connector_id} into space {search_space_id} from {start_date} to {end_date}" + f"Background task started: Indexing Confluence connector {connector_id} into space {workspace_id} from {start_date} to {end_date}" ) async with async_session_maker() as session: await run_confluence_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) logger.info( f"Background task finished: Indexing Confluence connector {connector_id}" @@ -1855,7 +1855,7 @@ async def run_confluence_indexing_with_new_session( async def run_confluence_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -1866,7 +1866,7 @@ async def run_confluence_indexing( Args: session: Database session connector_id: ID of the Confluence connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -1876,7 +1876,7 @@ async def run_confluence_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -1889,18 +1889,18 @@ async def run_confluence_indexing( # Add new helper functions for Google Calendar indexing async def run_google_calendar_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, ): """Wrapper to run Google Calendar indexing with its own database session.""" logger.info( - f"Background task started: Indexing Google Calendar connector {connector_id} into space {search_space_id} from {start_date} to {end_date}" + f"Background task started: Indexing Google Calendar connector {connector_id} into space {workspace_id} from {start_date} to {end_date}" ) async with async_session_maker() as session: await run_google_calendar_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) logger.info( f"Background task finished: Indexing Google Calendar connector {connector_id}" @@ -1910,7 +1910,7 @@ async def run_google_calendar_indexing_with_new_session( async def run_google_calendar_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -1921,7 +1921,7 @@ async def run_google_calendar_indexing( Args: session: Database session connector_id: ID of the Google Calendar connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -1931,7 +1931,7 @@ async def run_google_calendar_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -1943,7 +1943,7 @@ async def run_google_calendar_indexing( async def run_google_gmail_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -1954,14 +1954,14 @@ async def run_google_gmail_indexing_with_new_session( """ async with async_session_maker() as session: await run_google_gmail_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) async def run_google_gmail_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -1972,7 +1972,7 @@ async def run_google_gmail_indexing( Args: session: Database session connector_id: ID of the Google Gmail connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -1983,7 +1983,7 @@ async def run_google_gmail_indexing( async def gmail_indexing_wrapper( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None, end_date: str | None, @@ -1994,7 +1994,7 @@ async def run_google_gmail_indexing( indexed_count, skipped_count, error_message = await index_google_gmail_messages( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -2007,7 +2007,7 @@ async def run_google_gmail_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -2020,7 +2020,7 @@ async def run_google_gmail_indexing( async def run_google_drive_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, # Dictionary with 'folders', 'files', and 'indexing_options' ): @@ -2057,7 +2057,7 @@ async def run_google_drive_indexing( connector_id=connector_id, connector_name=connector.name, connector_type=connector.connector_type.value, - search_space_id=search_space_id, + workspace_id=workspace_id, folder_count=len(items.folders), file_count=len(items.files), folder_names=items.get_folder_names() if items.folders else None, @@ -2086,7 +2086,7 @@ async def run_google_drive_indexing( ) = await index_google_drive_files( session, connector_id, - search_space_id, + workspace_id, user_id, folder_id=folder.id, folder_name=folder.name, @@ -2119,7 +2119,7 @@ async def run_google_drive_indexing( ) = await index_google_drive_selected_files( session, connector_id, - search_space_id, + workspace_id, user_id, files=file_tuples, ) @@ -2199,7 +2199,7 @@ async def run_google_drive_indexing( async def run_onedrive_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, ): @@ -2224,7 +2224,7 @@ async def run_onedrive_indexing( connector_id=connector_id, connector_name=connector.name, connector_type=connector.connector_type.value, - search_space_id=search_space_id, + workspace_id=workspace_id, folder_count=len(items_dict.get("folders", [])), file_count=len(items_dict.get("files", [])), folder_names=[ @@ -2251,7 +2251,7 @@ async def run_onedrive_indexing( ) = await index_onedrive_files( session, connector_id, - search_space_id, + workspace_id, user_id, items_dict, ) @@ -2313,7 +2313,7 @@ async def run_onedrive_indexing( async def run_dropbox_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, ): @@ -2338,7 +2338,7 @@ async def run_dropbox_indexing( connector_id=connector_id, connector_name=connector.name, connector_type=connector.connector_type.value, - search_space_id=search_space_id, + workspace_id=workspace_id, folder_count=len(items_dict.get("folders", [])), file_count=len(items_dict.get("files", [])), folder_names=[ @@ -2365,7 +2365,7 @@ async def run_dropbox_indexing( ) = await index_dropbox_files( session, connector_id, - search_space_id, + workspace_id, user_id, items_dict, ) @@ -2426,18 +2426,18 @@ async def run_dropbox_indexing( async def run_elasticsearch_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, ): """Wrapper to run Elasticsearch indexing with its own database session.""" logger.info( - f"Background task started: Indexing Elasticsearch connector {connector_id} into space {search_space_id}" + f"Background task started: Indexing Elasticsearch connector {connector_id} into space {workspace_id}" ) async with async_session_maker() as session: await run_elasticsearch_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) logger.info( f"Background task finished: Indexing Elasticsearch connector {connector_id}" @@ -2447,7 +2447,7 @@ async def run_elasticsearch_indexing_with_new_session( async def run_elasticsearch_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -2458,7 +2458,7 @@ async def run_elasticsearch_indexing( Args: session: Database session connector_id: ID of the Elasticsearch connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -2468,7 +2468,7 @@ async def run_elasticsearch_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -2481,7 +2481,7 @@ async def run_elasticsearch_indexing( # Add new helper functions for crawled web page indexing async def run_web_page_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -2492,14 +2492,14 @@ async def run_web_page_indexing_with_new_session( """ async with async_session_maker() as session: await run_web_page_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) async def run_web_page_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -2510,7 +2510,7 @@ async def run_web_page_indexing( Args: session: Database session connector_id: ID of the webcrawler connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -2520,7 +2520,7 @@ async def run_web_page_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -2533,18 +2533,18 @@ async def run_web_page_indexing( # Add new helper functions for BookStack indexing async def run_bookstack_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, ): """Wrapper to run BookStack indexing with its own database session.""" logger.info( - f"Background task started: Indexing BookStack connector {connector_id} into space {search_space_id} from {start_date} to {end_date}" + f"Background task started: Indexing BookStack connector {connector_id} into space {workspace_id} from {start_date} to {end_date}" ) async with async_session_maker() as session: await run_bookstack_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) logger.info( f"Background task finished: Indexing BookStack connector {connector_id}" @@ -2554,7 +2554,7 @@ async def run_bookstack_indexing_with_new_session( async def run_bookstack_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -2565,7 +2565,7 @@ async def run_bookstack_indexing( Args: session: Database session connector_id: ID of the BookStack connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -2575,7 +2575,7 @@ async def run_bookstack_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -2587,7 +2587,7 @@ async def run_bookstack_indexing( async def run_composio_indexing_with_new_session( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -2598,14 +2598,14 @@ async def run_composio_indexing_with_new_session( """ async with async_session_maker() as session: await run_composio_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) async def run_composio_indexing( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None, end_date: str | None, @@ -2619,7 +2619,7 @@ async def run_composio_indexing( Args: session: Database session connector_id: ID of the Composio connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for indexing end_date: End date for indexing @@ -2629,7 +2629,7 @@ async def run_composio_indexing( await _run_indexing_with_notifications( session=session, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, start_date=start_date, end_date=end_date, @@ -2647,7 +2647,7 @@ async def run_composio_indexing( @router.post("/connectors/mcp", response_model=MCPConnectorRead, status_code=201) async def create_mcp_connector( connector_data: MCPConnectorCreate, - search_space_id: int = Query(..., description="Search space ID"), + workspace_id: int = Query(..., description="Workspace ID"), session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): @@ -2660,7 +2660,7 @@ async def create_mcp_connector( Args: connector_data: MCP server configuration (command, args, env) - search_space_id: ID of the search space to attach the connector to + workspace_id: ID of the workspace to attach the connector to session: Database session user: Current authenticated user @@ -2668,21 +2668,21 @@ async def create_mcp_connector( Created MCP connector with server configuration Raises: - HTTPException: If search space not found or permission denied + HTTPException: If workspace not found or permission denied """ try: # Check user has permission to create connectors await check_permission( session, auth, - search_space_id, + workspace_id, Permission.CONNECTORS_CREATE.value, - "You don't have permission to create connectors in this search space", + "You don't have permission to create connectors in this workspace", ) - # Ensure unique name across MCP connectors in this search space + # Ensure unique name across MCP connectors in this workspace unique_name = await ensure_unique_connector_name( - session, connector_data.name, search_space_id, user.id + session, connector_data.name, workspace_id, user.id ) # Create the connector with single server config @@ -2693,7 +2693,7 @@ async def create_mcp_connector( config={"server_config": connector_data.server_config.model_dump()}, periodic_indexing_enabled=False, indexing_frequency_minutes=None, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user.id, ) @@ -2703,14 +2703,14 @@ async def create_mcp_connector( logger.info( f"Created MCP connector {db_connector.id} " - f"for user {user.id} in search space {search_space_id}" + f"for user {user.id} in workspace {workspace_id}" ) from app.agents.chat.multi_agent_chat.shared.tools.mcp.cache import ( refresh_mcp_tools_cache_for_connector, ) - refresh_mcp_tools_cache_for_connector(db_connector.id, search_space_id) + refresh_mcp_tools_cache_for_connector(db_connector.id, workspace_id) connector_read = SearchSourceConnectorRead.model_validate(db_connector) return MCPConnectorRead.from_connector(connector_read) @@ -2727,15 +2727,15 @@ async def create_mcp_connector( @router.get("/connectors/mcp", response_model=list[MCPConnectorRead]) async def list_mcp_connectors( - search_space_id: int = Query(..., description="Search space ID"), + workspace_id: int = Query(..., description="Workspace ID"), session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - List all MCP connectors for a search space. + List all MCP connectors for a workspace. Args: - search_space_id: ID of the search space + workspace_id: ID of the workspace session: Database session user: Current authenticated user @@ -2747,9 +2747,9 @@ async def list_mcp_connectors( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.CONNECTORS_READ.value, - "You don't have permission to view connectors in this search space", + "You don't have permission to view connectors in this workspace", ) # Fetch MCP connectors @@ -2757,7 +2757,7 @@ async def list_mcp_connectors( select(SearchSourceConnector).filter( SearchSourceConnector.connector_type == SearchSourceConnectorType.MCP_CONNECTOR, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, ) ) @@ -2811,7 +2811,7 @@ async def get_mcp_connector( await check_permission( session, auth, - connector.search_space_id, + connector.workspace_id, Permission.CONNECTORS_READ.value, "You don't have permission to view this connector", ) @@ -2865,7 +2865,7 @@ async def update_mcp_connector( await check_permission( session, auth, - connector.search_space_id, + connector.workspace_id, Permission.CONNECTORS_UPDATE.value, "You don't have permission to update this connector", ) @@ -2890,7 +2890,7 @@ async def update_mcp_connector( refresh_mcp_tools_cache_for_connector, ) - refresh_mcp_tools_cache_for_connector(connector.id, connector.search_space_id) + refresh_mcp_tools_cache_for_connector(connector.id, connector.workspace_id) connector_read = SearchSourceConnectorRead.model_validate(connector) return MCPConnectorRead.from_connector(connector_read) @@ -2937,12 +2937,12 @@ async def delete_mcp_connector( await check_permission( session, auth, - connector.search_space_id, + connector.workspace_id, Permission.CONNECTORS_DELETE.value, "You don't have permission to delete this connector", ) - search_space_id = connector.search_space_id + workspace_id = connector.workspace_id await session.delete(connector) await session.commit() @@ -2950,7 +2950,7 @@ async def delete_mcp_connector( invalidate_mcp_tools_cache, ) - invalidate_mcp_tools_cache(search_space_id) + invalidate_mcp_tools_cache(workspace_id) logger.info(f"Deleted MCP connector {connector_id}") @@ -3060,7 +3060,7 @@ async def get_drive_picker_token( await check_permission( session, auth, - connector.search_space_id, + connector.workspace_id, Permission.CONNECTORS_READ.value, "You don't have permission to access this connector", ) @@ -3143,7 +3143,7 @@ async def _ensure_mcp_connector_for_user( The JSONB ``has_key("server_config")`` filter is the same MCP marker used elsewhere in this module. - Returns the connector's ``search_space_id`` (needed downstream for + Returns the connector's ``workspace_id`` (needed downstream for MCP tool cache invalidation). Raises ``HTTPException(404)`` when the connector does not exist, is not owned by the user, or is not MCP-backed. @@ -3152,16 +3152,16 @@ async def _ensure_mcp_connector_for_user( from sqlalchemy.dialects.postgresql import JSONB as PG_JSONB result = await session.execute( - select(SearchSourceConnector.search_space_id).where( + select(SearchSourceConnector.workspace_id).where( SearchSourceConnector.id == connector_id, SearchSourceConnector.user_id == user_id, cast(SearchSourceConnector.config, PG_JSONB).has_key("server_config"), ) ) - search_space_id = result.scalar_one_or_none() - if search_space_id is None: + workspace_id = result.scalar_one_or_none() + if workspace_id is None: raise HTTPException(status_code=404, detail="MCP connector not found") - return search_space_id + return workspace_id @router.post("/connectors/mcp/{connector_id}/trust-tool") @@ -3185,7 +3185,7 @@ async def trust_mcp_tool( from app.services.user_tool_allowlist import add_user_trust try: - search_space_id = await _ensure_mcp_connector_for_user( + workspace_id = await _ensure_mcp_connector_for_user( session, user_id=user.id, connector_id=connector_id ) trusted = await add_user_trust( @@ -3195,7 +3195,7 @@ async def trust_mcp_tool( tool_name=body.tool_name, ) await session.commit() - invalidate_mcp_tools_cache(search_space_id) + invalidate_mcp_tools_cache(workspace_id) return {"status": "ok", "trusted_tools": trusted} except HTTPException: @@ -3228,7 +3228,7 @@ async def untrust_mcp_tool( from app.services.user_tool_allowlist import remove_user_trust try: - search_space_id = await _ensure_mcp_connector_for_user( + workspace_id = await _ensure_mcp_connector_for_user( session, user_id=user.id, connector_id=connector_id ) trusted = await remove_user_trust( @@ -3238,7 +3238,7 @@ async def untrust_mcp_tool( tool_name=body.tool_name, ) await session.commit() - invalidate_mcp_tools_cache(search_space_id) + invalidate_mcp_tools_cache(workspace_id) return {"status": "ok", "trusted_tools": trusted} except HTTPException: diff --git a/surfsense_backend/app/routes/slack_add_connector_route.py b/surfsense_backend/app/routes/slack_add_connector_route.py index ee6f75417..10941dc5b 100644 --- a/surfsense_backend/app/routes/slack_add_connector_route.py +++ b/surfsense_backend/app/routes/slack_add_connector_route.py @@ -32,7 +32,7 @@ from app.utils.connector_naming import ( generate_unique_connector_name, ) from app.utils.oauth_security import OAuthStateManager, TokenEncryption -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access logger = logging.getLogger(__name__) @@ -87,7 +87,7 @@ async def connect_slack( Initiate Slack OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -319,7 +319,7 @@ async def slack_callback( connector_type=SearchSourceConnectorType.SLACK_CONNECTOR, is_indexable=False, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) session.add(new_connector) @@ -565,7 +565,7 @@ async def get_slack_channels( detail="Slack connector not found or access denied", ) - await check_search_space_access(session, auth, connector.search_space_id) + await check_workspace_access(session, auth, connector.workspace_id) # Get credentials and decrypt bot token credentials = SlackAuthCredentialsBase.from_dict(connector.config) diff --git a/surfsense_backend/app/routes/stripe_routes.py b/surfsense_backend/app/routes/stripe_routes.py index 288e38cc2..459b23d60 100644 --- a/surfsense_backend/app/routes/stripe_routes.py +++ b/surfsense_backend/app/routes/stripe_routes.py @@ -65,7 +65,7 @@ def _ensure_credit_buying_enabled() -> None: ) -def _get_checkout_urls(search_space_id: int) -> tuple[str, str]: +def _get_checkout_urls(workspace_id: int) -> tuple[str, str]: if not config.NEXT_FRONTEND_URL: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, @@ -79,10 +79,10 @@ def _get_checkout_urls(search_space_id: int) -> tuple[str, str]: # webhook-vs-redirect race where users land on /purchase-success before # checkout.session.completed has been delivered. success_url = ( - f"{base_url}/dashboard/{search_space_id}/purchase-success" + f"{base_url}/dashboard/{workspace_id}/purchase-success" f"?session_id={{CHECKOUT_SESSION_ID}}" ) - cancel_url = f"{base_url}/dashboard/{search_space_id}/purchase-cancel" + cancel_url = f"{base_url}/dashboard/{workspace_id}/purchase-cancel" return success_url, cancel_url @@ -471,7 +471,7 @@ async def create_credit_checkout_session( _ensure_credit_buying_enabled() stripe_client = get_stripe_client() price_id = _get_required_credit_price_id() - success_url, cancel_url = _get_checkout_urls(body.search_space_id) + success_url, cancel_url = _get_checkout_urls(body.workspace_id) credit_micros_granted = body.quantity * config.STRIPE_CREDIT_MICROS_PER_UNIT try: @@ -832,11 +832,11 @@ async def create_auto_reload_setup_session( base_url = config.NEXT_FRONTEND_URL.rstrip("/") success_url = ( - f"{base_url}/dashboard/{body.search_space_id}/user-settings/purchases" + f"{base_url}/dashboard/{body.workspace_id}/user-settings/purchases" f"?auto_reload_setup=success" ) cancel_url = ( - f"{base_url}/dashboard/{body.search_space_id}/user-settings/purchases" + f"{base_url}/dashboard/{body.workspace_id}/user-settings/purchases" f"?auto_reload_setup=cancel" ) diff --git a/surfsense_backend/app/routes/team_memory_routes.py b/surfsense_backend/app/routes/team_memory_routes.py index 76d934cb2..348ac5b18 100644 --- a/surfsense_backend/app/routes/team_memory_routes.py +++ b/surfsense_backend/app/routes/team_memory_routes.py @@ -1,4 +1,4 @@ -"""Routes for search-space team memory.""" +"""Routes for workspace team memory.""" from __future__ import annotations @@ -17,7 +17,7 @@ from app.services.memory import ( save_memory, ) from app.users import get_auth_context -from app.utils.rbac import check_search_space_access +from app.utils.rbac import check_workspace_access router = APIRouter() @@ -26,32 +26,32 @@ class TeamMemoryUpdate(BaseModel): memory_md: str -@router.get("/searchspaces/{search_space_id}/memory", response_model=MemoryRead) +@router.get("/workspaces/{workspace_id}/memory", response_model=MemoryRead) async def get_team_memory( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): - await check_search_space_access(session, auth, search_space_id) + await check_workspace_access(session, auth, workspace_id) memory_md = await read_memory( scope=MemoryScope.TEAM, - target_id=search_space_id, + target_id=workspace_id, session=session, ) return MemoryRead(memory_md=memory_md, limits=memory_limits()) -@router.put("/searchspaces/{search_space_id}/memory", response_model=MemoryRead) +@router.put("/workspaces/{workspace_id}/memory", response_model=MemoryRead) async def update_team_memory( - search_space_id: int, + workspace_id: int, body: TeamMemoryUpdate, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): - await check_search_space_access(session, auth, search_space_id) + await check_workspace_access(session, auth, workspace_id) result = await save_memory( scope=MemoryScope.TEAM, - target_id=search_space_id, + target_id=workspace_id, content=body.memory_md, session=session, ) @@ -60,16 +60,16 @@ async def update_team_memory( return MemoryRead(memory_md=result.memory_md, limits=memory_limits()) -@router.post("/searchspaces/{search_space_id}/memory/reset", response_model=MemoryRead) +@router.post("/workspaces/{workspace_id}/memory/reset", response_model=MemoryRead) async def reset_team_memory( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): - await check_search_space_access(session, auth, search_space_id) + await check_workspace_access(session, auth, workspace_id) result = await reset_memory( scope=MemoryScope.TEAM, - target_id=search_space_id, + target_id=workspace_id, session=session, ) if result.status == "error": diff --git a/surfsense_backend/app/routes/teams_add_connector_route.py b/surfsense_backend/app/routes/teams_add_connector_route.py index 3782b4720..88ffd63d1 100644 --- a/surfsense_backend/app/routes/teams_add_connector_route.py +++ b/surfsense_backend/app/routes/teams_add_connector_route.py @@ -82,7 +82,7 @@ async def connect_teams( Initiate Microsoft Teams OAuth flow. Args: - space_id: The search space ID + space_id: The workspace ID user: Current authenticated user Returns: @@ -327,7 +327,7 @@ async def teams_callback( connector_type=SearchSourceConnectorType.TEAMS_CONNECTOR, is_indexable=False, config=connector_config, - search_space_id=space_id, + workspace_id=space_id, user_id=user_id, ) diff --git a/surfsense_backend/app/routes/video_presentations_routes.py b/surfsense_backend/app/routes/video_presentations_routes.py index e40ccb2f9..2bc7a8ee1 100644 --- a/surfsense_backend/app/routes/video_presentations_routes.py +++ b/surfsense_backend/app/routes/video_presentations_routes.py @@ -19,8 +19,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.auth.context import AuthContext from app.db import ( Permission, - SearchSpace, - SearchSpaceMembership, + Workspace, + WorkspaceMembership, VideoPresentation, get_async_session, ) @@ -35,38 +35,38 @@ router = APIRouter() async def read_video_presentations( skip: int = 0, limit: int = 100, - search_space_id: int | None = None, + workspace_id: int | None = None, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user """ List video presentations the user has access to. - Requires VIDEO_PRESENTATIONS_READ permission for the search space(s). + Requires VIDEO_PRESENTATIONS_READ permission for the workspace(s). """ if skip < 0 or limit < 1: raise HTTPException(status_code=400, detail="Invalid pagination parameters") try: - if search_space_id is not None: + if workspace_id is not None: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.VIDEO_PRESENTATIONS_READ.value, - "You don't have permission to read video presentations in this search space", + "You don't have permission to read video presentations in this workspace", ) result = await session.execute( select(VideoPresentation) - .filter(VideoPresentation.search_space_id == search_space_id) + .filter(VideoPresentation.workspace_id == workspace_id) .offset(skip) .limit(limit) ) else: result = await session.execute( select(VideoPresentation) - .join(SearchSpace) - .join(SearchSpaceMembership) - .filter(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .filter(WorkspaceMembership.user_id == user.id) .offset(skip) .limit(limit) ) @@ -114,9 +114,9 @@ async def read_video_presentation( await check_permission( session, auth, - video_pres.search_space_id, + video_pres.workspace_id, Permission.VIDEO_PRESENTATIONS_READ.value, - "You don't have permission to read video presentations in this search space", + "You don't have permission to read video presentations in this workspace", ) return VideoPresentationRead.from_orm_with_slides(video_pres) @@ -137,7 +137,7 @@ async def delete_video_presentation( ): """ Delete a video presentation. - Requires VIDEO_PRESENTATIONS_DELETE permission for the search space. + Requires VIDEO_PRESENTATIONS_DELETE permission for the workspace. """ try: result = await session.execute( @@ -153,9 +153,9 @@ async def delete_video_presentation( await check_permission( session, auth, - db_video_pres.search_space_id, + db_video_pres.workspace_id, Permission.VIDEO_PRESENTATIONS_DELETE.value, - "You don't have permission to delete video presentations in this search space", + "You don't have permission to delete video presentations in this workspace", ) await session.delete(db_video_pres) @@ -196,9 +196,9 @@ async def stream_slide_audio( await check_permission( session, auth, - video_pres.search_space_id, + video_pres.workspace_id, Permission.VIDEO_PRESENTATIONS_READ.value, - "You don't have permission to access video presentations in this search space", + "You don't have permission to access video presentations in this workspace", ) slides = video_pres.slides or [] diff --git a/surfsense_backend/app/routes/search_spaces_routes.py b/surfsense_backend/app/routes/workspaces_routes.py similarity index 54% rename from surfsense_backend/app/routes/search_spaces_routes.py rename to surfsense_backend/app/routes/workspaces_routes.py index 6eebaf201..721038c6f 100644 --- a/surfsense_backend/app/routes/search_spaces_routes.py +++ b/surfsense_backend/app/routes/workspaces_routes.py @@ -8,21 +8,21 @@ from sqlalchemy.future import select from app.auth.context import AuthContext from app.db import ( Permission, - SearchSpace, - SearchSpaceMembership, - SearchSpaceRole, + Workspace, + WorkspaceMembership, + WorkspaceRole, get_async_session, get_default_roles_config, ) from app.schemas import ( - SearchSpaceApiAccessUpdate, - SearchSpaceCreate, - SearchSpaceRead, - SearchSpaceUpdate, - SearchSpaceWithStats, + WorkspaceApiAccessUpdate, + WorkspaceCreate, + WorkspaceRead, + WorkspaceUpdate, + WorkspaceWithStats, ) from app.users import allow_any_principal, get_auth_context, require_session_context -from app.utils.rbac import check_permission, check_search_space_access +from app.utils.rbac import check_permission, check_workspace_access logger = logging.getLogger(__name__) @@ -31,29 +31,29 @@ router = APIRouter() async def create_default_roles_and_membership( session: AsyncSession, - search_space_id: int, + workspace_id: int, owner_user_id, ) -> None: """ - Create default system roles for a search space and add the owner as a member. + Create default system roles for a workspace and add the owner as a member. Args: session: Database session - search_space_id: The ID of the newly created search space - owner_user_id: The UUID of the user who created the search space + workspace_id: The ID of the newly created workspace + owner_user_id: The UUID of the user who created the workspace """ # Create default roles default_roles = get_default_roles_config() owner_role_id = None for role_config in default_roles: - db_role = SearchSpaceRole( + db_role = WorkspaceRole( name=role_config["name"], description=role_config["description"], permissions=role_config["permissions"], is_default=role_config["is_default"], is_system_role=role_config["is_system_role"], - search_space_id=search_space_id, + workspace_id=workspace_id, ) session.add(db_role) await session.flush() # Get the ID @@ -62,50 +62,50 @@ async def create_default_roles_and_membership( owner_role_id = db_role.id # Create owner membership - owner_membership = SearchSpaceMembership( + owner_membership = WorkspaceMembership( user_id=owner_user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, role_id=owner_role_id, is_owner=True, ) session.add(owner_membership) -@router.post("/searchspaces", response_model=SearchSpaceRead) -async def create_search_space( - search_space: SearchSpaceCreate, +@router.post("/workspaces", response_model=WorkspaceRead) +async def create_workspace( + workspace: WorkspaceCreate, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(require_session_context), ): user = auth.user try: - search_space_data = search_space.model_dump() + workspace_data = workspace.model_dump() # citations_enabled defaults to True (handled by Pydantic schema) # qna_custom_instructions defaults to None/empty (handled by DB) - db_search_space = SearchSpace(**search_space_data, user_id=user.id) - session.add(db_search_space) - await session.flush() # Get the search space ID + db_workspace = Workspace(**workspace_data, user_id=user.id) + session.add(db_workspace) + await session.flush() # Get the workspace ID # Create default roles and owner membership - await create_default_roles_and_membership(session, db_search_space.id, user.id) + await create_default_roles_and_membership(session, db_workspace.id, user.id) await session.commit() - await session.refresh(db_search_space) - return db_search_space + await session.refresh(db_workspace) + return db_workspace except HTTPException: raise except Exception as e: await session.rollback() - logger.error(f"Failed to create search space: {e!s}", exc_info=True) + logger.error(f"Failed to create workspace: {e!s}", exc_info=True) raise HTTPException( - status_code=500, detail=f"Failed to create search space: {e!s}" + status_code=500, detail=f"Failed to create workspace: {e!s}" ) from e -@router.get("/searchspaces", response_model=list[SearchSpaceWithStats]) -async def read_search_spaces( +@router.get("/workspaces", response_model=list[WorkspaceWithStats]) +async def read_workspaces( skip: int = 0, limit: int = 200, owned_only: bool = False, @@ -114,73 +114,73 @@ async def read_search_spaces( ): user = auth.user """ - Get all search spaces the user has access to, with member count and ownership info. + Get all workspaces the user has access to, with member count and ownership info. Args: skip: Number of items to skip limit: Maximum number of items to return - owned_only: If True, only return search spaces owned by the user. - If False (default), return all search spaces the user has access to. + owned_only: If True, only return workspaces owned by the user. + If False (default), return all workspaces the user has access to. """ try: # Exclude spaces that are pending background deletion - not_deleting = ~SearchSpace.name.startswith("[DELETING] ") + not_deleting = ~Workspace.name.startswith("[DELETING] ") api_access_filter = ( - SearchSpace.api_access_enabled == True # noqa: E712 + Workspace.api_access_enabled == True # noqa: E712 if auth.is_gated else True ) if owned_only: - # Return only search spaces where user is the original creator (user_id) + # Return only workspaces where user is the original creator (user_id) result = await session.execute( - select(SearchSpace) - .filter(SearchSpace.user_id == user.id, not_deleting, api_access_filter) - .order_by(SearchSpace.id.asc()) + select(Workspace) + .filter(Workspace.user_id == user.id, not_deleting, api_access_filter) + .order_by(Workspace.id.asc()) .offset(skip) .limit(limit) ) else: - # Return all search spaces the user has membership in + # Return all workspaces the user has membership in result = await session.execute( - select(SearchSpace) - .join(SearchSpaceMembership) + select(Workspace) + .join(WorkspaceMembership) .filter( - SearchSpaceMembership.user_id == user.id, + WorkspaceMembership.user_id == user.id, not_deleting, api_access_filter, ) - .order_by(SearchSpace.id.asc()) + .order_by(Workspace.id.asc()) .offset(skip) .limit(limit) ) - search_spaces = result.scalars().all() + workspaces = result.scalars().all() - # Get member counts and ownership info for each search space - search_spaces_with_stats = [] - for space in search_spaces: + # Get member counts and ownership info for each workspace + workspaces_with_stats = [] + for space in workspaces: # Get member count count_result = await session.execute( - select(func.count(SearchSpaceMembership.id)).filter( - SearchSpaceMembership.search_space_id == space.id + select(func.count(WorkspaceMembership.id)).filter( + WorkspaceMembership.workspace_id == space.id ) ) member_count = count_result.scalar() or 1 # Check if current user is owner ownership_result = await session.execute( - select(SearchSpaceMembership).filter( - SearchSpaceMembership.search_space_id == space.id, - SearchSpaceMembership.user_id == user.id, - SearchSpaceMembership.is_owner == True, # noqa: E712 + select(WorkspaceMembership).filter( + WorkspaceMembership.workspace_id == space.id, + WorkspaceMembership.user_id == user.id, + WorkspaceMembership.is_owner == True, # noqa: E712 ) ) is_owner = ownership_result.scalars().first() is not None - search_spaces_with_stats.append( - SearchSpaceWithStats( + workspaces_with_stats.append( + WorkspaceWithStats( id=space.id, name=space.name, description=space.description, @@ -195,54 +195,54 @@ async def read_search_spaces( ) ) - return search_spaces_with_stats + return workspaces_with_stats except Exception as e: raise HTTPException( - status_code=500, detail=f"Failed to fetch search spaces: {e!s}" + status_code=500, detail=f"Failed to fetch workspaces: {e!s}" ) from e -@router.get("/searchspaces/{search_space_id}", response_model=SearchSpaceRead) -async def read_search_space( - search_space_id: int, +@router.get("/workspaces/{workspace_id}", response_model=WorkspaceRead) +async def read_workspace( + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - Get a specific search space by ID. + Get a specific workspace by ID. Requires SETTINGS_VIEW permission or membership. """ try: # Check if user has access (is a member) - await check_search_space_access(session, auth, search_space_id) + await check_workspace_access(session, auth, workspace_id) result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - search_space = result.scalars().first() + workspace = result.scalars().first() - if not search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") - return search_space + return workspace except HTTPException: raise except Exception as e: raise HTTPException( - status_code=500, detail=f"Failed to fetch search space: {e!s}" + status_code=500, detail=f"Failed to fetch workspace: {e!s}" ) from e -@router.put("/searchspaces/{search_space_id}", response_model=SearchSpaceRead) -async def update_search_space( - search_space_id: int, - search_space_update: SearchSpaceUpdate, +@router.put("/workspaces/{workspace_id}", response_model=WorkspaceRead) +async def update_workspace( + workspace_id: int, + workspace_update: WorkspaceUpdate, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - Update a search space. + Update a workspace. Requires SETTINGS_UPDATE permission. """ try: @@ -250,46 +250,46 @@ async def update_search_space( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.SETTINGS_UPDATE.value, - "You don't have permission to update this search space", + "You don't have permission to update this workspace", ) result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - db_search_space = result.scalars().first() + db_workspace = result.scalars().first() - if not db_search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not db_workspace: + raise HTTPException(status_code=404, detail="Workspace not found") - update_data = search_space_update.model_dump(exclude_unset=True) + update_data = workspace_update.model_dump(exclude_unset=True) for key, value in update_data.items(): - setattr(db_search_space, key, value) + setattr(db_workspace, key, value) await session.commit() - await session.refresh(db_search_space) - return db_search_space + await session.refresh(db_workspace) + return db_workspace except HTTPException: raise except Exception as e: await session.rollback() raise HTTPException( - status_code=500, detail=f"Failed to update search space: {e!s}" + status_code=500, detail=f"Failed to update workspace: {e!s}" ) from e @router.put( - "/searchspaces/{search_space_id}/api-access", response_model=SearchSpaceRead + "/workspaces/{workspace_id}/api-access", response_model=WorkspaceRead ) -async def update_search_space_api_access( - search_space_id: int, - body: SearchSpaceApiAccessUpdate, +async def update_workspace_api_access( + workspace_id: int, + body: WorkspaceApiAccessUpdate, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - Toggle programmatic API/PAT access for a search space. + Toggle programmatic API/PAT access for a workspace. Requires API_ACCESS_MANAGE permission. """ try: @@ -302,23 +302,23 @@ async def update_search_space_api_access( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.API_ACCESS_MANAGE.value, - "You don't have permission to manage API access for this search space", + "You don't have permission to manage API access for this workspace", ) result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - db_search_space = result.scalars().first() + db_workspace = result.scalars().first() - if not db_search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not db_workspace: + raise HTTPException(status_code=404, detail="Workspace not found") - db_search_space.api_access_enabled = body.api_access_enabled + db_workspace.api_access_enabled = body.api_access_enabled await session.commit() - await session.refresh(db_search_space) - return db_search_space + await session.refresh(db_workspace) + return db_workspace except HTTPException: raise except Exception as e: @@ -328,33 +328,33 @@ async def update_search_space_api_access( ) from e -@router.post("/searchspaces/{search_space_id}/ai-sort") +@router.post("/workspaces/{workspace_id}/ai-sort") async def trigger_ai_sort( - search_space_id: int, + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): user = auth.user - """Trigger a full AI file sort for all documents in the search space.""" + """Trigger a full AI file sort for all documents in the workspace.""" try: await check_permission( session, auth, - search_space_id, + workspace_id, Permission.SETTINGS_UPDATE.value, - "You don't have permission to trigger AI sort on this search space", + "You don't have permission to trigger AI sort on this workspace", ) result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - db_search_space = result.scalars().first() - if not db_search_space: - raise HTTPException(status_code=404, detail="Search space not found") + db_workspace = result.scalars().first() + if not db_workspace: + raise HTTPException(status_code=404, detail="Workspace not found") - from app.tasks.celery_tasks.document_tasks import ai_sort_search_space_task + from app.tasks.celery_tasks.document_tasks import ai_sort_workspace_task - ai_sort_search_space_task.delay(search_space_id, str(user.id)) + ai_sort_workspace_task.delay(workspace_id, str(user.id)) return {"message": "AI sort started"} except HTTPException: raise @@ -365,14 +365,14 @@ async def trigger_ai_sort( ) from e -@router.delete("/searchspaces/{search_space_id}", response_model=dict) -async def delete_search_space( - search_space_id: int, +@router.delete("/workspaces/{workspace_id}", response_model=dict) +async def delete_workspace( + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - Delete a search space. + Delete a workspace. Requires SETTINGS_DELETE permission (only owners have this by default). Heavy cascade deletion (documents, chunks, threads, etc.) is dispatched @@ -383,74 +383,74 @@ async def delete_search_space( await check_permission( session, auth, - search_space_id, + workspace_id, Permission.SETTINGS_DELETE.value, - "You don't have permission to delete this search space", + "You don't have permission to delete this workspace", ) result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - db_search_space = result.scalars().first() + db_workspace = result.scalars().first() - if not db_search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not db_workspace: + raise HTTPException(status_code=404, detail="Workspace not found") - if (db_search_space.name or "").startswith("[DELETING] "): + if (db_workspace.name or "").startswith("[DELETING] "): raise HTTPException( status_code=409, - detail="Search space is already being deleted.", + detail="Workspace is already being deleted.", ) # Soft-delete marker (length-safe for String(100)) so users see pending state. prefix = "[DELETING] " max_len = 100 available = max_len - len(prefix) - base_name = db_search_space.name or "" - db_search_space.name = f"{prefix}{base_name[:available]}" + base_name = db_workspace.name or "" + db_workspace.name = f"{prefix}{base_name[:available]}" await session.commit() # Dispatch durable background deletion via Celery. # If queue dispatch fails, revert name to avoid stuck "[DELETING]" state. try: - from app.tasks.celery_tasks.document_tasks import delete_search_space_task + from app.tasks.celery_tasks.document_tasks import delete_workspace_task - delete_search_space_task.delay(search_space_id) + delete_workspace_task.delay(workspace_id) except Exception as dispatch_error: - db_search_space.name = base_name + db_workspace.name = base_name await session.commit() raise HTTPException( status_code=503, detail="Failed to queue background deletion. Please try again.", ) from dispatch_error - return {"message": "Search space deleted successfully"} + return {"message": "Workspace deleted successfully"} except HTTPException: raise except Exception as e: await session.rollback() raise HTTPException( - status_code=500, detail=f"Failed to delete search space: {e!s}" + status_code=500, detail=f"Failed to delete workspace: {e!s}" ) from e -@router.get("/searchspaces/{search_space_id}/snapshots") -async def list_search_space_snapshots( - search_space_id: int, +@router.get("/workspaces/{workspace_id}/snapshots") +async def list_workspace_snapshots( + workspace_id: int, session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): """ - List all public chat snapshots for a search space. + List all public chat snapshots for a workspace. Requires PUBLIC_SHARING_VIEW permission. """ from app.schemas.new_chat import PublicChatSnapshotsBySpaceResponse - from app.services.public_chat_service import list_snapshots_for_search_space + from app.services.public_chat_service import list_snapshots_for_workspace - snapshots = await list_snapshots_for_search_space( + snapshots = await list_snapshots_for_workspace( session=session, - search_space_id=search_space_id, + workspace_id=workspace_id, auth=auth, ) return PublicChatSnapshotsBySpaceResponse(snapshots=snapshots)