     1→"""
     2→Projects Router
     3→===============
     4→
     5→API endpoints for project management.
     6→"""
     7→
     8→from datetime import datetime
     9→from typing import Optional, List
    10→
    11→from fastapi import APIRouter, Depends, HTTPException, status
    12→from pydantic import BaseModel, Field
    13→from sqlalchemy import select
    14→from sqlalchemy.ext.asyncio import AsyncSession
    15→
    16→from ..database import get_db
    17→from ..models import Project
    18→from ..path_security import validate_project_path
    19→
    20→router = APIRouter(prefix="/api/projects", tags=["projects"])
    21→
    22→
    23→# ============================================================================
    24→# Pydantic Schemas
    25→# ============================================================================
    26→
    27→class ProjectCreate(BaseModel):
    28→    """Schema for creating a new project."""
    29→    name: str = Field(..., min_length=1, max_length=255, description="Unique project name")
    30→    path: str = Field(..., min_length=1, max_length=1024, description="Project directory path")
    31→
    32→
    33→class ProjectUpdate(BaseModel):
    34→    """Schema for updating a project."""
    35→    path: Optional[str] = Field(None, min_length=1, max_length=1024)
    36→
    37→
    38→class ProjectResponse(BaseModel):
    39→    """Schema for project response."""
    40→    id: int
    41→    name: str
    42→    path: str
    43→    created_at: datetime
    44→    updated_at: datetime
    45→    spec_version: int
    46→
    47→    class Config:
    48→        from_attributes = True
    49→
    50→
    51→class ProjectListResponse(BaseModel):
    52→    """Schema for project list response."""
    53→    projects: List[ProjectResponse]
    54→    count: int
    55→
    56→
    57→# ============================================================================
    58→# API Endpoints
    59→# ============================================================================
    60→
    61→@router.get("", response_model=ProjectListResponse)
    62→async def list_projects(db: AsyncSession = Depends(get_db)):
    63→    """List all projects."""
    64→    result = await db.execute(select(Project).order_by(Project.name))
    65→    projects = result.scalars().all()
    66→    return ProjectListResponse(
    67→        projects=[ProjectResponse.model_validate(p) for p in projects],
    68→        count=len(projects)
    69→    )
    70→
    71→
    72→@router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
    73→async def create_project(project: ProjectCreate, db: AsyncSession = Depends(get_db)):
    74→    """Create a new project."""
    75→    # Validate path for security
    76→    validate_project_path(project.path)
    77→    # Check if project with this name already exists
    78→    result = await db.execute(select(Project).where(Project.name == project.name))
    79→    if result.scalar_one_or_none():
    80→        raise HTTPException(
    81→            status_code=status.HTTP_409_CONFLICT,
    82→            detail=f"Project with name '{project.name}' already exists"
    83→        )
    84→
    85→    # Create new project
    86→    db_project = Project(
    87→        name=project.name,
    88→        path=project.path,
    89→    )
    90→    db.add(db_project)
    91→    await db.commit()
    92→    await db.refresh(db_project)
    93→
    94→    return ProjectResponse.model_validate(db_project)
    95→
    96→
    97→@router.get("/{name}", response_model=ProjectResponse)
    98→async def get_project(name: str, db: AsyncSession = Depends(get_db)):
    99→    """Get a project by name."""
   100→    result = await db.execute(select(Project).where(Project.name == name))
   101→    project = result.scalar_one_or_none()
   102→
   103→    if not project:
   104→        raise HTTPException(
   105→            status_code=status.HTTP_404_NOT_FOUND,
   106→            detail=f"Project '{name}' not found"
   107→        )
   108→
   109→    return ProjectResponse.model_validate(project)
   110→
   111→
   112→@router.put("/{name}", response_model=ProjectResponse)
   113→async def update_project(name: str, project_update: ProjectUpdate, db: AsyncSession = Depends(get_db)):
   114→    """Update a project by name."""
   115→    result = await db.execute(select(Project).where(Project.name == name))
   116→    project = result.scalar_one_or_none()
   117→
   118→    if not project:
   119→        raise HTTPException(
   120→            status_code=status.HTTP_404_NOT_FOUND,
   121→            detail=f"Project '{name}' not found"
   122→        )
   123→
   124→    if project_update.path is not None:
   125→        # Validate path for security
   126→        validate_project_path(project_update.path)
   127→        project.path = project_update.path
   128→
   129→    project.updated_at = datetime.utcnow()
   130→    await db.commit()
   131→    await db.refresh(project)
   132→
   133→    return ProjectResponse.model_validate(project)
   134→
   135→
   136→@router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT)
   137→async def delete_project(name: str, db: AsyncSession = Depends(get_db)):
   138→    """Delete a project by name."""
   139→    result = await db.execute(select(Project).where(Project.name == name))
   140→    project = result.scalar_one_or_none()
   141→
   142→    if not project:
   143→        raise HTTPException(
   144→            status_code=status.HTTP_404_NOT_FOUND,
   145→            detail=f"Project '{name}' not found"
   146→        )
   147→
   148→    await db.delete(project)
   149→    await db.commit()
   150→
   151→
   152→class DuplicateProjectRequest(BaseModel):
   153→    new_name: str
   154→
   155→
   156→@router.post("/{name}/duplicate", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
   157→async def duplicate_project(name: str, request: DuplicateProjectRequest, db: AsyncSession = Depends(get_db)):
   158→    """Duplicate a project with a new name."""
   159→    new_name = request.new_name
   160→    # Get source project
   161→    result = await db.execute(select(Project).where(Project.name == name))
   162→    source_project = result.scalar_one_or_none()
   163→
   164→    if not source_project:
   165→        raise HTTPException(
   166→            status_code=status.HTTP_404_NOT_FOUND,
   167→            detail=f"Project '{name}' not found"
   168→        )
   169→
   170→    # Check if new name already exists
   171→    result = await db.execute(select(Project).where(Project.name == new_name))
   172→    if result.scalar_one_or_none():
   173→        raise HTTPException(
   174→            status_code=status.HTTP_409_CONFLICT,
   175→            detail=f"Project with name '{new_name}' already exists"
   176→        )
   177→
   178→    # Create duplicate
   179→    new_project = Project(
   180→        name=new_name,
   181→        path=source_project.path,
   182→    )
   183→    db.add(new_project)
   184→    await db.commit()
   185→    await db.refresh(new_project)
   186→
   187→    return ProjectResponse.model_validate(new_project)
   188→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

"""
Projects Router
===============

API endpoints for project management.
"""

from datetime import datetime
from typing import Optional, List

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from ..database import get_db
from ..models import Project
from ..path_security import validate_project_path

router = APIRouter(prefix="/api/projects", tags=["projects"])

# ============================================================================
# Pydantic Schemas
# ============================================================================

class ProjectCreate(BaseModel):
    """Schema for creating a new project."""
    name: str = Field(..., min_length=1, max_length=255, description="Unique project name")
    path: str = Field(..., min_length=1, max_length=1024, description="Project directory path")

class ProjectUpdate(BaseModel):
    """Schema for updating a project."""
    path: Optional[str] = Field(None, min_length=1, max_length=1024)

class ProjectResponse(BaseModel):
    """Schema for project response."""
    id: int
    name: str
    path: str
    created_at: datetime
    updated_at: datetime
    spec_version: int

    class Config:
        from_attributes = True

class ProjectListResponse(BaseModel):
    """Schema for project list response."""
    projects: List[ProjectResponse]
    count: int

# ============================================================================
# API Endpoints
# ============================================================================

@router.get("", response_model=ProjectListResponse)
async def list_projects(db: AsyncSession = Depends(get_db)):
    """List all projects."""
    result = await db.execute(select(Project).order_by(Project.name))
    projects = result.scalars().all()
    return ProjectListResponse(
        projects=[ProjectResponse.model_validate(p) for p in projects],
        count=len(projects)
    )

@router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
async def create_project(project: ProjectCreate, db: AsyncSession = Depends(get_db)):
    """Create a new project."""
    # Validate path for security
    validate_project_path(project.path)
    # Check if project with this name already exists
    result = await db.execute(select(Project).where(Project.name == project.name))
    if result.scalar_one_or_none():
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Project with name '{project.name}' already exists"
        )

    # Create new project
    db_project = Project(
        name=project.name,
        path=project.path,
    )
    db.add(db_project)
    await db.commit()
    await db.refresh(db_project)

    return ProjectResponse.model_validate(db_project)

