Add public/private visibility for trees

- Add is_public field to Tree model (private by default)
- Update access control: users see default trees, public trees, or their own
- Update all tree endpoints (list, search, get, categories) with new visibility logic
- Default/system trees are automatically marked as public
- Add migration 004 to add is_public column and update existing defaults
- Fix pydantic settings to ignore extra env vars (DATABASE_URL_SYNC)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-01 16:53:19 -05:00
parent db0b05eba7
commit 2d99c52025
6 changed files with 63 additions and 8 deletions

View File

@@ -31,11 +31,15 @@ async def list_trees(
if is_active is not None:
query = query.where(Tree.is_active == is_active)
# Only show active trees or trees owned by user (for now)
# Later, add team-based filtering
# Only show trees user has access to:
# - Default/system trees (visible to all)
# - Public trees
# - User's own trees (public or private)
query = query.where(
Tree.is_active == True,
or_(
Tree.is_active == True,
Tree.is_default == True,
Tree.is_public == True,
Tree.author_id == current_user.id
)
)
@@ -53,10 +57,15 @@ async def list_categories(
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
):
"""List all unique categories."""
"""List all unique categories from trees the user can access."""
query = select(Tree.category).where(
Tree.category.isnot(None),
Tree.is_active == True
Tree.is_active == True,
or_(
Tree.is_default == True,
Tree.is_public == True,
Tree.author_id == current_user.id
)
).distinct()
result = await db.execute(query)
categories = [row[0] for row in result.all() if row[0]]
@@ -77,6 +86,11 @@ async def search_trees(
query = select(Tree).where(
Tree.is_active == True,
or_(
Tree.is_default == True,
Tree.is_public == True,
Tree.author_id == current_user.id
),
search_vector.op('@@')(search_query)
).order_by(
func.ts_rank(search_vector, search_query).desc()
@@ -103,8 +117,9 @@ async def get_tree(
detail="Tree not found"
)
# Check access: tree must be active OR user is the author
if not tree.is_active and tree.author_id != current_user.id:
# Check access: tree must be active AND (default OR public OR author)
can_access = tree.is_default or tree.is_public or tree.author_id == current_user.id
if not tree.is_active or not can_access:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have access to this tree"
@@ -130,6 +145,7 @@ async def create_tree(
tree_structure=tree_data.tree_structure,
author_id=None if is_default else current_user.id, # Default trees have no author
team_id=None if is_default else current_user.team_id,
is_public=True if is_default else tree_data.is_public, # Default trees are always public
is_default=is_default
)
db.add(new_tree)