import os import shutil import uuid from fastapi import APIRouter, Depends, HTTPException, Response, status from pydantic import BaseModel, ConfigDict from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src.auth.dependencies import _get_owned_project, _get_user, get_current_user_id, get_db_session from src.models.git_repository import GitRepository from src.models.project import Project from src.models.ssh_key import SSHKey router = APIRouter(prefix="/projects", tags=["projects"]) class ProjectCreate(BaseModel): name: str description: str | None = None class ProjectUpdate(BaseModel): name: str | None = None description: str | None = None class ProjectResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: uuid.UUID name: str description: str | None owner_id: uuid.UUID default_ssh_key_id: uuid.UUID | None class SetDefaultSSHKeyRequest(BaseModel): ssh_key_id: uuid.UUID @router.post( "", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED, summary="Create a new project", description="Create a new project for the authenticated user.", ) async def create_project( data: ProjectCreate, user_id: uuid.UUID = Depends(get_current_user_id), session: AsyncSession = Depends(get_db_session), ) -> Project: """Create a new project. Args: data: Project creation data including name and optional description. user_id: ID of the authenticated user. session: Database session. Returns: The newly created project. """ user = await _get_user(session, user_id) project = Project( name=data.name, description=data.description, owner_id=user.id, default_ssh_key_id=None, ) session.add(project) await session.commit() await session.refresh(project) return project @router.get( "", response_model=list[ProjectResponse], summary="List all projects", description="Retrieve all projects owned by the authenticated user.", ) async def list_projects( user_id: uuid.UUID = Depends(get_current_user_id), session: AsyncSession = Depends(get_db_session), ) -> list[Project]: """List all projects for the authenticated user. Args: user_id: ID of the authenticated user. session: Database session. Returns: List of projects owned by the user. """ user = await _get_user(session, user_id) result = await session.execute(select(Project).where(Project.owner_id == user.id)) return list(result.scalars().all()) @router.get( "/{project_id}", response_model=ProjectResponse, summary="Get a project", description="Retrieve a specific project by ID.", ) async def get_project( project_id: uuid.UUID, user_id: uuid.UUID = Depends(get_current_user_id), session: AsyncSession = Depends(get_db_session), ) -> Project: """Get a specific project by ID. Args: project_id: UUID of the project to retrieve. user_id: ID of the authenticated user. session: Database session. Returns: The requested project. """ await _get_user(session, user_id) return await _get_owned_project(project_id, user_id, session) @router.patch( "/{project_id}", response_model=ProjectResponse, summary="Update a project", description="Update a project's name or description.", ) async def update_project( project_id: uuid.UUID, data: ProjectUpdate, user_id: uuid.UUID = Depends(get_current_user_id), session: AsyncSession = Depends(get_db_session), ) -> Project: """Update a project. Args: project_id: UUID of the project to update. data: Project update data with optional name and description. user_id: ID of the authenticated user. session: Database session. Returns: The updated project. """ await _get_user(session, user_id) project = await _get_owned_project(project_id, user_id, session) if data.name is not None: project.name = data.name if data.description is not None: project.description = data.description await session.commit() await session.refresh(project) return project @router.delete( "/{project_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete a project", description="Delete a project and all its associated repositories.", ) async def delete_project( project_id: uuid.UUID, user_id: uuid.UUID = Depends(get_current_user_id), session: AsyncSession = Depends(get_db_session), ) -> Response: """Delete a project and all its repositories. Args: project_id: UUID of the project to delete. user_id: ID of the authenticated user. session: Database session. Returns: Empty response with 204 status code. """ await _get_user(session, user_id) project = await _get_owned_project(project_id, user_id, session) # Delete repositories from disk and database result = await session.execute(select(GitRepository).where(GitRepository.project_id == project_id)) repositories = result.scalars().all() for repo in repositories: if os.path.exists(repo.path): shutil.rmtree(repo.path) await session.delete(repo) await session.delete(project) await session.commit() return Response(status_code=status.HTTP_204_NO_CONTENT) @router.patch( "/{project_id}/default-ssh-key", response_model=ProjectResponse, summary="Set default SSH key", description="Set the default SSH key for a project.", ) async def set_default_ssh_key( project_id: uuid.UUID, data: SetDefaultSSHKeyRequest, user_id: uuid.UUID = Depends(get_current_user_id), session: AsyncSession = Depends(get_db_session), ) -> Project: """Set the default SSH key for a project. Args: project_id: UUID of the project. data: Request containing the SSH key ID to set as default. user_id: ID of the authenticated user. session: Database session. Returns: The updated project. """ user = await _get_user(session, user_id) project = await _get_owned_project(project_id, user_id, session) ssh_key = await session.get(SSHKey, data.ssh_key_id) if ssh_key is None or ssh_key.user_id != user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="invalid ssh key", ) project.default_ssh_key_id = data.ssh_key_id await session.commit() await session.refresh(project) return project