Files
headquarter/apps/api/src/api/projects.py
T
alex 22474cdba5 style: fix all ruff and eslint errors across codebase
Backend (ruff):
- Fix 106 errors: move imports to top of file (E402)
- Remove unused imports (F401)
- Add missing imports for undefined names (F821)
- Remove unused variables (F841)
- Fix test_models.py broken RefreshToken test
- Fix test_projects_api.py missing TestClient import

Frontend (eslint):
- Remove unused imports/variables across 10 files
- Fix explicit any types in client.ts and sessions.ts
- Clean up empty block statements in terminal.tsx

Quality gates: ruff (pass), eslint (pass), tsc --noEmit (pass),
pytest (98 passed, 4 pre-existing failures)
2026-05-28 10:15:59 +02:00

236 lines
6.5 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_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