Files
headquarter/apps/api/app/routers/repository_connections.py
T
alex 3728c245d3 feat(FN-007): implement repository connection API and git operations
- 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
2026-05-16 13:41:37 +02:00

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