fix: add ResolutionFlow service account to own default tree steps in library
Default/system trees had no author_id (NULL), causing a NOT NULL violation when syncing steps to step_library.created_by on publish. - Add is_service_account flag to users table (migration 4f4137ce) - Add service_account.py: idempotent ensure_service_account() creates noreply@resolutionflow.com with unusable password on startup - Cache service account ID on app.state at lifespan startup - Add get_service_account_id() FastAPI dep (returns None in tests) - sync_steps_from_tree: resolve author_id or service_account_id as created_by - create_tree: set author_id=service_account_id for is_default trees - Migration 1490781700bc: backfill author_id on 31 existing default trees Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,14 @@ async def require_account_owner(
|
||||
)
|
||||
|
||||
|
||||
def get_service_account_id(request: Request) -> Optional[UUID]:
|
||||
"""Return the cached ResolutionFlow service account UUID from app.state.
|
||||
|
||||
Returns None in test environments where lifespan startup did not run.
|
||||
"""
|
||||
return getattr(request.app.state, "service_account_id", None)
|
||||
|
||||
|
||||
async def get_plan_limits_for_user(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
|
||||
@@ -21,7 +21,7 @@ from app.schemas.tree import (
|
||||
PinnedFlowResponse, PinnedFlowsListResponse, PinnedFlowReorderRequest
|
||||
)
|
||||
from app.models.user_pinned_tree import UserPinnedTree
|
||||
from app.api.deps import get_current_active_user, require_engineer_or_admin, require_admin
|
||||
from app.api.deps import get_current_active_user, require_engineer_or_admin, require_admin, get_service_account_id
|
||||
from app.core.permissions import can_edit_tree, can_access_tree
|
||||
from app.core.filters import build_tree_access_filter
|
||||
from app.core.subscriptions import check_tree_limit
|
||||
@@ -400,7 +400,8 @@ async def get_tree(
|
||||
async def create_tree(
|
||||
tree_data: TreeCreate,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(require_engineer_or_admin)]
|
||||
current_user: Annotated[User, Depends(require_engineer_or_admin)],
|
||||
service_account_id: Annotated[Optional[UUID], Depends(get_service_account_id)],
|
||||
):
|
||||
"""Create a new tree (engineers and admins only).
|
||||
|
||||
@@ -465,7 +466,7 @@ async def create_tree(
|
||||
tree_type=tree_data.tree_type,
|
||||
tree_structure=tree_data.tree_structure,
|
||||
intake_form=intake_form_data,
|
||||
author_id=None if is_default else current_user.id, # Default trees have no author
|
||||
author_id=service_account_id if is_default else current_user.id,
|
||||
account_id=None 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,
|
||||
@@ -549,7 +550,8 @@ async def update_tree(
|
||||
tree_id: UUID,
|
||||
tree_data: TreeUpdate,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(require_engineer_or_admin)]
|
||||
current_user: Annotated[User, Depends(require_engineer_or_admin)],
|
||||
service_account_id: Annotated[Optional[UUID], Depends(get_service_account_id)],
|
||||
):
|
||||
"""Update an existing tree (engineers and admins only).
|
||||
|
||||
@@ -654,6 +656,7 @@ async def update_tree(
|
||||
author_id=tree.author_id,
|
||||
account_id=tree.account_id,
|
||||
is_public=_is_public,
|
||||
service_account_id=service_account_id,
|
||||
)
|
||||
|
||||
# Handle tags replacement
|
||||
|
||||
Reference in New Issue
Block a user