"""Repository connection router.""" from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.auth.dependencies import get_current_active_user from app.db import get_db_session from app.git.credential_storage import DatabaseCredentialStorage from app.git.credentials import AccessTokenCredential, GitCredential from app.git.providers import get_provider from app.git.ssh_key import SshKeyLifecycle from app.git.types import ConnectionStatus, ProviderKind from app.models.project import Project from app.models.repository import Repository from app.models.repository_connection import RepositoryConnection from app.models.user import User from app.schemas.repository_connection import ( RepositoryConnectionCreate, RepositoryConnectionRead, SshKeyResponse, ) router = APIRouter(tags=["repository-connections"]) async def _get_project_for_user( project_id: UUID, user: User, session: AsyncSession ) -> Project: project = await session.get(Project, project_id) if not project or project.owner_id != user.id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" ) return project @router.post( "/projects/{project_id}/repository-connections", response_model=RepositoryConnectionRead, status_code=status.HTTP_201_CREATED, ) async def create_repository_connection( project_id: UUID, conn_in: RepositoryConnectionCreate, current_user: User = Depends(get_current_active_user), session: AsyncSession = Depends(get_db_session), ) -> RepositoryConnection: await _get_project_for_user(project_id, current_user, session) repo = await session.get(Repository, conn_in.repository_id) if not repo or repo.project_id != project_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found" ) result = await session.execute( select(RepositoryConnection).where( RepositoryConnection.project_id == project_id, RepositoryConnection.repository_id == conn_in.repository_id, RepositoryConnection.provider_kind == conn_in.provider_kind, ) ) existing = result.scalar_one_or_none() if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Connection already exists for this repository and provider", ) storage = DatabaseCredentialStorage(session) credential: GitCredential if conn_in.credential_kind == "access_token": credential = AccessTokenCredential( encrypted_payload=conn_in.credential_payload ) else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Unsupported credential kind: {conn_in.credential_kind}", ) credential_id = await storage.create(credential) connection = RepositoryConnection( project_id=project_id, repository_id=conn_in.repository_id, provider_kind=conn_in.provider_kind, credential_id=credential_id, connection_status=str(ConnectionStatus.pending), ) session.add(connection) await session.commit() await session.refresh(connection) try: provider = get_provider(ProviderKind(conn_in.provider_kind)) provider_status = provider.validate_connection(repo.git_url, str(credential_id)) connection.connection_status = str(provider_status) except Exception: connection.connection_status = str(ConnectionStatus.error) await session.commit() await session.refresh(connection) return connection @router.get( "/projects/{project_id}/repository-connections", response_model=list[RepositoryConnectionRead], ) async def list_repository_connections( project_id: UUID, current_user: User = Depends(get_current_active_user), session: AsyncSession = Depends(get_db_session), ) -> list[RepositoryConnection]: await _get_project_for_user(project_id, current_user, session) result = await session.execute( select(RepositoryConnection).where( RepositoryConnection.project_id == project_id ) ) return list(result.scalars().all()) @router.get( "/projects/{project_id}/repository-connections/{connection_id}", response_model=RepositoryConnectionRead, ) async def get_repository_connection( project_id: UUID, connection_id: UUID, current_user: User = Depends(get_current_active_user), session: AsyncSession = Depends(get_db_session), ) -> RepositoryConnection: await _get_project_for_user(project_id, current_user, session) connection = await session.get(RepositoryConnection, connection_id) if not connection or connection.project_id != project_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found" ) return connection @router.delete( "/projects/{project_id}/repository-connections/{connection_id}", status_code=status.HTTP_204_NO_CONTENT, ) async def delete_repository_connection( project_id: UUID, connection_id: UUID, current_user: User = Depends(get_current_active_user), session: AsyncSession = Depends(get_db_session), ) -> None: await _get_project_for_user(project_id, current_user, session) connection = await session.get(RepositoryConnection, connection_id) if not connection or connection.project_id != project_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found" ) if connection.credential_id: storage = DatabaseCredentialStorage(session) await storage.delete(connection.credential_id) await session.delete(connection) await session.commit() @router.post( "/projects/{project_id}/repository-connections/{connection_id}/ssh-key", response_model=SshKeyResponse, status_code=status.HTTP_201_CREATED, ) async def generate_ssh_key( project_id: UUID, connection_id: UUID, current_user: User = Depends(get_current_active_user), session: AsyncSession = Depends(get_db_session), ) -> dict[str, str]: await _get_project_for_user(project_id, current_user, session) connection = await session.get(RepositoryConnection, connection_id) if not connection or connection.project_id != project_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found" ) key_pair = SshKeyLifecycle.generate(connection_id) storage = DatabaseCredentialStorage(session) ssh_credential = GitCredential( kind="ssh_key", encrypted_payload=key_pair.encrypted_private_key, ) credential_id = await storage.create(ssh_credential) connection.credential_id = credential_id await session.commit() return { "connection_id": str(connection_id), "public_key": key_pair.public_key, "credential_id": str(credential_id), } @router.post( "/projects/{project_id}/repository-connections/{connection_id}/validate", response_model=RepositoryConnectionRead, ) async def validate_connection( project_id: UUID, connection_id: UUID, current_user: User = Depends(get_current_active_user), session: AsyncSession = Depends(get_db_session), ) -> RepositoryConnection: await _get_project_for_user(project_id, current_user, session) connection = await session.get(RepositoryConnection, connection_id) if not connection or connection.project_id != project_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found" ) repo = await session.get(Repository, connection.repository_id) if not repo: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found" ) try: provider = get_provider(ProviderKind(connection.provider_kind)) provider_status = provider.validate_connection( repo.git_url, str(connection.credential_id) if connection.credential_id else "" ) connection.connection_status = str(provider_status) except Exception: connection.connection_status = str(ConnectionStatus.error) await session.commit() await session.refresh(connection) return connection