diff --git a/backend/app/api/endpoints/admin_categories.py b/backend/app/api/endpoints/admin_categories.py index 39218bcb..36aa4abc 100644 --- a/backend/app/api/endpoints/admin_categories.py +++ b/backend/app/api/endpoints/admin_categories.py @@ -11,6 +11,7 @@ from app.models.category import TreeCategory from app.models.tree import Tree from app.schemas.admin import GlobalCategoryCreate, GlobalCategoryUpdate, GlobalCategoryResponse from app.api.deps import require_admin +from app.core.service_account import PLATFORM_ACCOUNT_ID router = APIRouter(prefix="/admin/categories", tags=["admin-categories"]) @@ -22,7 +23,7 @@ async def list_global_categories( ): """List all global categories (account_id IS NULL).""" result = await db.execute( - select(TreeCategory).where(TreeCategory.account_id.is_(None)).order_by(TreeCategory.name) + select(TreeCategory).where(TreeCategory.account_id == PLATFORM_ACCOUNT_ID).order_by(TreeCategory.name) ) categories = result.scalars().all() @@ -51,18 +52,18 @@ async def create_global_category( """Create a global category.""" # Check slug uniqueness for global categories existing = await db.execute( - select(TreeCategory).where(TreeCategory.slug == data.slug, TreeCategory.account_id.is_(None)) + select(TreeCategory).where(TreeCategory.slug == data.slug, TreeCategory.account_id == PLATFORM_ACCOUNT_ID) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Global category with this slug already exists") - category = TreeCategory(name=data.name, slug=data.slug, account_id=None) + category = TreeCategory(name=data.name, slug=data.slug, account_id=PLATFORM_ACCOUNT_ID) db.add(category) await log_audit(db, current_user.id, "global_category.create", "category", details={"name": data.name}) await db.commit() await db.refresh(category) - return GlobalCategoryResponse(id=category.id, name=category.name, slug=category.slug, account_id=None, tree_count=0) + return GlobalCategoryResponse(id=category.id, name=category.name, slug=category.slug, account_id=PLATFORM_ACCOUNT_ID, tree_count=0) @router.put("/global/{category_id}", response_model=GlobalCategoryResponse) @@ -74,7 +75,7 @@ async def update_global_category( ): """Update a global category.""" result = await db.execute( - select(TreeCategory).where(TreeCategory.id == category_id, TreeCategory.account_id.is_(None)) + select(TreeCategory).where(TreeCategory.id == category_id, TreeCategory.account_id == PLATFORM_ACCOUNT_ID) ) category = result.scalar_one_or_none() if not category: @@ -86,7 +87,7 @@ async def update_global_category( # Check slug uniqueness existing = await db.execute( select(TreeCategory).where( - TreeCategory.slug == data.slug, TreeCategory.account_id.is_(None), TreeCategory.id != category_id + TreeCategory.slug == data.slug, TreeCategory.account_id == PLATFORM_ACCOUNT_ID, TreeCategory.id != category_id ) ) if existing.scalar_one_or_none(): @@ -103,7 +104,7 @@ async def update_global_category( return GlobalCategoryResponse( id=category.id, name=category.name, slug=category.slug, - account_id=None, tree_count=tree_count, + account_id=PLATFORM_ACCOUNT_ID, tree_count=tree_count, ) @@ -115,7 +116,7 @@ async def delete_global_category( ): """Delete (archive) a global category.""" result = await db.execute( - select(TreeCategory).where(TreeCategory.id == category_id, TreeCategory.account_id.is_(None)) + select(TreeCategory).where(TreeCategory.id == category_id, TreeCategory.account_id == PLATFORM_ACCOUNT_ID) ) category = result.scalar_one_or_none() if not category: diff --git a/backend/app/api/endpoints/categories.py b/backend/app/api/endpoints/categories.py index 73505c05..e4c8b37e 100644 --- a/backend/app/api/endpoints/categories.py +++ b/backend/app/api/endpoints/categories.py @@ -12,6 +12,7 @@ from app.models.user import User from app.schemas.category import CategoryCreate, CategoryUpdate, CategoryResponse, CategoryListResponse from app.api.deps import get_current_active_user from app.core.permissions import can_manage_category, can_create_category +from app.core.service_account import PLATFORM_ACCOUNT_ID router = APIRouter(prefix="/categories", tags=["categories"]) @@ -47,13 +48,13 @@ async def list_categories( elif current_user.account_id: query = query.where( or_( - TreeCategory.account_id.is_(None), # Global + TreeCategory.account_id == PLATFORM_ACCOUNT_ID, # Global TreeCategory.account_id == current_user.account_id # User's account ) ) else: # User has no account, only show global categories - query = query.where(TreeCategory.account_id.is_(None)) + query = query.where(TreeCategory.account_id == PLATFORM_ACCOUNT_ID) query = query.order_by(TreeCategory.display_order, TreeCategory.name) @@ -173,7 +174,7 @@ async def create_category( name=category_data.name, slug=slug, description=category_data.description, - account_id=category_data.account_id, + account_id=category_data.account_id if category_data.account_id is not None else PLATFORM_ACCOUNT_ID, display_order=max_order + 1, created_by=current_user.id ) diff --git a/backend/app/api/endpoints/step_categories.py b/backend/app/api/endpoints/step_categories.py index 5d890225..d0bb3ae9 100644 --- a/backend/app/api/endpoints/step_categories.py +++ b/backend/app/api/endpoints/step_categories.py @@ -16,6 +16,7 @@ from app.schemas.step_category import ( ) from app.api.deps import get_current_active_user from app.core.permissions import can_manage_step_category, can_create_step_category +from app.core.service_account import PLATFORM_ACCOUNT_ID router = APIRouter(prefix="/step-categories", tags=["step-categories"]) @@ -44,13 +45,13 @@ async def list_step_categories( elif current_user.account_id: query = query.where( or_( - StepCategory.account_id.is_(None), # Global + StepCategory.account_id == PLATFORM_ACCOUNT_ID, # Global StepCategory.account_id == current_user.account_id # User's account ) ) else: # User has no account, only show global categories - query = query.where(StepCategory.account_id.is_(None)) + query = query.where(StepCategory.account_id == PLATFORM_ACCOUNT_ID) query = query.order_by(StepCategory.display_order, StepCategory.name) @@ -155,7 +156,7 @@ async def create_step_category( name=category_data.name, slug=slug, description=category_data.description, - account_id=category_data.account_id, + account_id=category_data.account_id if category_data.account_id is not None else PLATFORM_ACCOUNT_ID, display_order=max_order + 1, created_by=current_user.id ) diff --git a/backend/app/api/endpoints/tags.py b/backend/app/api/endpoints/tags.py index 334e33f8..30f01504 100644 --- a/backend/app/api/endpoints/tags.py +++ b/backend/app/api/endpoints/tags.py @@ -12,6 +12,7 @@ from app.models.user import User from app.schemas.tag import TagCreate, TagResponse, TagListResponse, TagAssignment from app.api.deps import get_current_active_user from app.core.permissions import can_manage_tree_tags, can_create_tag +from app.core.service_account import PLATFORM_ACCOUNT_ID router = APIRouter(prefix="/tags", tags=["tags"]) @@ -33,13 +34,13 @@ async def list_tags( if include_account and current_user.account_id: query = query.where( or_( - TreeTag.account_id.is_(None), # Global + TreeTag.account_id == PLATFORM_ACCOUNT_ID, # Global TreeTag.account_id == current_user.account_id # User's account ) ) else: # Only show global tags - query = query.where(TreeTag.account_id.is_(None)) + query = query.where(TreeTag.account_id == PLATFORM_ACCOUNT_ID) query = query.order_by(TreeTag.usage_count.desc(), TreeTag.name) @@ -71,12 +72,12 @@ async def search_tags( if include_account and current_user.account_id: query = query.where( or_( - TreeTag.account_id.is_(None), + TreeTag.account_id == PLATFORM_ACCOUNT_ID, TreeTag.account_id == current_user.account_id ) ) else: - query = query.where(TreeTag.account_id.is_(None)) + query = query.where(TreeTag.account_id == PLATFORM_ACCOUNT_ID) query = query.order_by(TreeTag.usage_count.desc(), TreeTag.name).limit(limit) @@ -147,7 +148,7 @@ async def create_tag( new_tag = TreeTag( name=tag_data.name, slug=slug, - account_id=tag_data.account_id, + account_id=tag_data.account_id if tag_data.account_id is not None else PLATFORM_ACCOUNT_ID, created_by=current_user.id ) db.add(new_tag) @@ -206,7 +207,7 @@ async def add_tags_to_tree( tag_query = select(TreeTag).where( TreeTag.slug == slug, or_( - TreeTag.account_id.is_(None), # Global tag + TreeTag.account_id == PLATFORM_ACCOUNT_ID, # Global tag TreeTag.account_id == tag_account_id # Account tag ) ) @@ -340,7 +341,7 @@ async def replace_tree_tags( tag_query = select(TreeTag).where( TreeTag.slug == slug, or_( - TreeTag.account_id.is_(None), + TreeTag.account_id == PLATFORM_ACCOUNT_ID, TreeTag.account_id == tag_account_id ) ) diff --git a/backend/app/api/endpoints/trees.py b/backend/app/api/endpoints/trees.py index 73927cf8..c24edca2 100644 --- a/backend/app/api/endpoints/trees.py +++ b/backend/app/api/endpoints/trees.py @@ -29,6 +29,7 @@ from app.core.subscriptions import check_tree_limit, get_account_subscription, g from app.core.audit import log_audit from app.core.config import settings from app.core.tree_validation import can_publish_tree +from app.core.service_account import PLATFORM_ACCOUNT_ID from app.core.step_sync import sync_steps_from_tree, deactivate_synced_steps_for_tree from app.services.rag_service import index_tree as rag_index_tree @@ -470,7 +471,7 @@ async def create_tree( tree_structure=tree_data.tree_structure, intake_form=intake_form_data, author_id=service_account_id if is_default else current_user.id, - account_id=None if is_default else current_user.account_id, + account_id=PLATFORM_ACCOUNT_ID if is_default else current_user.account_id, is_public=True if is_default else tree_data.is_public, # Default trees are always public is_default=is_default, status=tree_data.status diff --git a/backend/app/core/service_account.py b/backend/app/core/service_account.py index a2175981..9d00a1d9 100644 --- a/backend/app/core/service_account.py +++ b/backend/app/core/service_account.py @@ -18,6 +18,10 @@ logger = logging.getLogger(__name__) SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com" SERVICE_ACCOUNT_NAME = "ResolutionFlow" + +# Well-known UUID for the platform account — owns all default/global content. +# Created by migration 3a40fe11b427_create_global_content_tables. +PLATFORM_ACCOUNT_ID = uuid.UUID("00000000-0000-0000-0000-000000000001") SYSTEM_ACCOUNT_NAME = "ResolutionFlow System" SYSTEM_ACCOUNT_DISPLAY_CODE = "RF-SYS-1"