40a940304b
- Create enhanced health endpoints with /health and /health/db - Add comprehensive docstrings to all API endpoints - Add Pydantic response models with Field descriptions - Create apps/api/README.md with setup guide - Create ADR-001 for session auth decision - Create ADR-002 for async SQLAlchemy decision - Quality gates: Python syntax OK, TypeScript OK
271 lines
7.6 KiB
Python
271 lines
7.6 KiB
Python
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_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
|
|
from src.models.user import User
|
|
|
|
router = APIRouter(prefix="/projects", tags=["projects"])
|
|
|
|
|
|
async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
|
|
"""Fetch a user by ID or raise 401 if not found."""
|
|
user = await session.get(User, user_id)
|
|
if user is None:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found")
|
|
return user
|
|
|
|
|
|
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)
|
|
|
|
|
|
async def _get_owned_project(
|
|
project_id: uuid.UUID,
|
|
user_id: uuid.UUID,
|
|
session: AsyncSession,
|
|
) -> Project:
|
|
"""Fetch a project and verify ownership.
|
|
|
|
Args:
|
|
project_id: UUID of the project.
|
|
user_id: ID of the authenticated user.
|
|
session: Database session.
|
|
|
|
Returns:
|
|
The project if found and owned by the user.
|
|
|
|
Raises:
|
|
HTTPException: If project not found or user is not the owner.
|
|
"""
|
|
project = await session.get(Project, project_id)
|
|
if project is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="project not found")
|
|
if project.owner_id != user_id:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="not project owner")
|
|
return project
|
|
|
|
|
|
@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
|