diff --git a/backend/alembic/versions/003_add_tree_is_default.py b/backend/alembic/versions/003_add_tree_is_default.py new file mode 100644 index 00000000..b9aca0bd --- /dev/null +++ b/backend/alembic/versions/003_add_tree_is_default.py @@ -0,0 +1,28 @@ +"""Add is_default field to trees + +Revision ID: 003 +Revises: 002 +Create Date: 2026-02-01 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '003' +down_revision: Union[str, None] = '002' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('trees', sa.Column('is_default', sa.Boolean(), nullable=False, server_default='false')) + op.create_index('ix_trees_is_default', 'trees', ['is_default']) + + +def downgrade() -> None: + op.drop_index('ix_trees_is_default', table_name='trees') + op.drop_column('trees', 'is_default') diff --git a/backend/app/api/endpoints/trees.py b/backend/app/api/endpoints/trees.py index 98fad295..97947281 100644 --- a/backend/app/api/endpoints/trees.py +++ b/backend/app/api/endpoints/trees.py @@ -120,13 +120,17 @@ async def create_tree( current_user: Annotated[User, Depends(require_engineer_or_admin)] ): """Create a new tree (engineers and admins only).""" + # Only admins can create default/system trees + is_default = tree_data.is_default and current_user.role == "admin" + new_tree = Tree( name=tree_data.name, description=tree_data.description, category=tree_data.category, tree_structure=tree_data.tree_structure, - author_id=current_user.id, - team_id=current_user.team_id + 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_default=is_default ) db.add(new_tree) await db.commit() diff --git a/backend/app/models/tree.py b/backend/app/models/tree.py index 6098b0ed..48077e35 100644 --- a/backend/app/models/tree.py +++ b/backend/app/models/tree.py @@ -31,6 +31,7 @@ class Tree(Base): index=True ) is_active: Mapped[bool] = mapped_column(Boolean, default=True) + is_default: Mapped[bool] = mapped_column(Boolean, default=False, index=True) version: Mapped[int] = mapped_column(Integer, default=1) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), diff --git a/backend/app/schemas/tree.py b/backend/app/schemas/tree.py index 84fc590e..2d1e98d9 100644 --- a/backend/app/schemas/tree.py +++ b/backend/app/schemas/tree.py @@ -12,6 +12,7 @@ class TreeBase(BaseModel): class TreeCreate(TreeBase): tree_structure: dict[str, Any] = Field(..., description="The decision tree structure in JSON format") + is_default: bool = Field(False, description="Mark as a default/system tree (admin only)") class TreeUpdate(BaseModel): @@ -28,6 +29,7 @@ class TreeResponse(TreeBase): author_id: Optional[UUID] = None team_id: Optional[UUID] = None is_active: bool + is_default: bool version: int created_at: datetime updated_at: datetime @@ -43,6 +45,7 @@ class TreeListResponse(BaseModel): description: Optional[str] = None category: Optional[str] = None is_active: bool + is_default: bool version: int usage_count: int created_at: datetime diff --git a/backend/scripts/seed_trees.py b/backend/scripts/seed_trees.py index e0e81602..e3419c0e 100644 --- a/backend/scripts/seed_trees.py +++ b/backend/scripts/seed_trees.py @@ -20,13 +20,9 @@ from typing import Any # API Configuration API_BASE_URL = "http://localhost:8000/api/v1" -# Default admin user for seeding -SEED_USER = { - "email": "seed.admin@example.com", - "password": "SeedAdmin123!", - "name": "Seed Admin", - "role": "admin" -} +# Admin credentials (set via command line or environment) +ADMIN_EMAIL = None +ADMIN_PASSWORD = None # ============================================================================= @@ -3308,39 +3304,23 @@ def get_file_share_access_tree() -> dict[str, Any]: # SEEDING FUNCTIONS # ============================================================================= -async def get_or_create_admin_user(client: httpx.AsyncClient) -> tuple[str, dict]: - """Get authentication token for seeding. Creates admin user if needed.""" +async def get_admin_token(client: httpx.AsyncClient) -> str: + """Get authentication token using provided admin credentials.""" - # Try to login first + if not ADMIN_EMAIL or not ADMIN_PASSWORD: + raise Exception("Admin email and password are required. Use --email and --password arguments.") + + # Login with provided credentials login_response = await client.post( - f"{API_BASE_URL}/auth/login/json", - json={"email": SEED_USER["email"], "password": SEED_USER["password"]} - ) - - if login_response.status_code == 200: - token_data = login_response.json() - return token_data["access_token"], {"exists": True} - - # User doesn't exist, create them - register_response = await client.post( - f"{API_BASE_URL}/auth/register", - json=SEED_USER - ) - - if register_response.status_code not in (200, 201): - raise Exception(f"Failed to create seed user: {register_response.text}") - - # Now login - login_response = await client.post( - f"{API_BASE_URL}/auth/login/json", - json={"email": SEED_USER["email"], "password": SEED_USER["password"]} + f"{API_BASE_URL}/auth/login", + data={"username": ADMIN_EMAIL, "password": ADMIN_PASSWORD} ) if login_response.status_code != 200: - raise Exception(f"Failed to login as seed user: {login_response.text}") + raise Exception(f"Failed to login: {login_response.text}") token_data = login_response.json() - return token_data["access_token"], {"exists": False, "user": register_response.json()} + return token_data["access_token"] async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict) -> dict | None: @@ -3356,6 +3336,9 @@ async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict) -> print(f" [SKIP] Tree '{tree_data['name']}' already exists (ID: {tree['id']})") return None + # Mark as default/system tree + tree_data["is_default"] = True + # Create the tree response = await client.post( f"{API_BASE_URL}/trees", @@ -3390,16 +3373,13 @@ async def seed_database(): print(" Run: uvicorn app.main:app --reload") return False - # Get or create admin user - print("\n[1/3] Setting up seed user...") + # Authenticate with admin credentials + print("\n[1/3] Authenticating...") try: - token, user_info = await get_or_create_admin_user(client) - if user_info.get("exists"): - print(" Using existing seed admin user") - else: - print(f" Created seed admin user: {SEED_USER['email']}") + token = await get_admin_token(client) + print(f" Logged in as {ADMIN_EMAIL}") except Exception as e: - print(f" [ERROR] Failed to setup seed user: {e}") + print(f" [ERROR] Failed to authenticate: {e}") return False # Get all tree definitions @@ -3460,10 +3440,22 @@ def main(): default="http://localhost:8000/api/v1", help="API base URL (default: http://localhost:8000/api/v1)" ) + parser.add_argument( + "--email", + required=True, + help="Admin user email for authentication" + ) + parser.add_argument( + "--password", + required=True, + help="Admin user password for authentication" + ) args = parser.parse_args() - global API_BASE_URL + global API_BASE_URL, ADMIN_EMAIL, ADMIN_PASSWORD API_BASE_URL = args.api_url + ADMIN_EMAIL = args.email + ADMIN_PASSWORD = args.password success = asyncio.run(seed_database()) exit(0 if success else 1)