@router.get("/{name}", response_model=ProjectResponse)
async def get_project(name: str, db: AsyncSession = Depends(get_db)):
    """Get a project by name."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    return ProjectResponse.model_validate(project)

@router.put("/{name}", response_model=ProjectResponse)
async def update_project(name: str, project_update: ProjectUpdate, db: AsyncSession = Depends(get_db)):
    """Update a project by name."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    if project_update.path is not None:
        # Validate path for security
        validate_project_path(project_update.path)
        project.path = project_update.path

    project.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(project)

    return ProjectResponse.model_validate(project)

@router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_project(name: str, db: AsyncSession = Depends(get_db)):
    """Delete a project by name."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    await db.delete(project)
    await db.commit()

class DuplicateProjectRequest(BaseModel):
    new_name: str

@router.post("/{name}/duplicate", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
async def duplicate_project(name: str, request: DuplicateProjectRequest, db: AsyncSession = Depends(get_db)):
    """Duplicate a project with a new name."""
    new_name = request.new_name
    # Get source project
    result = await db.execute(select(Project).where(Project.name == name))
    source_project = result.scalar_one_or_none()

    if not source_project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Check if new name already exists
    result = await db.execute(select(Project).where(Project.name == new_name))
    if result.scalar_one_or_none():
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Project with name '{new_name}' already exists"
        )

    # Create duplicate
    new_project = Project(
        name=new_name,
        path=source_project.path,
    )
    db.add(new_project)
    await db.commit()
    await db.refresh(new_project)

    return ProjectResponse.model_validate(new_project)


All three edits have been successfully applied to the file at `C:/Users/MikeSwanson/claude-projects/Autocode-remix/Autocode-fork/backend/app/routers/projects.py`. Here is a summary of the changes made:

1. **Import added** (line 18): Added the import for `validate_project_path` from `..path_security`:
   ```python
   from ..path_security import validate_project_path
   ```

2. **create_project function** (lines 75-76): Added path validation at the start of the function body:
   ```python
   # Validate path for security
   validate_project_path(project.path)
   ```

3. **update_project function** (lines 125-126): Added path validation before updating the path:
   ```python
   # Validate path for security
   validate_project_path(project_update.path)
   ```