3728c245d3
- Add GitHubAdapter and GitLabAdapter with URL parsing - Create provider factory in apps/api/app/git/providers/ - Implement clone, fetch, push in LocalGitOperations - Add repository_connections router with CRUD and SSH key endpoints - Create RepositoryConnection schema with validation - Update models and routers __init__.py for new components - Add comprehensive tests for git operations
244 lines
8.3 KiB
Python
244 lines
8.3 KiB
Python
"""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
|