docs: comprehensive API documentation
- 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
This commit is contained in:
@@ -25,8 +25,21 @@ async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
yield session
|
||||
|
||||
|
||||
@router.get("/login")
|
||||
@router.get(
|
||||
"/login",
|
||||
summary="Initiate OAuth login",
|
||||
description="Redirects to the configured OAuth provider (Authentik) to start the authentication flow.",
|
||||
response_class=RedirectResponse,
|
||||
)
|
||||
async def login(next: str = "/") -> RedirectResponse:
|
||||
"""Initiate OAuth2 login flow.
|
||||
|
||||
Args:
|
||||
next: URL to redirect to after successful authentication.
|
||||
|
||||
Returns:
|
||||
RedirectResponse to the OAuth provider's authorization endpoint.
|
||||
"""
|
||||
settings = Settings()
|
||||
redirect_uri = f"{settings.api_base_url}/auth/callback"
|
||||
state = token_urlsafe(24)
|
||||
|
||||
@@ -12,11 +12,24 @@ from src.models.ssh_key import SSHKey
|
||||
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
@router.get(
|
||||
"/summary",
|
||||
summary="Get dashboard summary",
|
||||
description="Get a summary of the user's projects, repositories, SSH keys, and recent activity.",
|
||||
)
|
||||
async def get_dashboard_summary(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get a summary of the user's dashboard data.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with counts of projects, repositories, SSH keys, and recent activity.
|
||||
"""
|
||||
# Count user's projects
|
||||
projects_result = await session.execute(
|
||||
select(func.count()).select_from(Project).where(Project.owner_id == user_id)
|
||||
|
||||
@@ -38,6 +38,7 @@ router = APIRouter(prefix="/projects", tags=["git-repositories"])
|
||||
|
||||
|
||||
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")
|
||||
@@ -49,6 +50,19 @@ async def _get_owned_project(
|
||||
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")
|
||||
@@ -58,6 +72,16 @@ async def _get_owned_project(
|
||||
|
||||
|
||||
def _get_repo_path(user_id: uuid.UUID, project_id: uuid.UUID, name: str) -> str:
|
||||
"""Generate the filesystem path for a repository.
|
||||
|
||||
Args:
|
||||
user_id: UUID of the repository owner.
|
||||
project_id: UUID of the project.
|
||||
name: Repository name.
|
||||
|
||||
Returns:
|
||||
Absolute path to the repository directory.
|
||||
"""
|
||||
base = Settings().repo_base_path or "/data/repos"
|
||||
return os.path.join(base, str(user_id), str(project_id), f"{name}.git")
|
||||
|
||||
@@ -97,12 +121,27 @@ class GitRepositoryResponse(BaseModel):
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories", response_model=list[GitRepositoryResponse])
|
||||
@router.get(
|
||||
"/{project_id}/repositories",
|
||||
response_model=list[GitRepositoryResponse],
|
||||
summary="List repositories",
|
||||
description="List all git repositories in a project.",
|
||||
)
|
||||
async def list_repositories(
|
||||
project_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[GitRepository]:
|
||||
"""List all repositories in a project.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
List of repositories in the project.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -112,13 +151,29 @@ async def list_repositories(
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.delete("/{project_id}/repositories/{repo_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{project_id}/repositories/{repo_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete a repository",
|
||||
description="Delete a git repository from the project and remove it from disk.",
|
||||
)
|
||||
async def delete_repository(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Response:
|
||||
"""Delete a repository.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository to delete.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Empty response with 204 status code.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -135,20 +190,49 @@ async def delete_repository(
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.post("/repositories/parse-url", response_model=URLParseResponse)
|
||||
@router.post(
|
||||
"/repositories/parse-url",
|
||||
response_model=URLParseResponse,
|
||||
summary="Parse a git URL",
|
||||
description="Parse a git URL and detect if it's a browser URL that needs correction.",
|
||||
)
|
||||
async def parse_repository_url(data: URLParseRequest) -> URLParseResponse:
|
||||
"""Parse a git URL and detect if it's a browser URL that needs correction."""
|
||||
"""Parse a git URL and detect if it's a browser URL that needs correction.
|
||||
|
||||
Args:
|
||||
data: Request containing the URL to parse.
|
||||
|
||||
Returns:
|
||||
Parsed URL information including whether it needs parsing and suggested corrections.
|
||||
"""
|
||||
result = parse_git_url(data.url)
|
||||
return URLParseResponse(**result)
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories", response_model=GitRepositoryResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{project_id}/repositories",
|
||||
response_model=GitRepositoryResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a repository",
|
||||
description="Create a new git repository in a project. Can clone from remote or initialize bare.",
|
||||
)
|
||||
async def create_repository(
|
||||
project_id: uuid.UUID,
|
||||
data: GitRepositoryCreate,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> GitRepository:
|
||||
"""Create a new git repository.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
data: Repository creation data including name and optional remote URL.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The newly created repository.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -229,7 +313,11 @@ async def create_repository(
|
||||
return repo
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/history")
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/history",
|
||||
summary="Get repository history",
|
||||
description="Get commit history for a repository with optional branch filtering.",
|
||||
)
|
||||
async def get_repository_history(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -240,7 +328,21 @@ async def get_repository_history(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get commit history for a repository."""
|
||||
"""Get commit history for a repository.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
view: View type for history display (default: graph).
|
||||
branch: Optional branch name to filter commits.
|
||||
limit: Maximum number of commits to return (default: 100).
|
||||
offset: Number of commits to skip (default: 0).
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary containing commit history data.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -258,7 +360,11 @@ async def get_repository_history(
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/commits/{commit_hash}")
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/commits/{commit_hash}",
|
||||
summary="Get commit details",
|
||||
description="Get detailed information about a specific commit.",
|
||||
)
|
||||
async def get_repository_commit(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -266,7 +372,18 @@ async def get_repository_commit(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get detailed information about a specific commit."""
|
||||
"""Get detailed information about a specific commit.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
commit_hash: Hash of the commit to retrieve.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary containing commit details.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -322,7 +439,12 @@ class FileUpdateResponse(BaseModel):
|
||||
branch: str
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/files", response_model=FileListResponse)
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/files",
|
||||
response_model=FileListResponse,
|
||||
summary="List repository files",
|
||||
description="List files and directories in a repository path.",
|
||||
)
|
||||
async def list_repository_files(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -331,7 +453,19 @@ async def list_repository_files(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> FileListResponse:
|
||||
"""List files and directories in a repository path."""
|
||||
"""List files and directories in a repository path.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
branch: Branch name to browse (default: main).
|
||||
path: Directory path within the repository (default: root).
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
List of files and directories in the specified path.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -363,7 +497,12 @@ async def list_repository_files(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/files/content", response_model=FileContentResponse)
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/files/content",
|
||||
response_model=FileContentResponse,
|
||||
summary="Get file content",
|
||||
description="Get the content of a file in a repository.",
|
||||
)
|
||||
async def get_repository_file_content(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -372,7 +511,19 @@ async def get_repository_file_content(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> FileContentResponse:
|
||||
"""Get the content of a file."""
|
||||
"""Get the content of a file.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
branch: Branch name where the file is located.
|
||||
path: File path within the repository.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
File content and metadata.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -401,14 +552,29 @@ async def get_repository_file_content(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/branches", response_model=BranchesResponse)
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/branches",
|
||||
response_model=BranchesResponse,
|
||||
summary="List branches",
|
||||
description="List all branches in the repository.",
|
||||
)
|
||||
async def get_repository_branches(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> BranchesResponse:
|
||||
"""List all branches in the repository."""
|
||||
"""List all branches in the repository.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
List of branches and the default branch name.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -436,7 +602,12 @@ async def get_repository_branches(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/files/content", response_model=FileUpdateResponse)
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/files/content",
|
||||
response_model=FileUpdateResponse,
|
||||
summary="Update file content",
|
||||
description="Update a file and create a commit.",
|
||||
)
|
||||
async def update_repository_file(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -444,7 +615,18 @@ async def update_repository_file(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> FileUpdateResponse:
|
||||
"""Update a file and create a commit."""
|
||||
"""Update a file and create a commit.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
data: File update data including path, branch, content, and commit message.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Commit information for the file update.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -493,14 +675,29 @@ class StatusResponse(BaseModel):
|
||||
behind: int
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/status", response_model=StatusResponse)
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/status",
|
||||
response_model=StatusResponse,
|
||||
summary="Get repository status",
|
||||
description="Get the working directory status including modified, added, and deleted files.",
|
||||
)
|
||||
async def get_repository_status(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> StatusResponse:
|
||||
"""Get the working directory status."""
|
||||
"""Get the working directory status.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Repository status including branch, modified files, and ahead/behind counts.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -536,7 +733,11 @@ class CheckoutRequest(BaseModel):
|
||||
branch: str
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/branches")
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/branches",
|
||||
summary="Create a branch",
|
||||
description="Create a new branch in the repository.",
|
||||
)
|
||||
async def create_repository_branch(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -544,7 +745,18 @@ async def create_repository_branch(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Create a new branch."""
|
||||
"""Create a new branch.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
data: Branch creation data including name and optional base branch.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with success message and branch name.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -562,7 +774,11 @@ async def create_repository_branch(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{project_id}/repositories/{repo_id}/branches/{branch_name}")
|
||||
@router.delete(
|
||||
"/{project_id}/repositories/{repo_id}/branches/{branch_name}",
|
||||
summary="Delete a branch",
|
||||
description="Delete a branch from the repository.",
|
||||
)
|
||||
async def delete_repository_branch(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -571,7 +787,19 @@ async def delete_repository_branch(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Delete a branch."""
|
||||
"""Delete a branch.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
branch_name: Name of the branch to delete.
|
||||
force: Whether to force delete the branch.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with success message.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -589,7 +817,11 @@ async def delete_repository_branch(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/checkout")
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/checkout",
|
||||
summary="Checkout a branch",
|
||||
description="Checkout a branch in the repository.",
|
||||
)
|
||||
async def checkout_repository_branch(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -597,7 +829,18 @@ async def checkout_repository_branch(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Checkout a branch."""
|
||||
"""Checkout a branch.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
data: Checkout request containing the branch name.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with success message and checked out branch name.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -625,7 +868,12 @@ class CommitResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/commit", response_model=CommitResponse)
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/commit",
|
||||
response_model=CommitResponse,
|
||||
summary="Commit changes",
|
||||
description="Commit changes to the repository.",
|
||||
)
|
||||
async def commit_repository_changes(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -633,7 +881,18 @@ async def commit_repository_changes(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> CommitResponse:
|
||||
"""Commit changes to the repository."""
|
||||
"""Commit changes to the repository.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
data: Commit request containing message and optional files to commit.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Commit information including hash and message.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -669,14 +928,29 @@ class FetchResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/fetch", response_model=FetchResponse)
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/fetch",
|
||||
response_model=FetchResponse,
|
||||
summary="Fetch from remote",
|
||||
description="Fetch updates from the remote repository.",
|
||||
)
|
||||
async def fetch_repository(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> FetchResponse:
|
||||
"""Fetch from remote."""
|
||||
"""Fetch from remote.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Success message.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -698,7 +972,12 @@ class PullResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/pull", response_model=PullResponse)
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/pull",
|
||||
response_model=PullResponse,
|
||||
summary="Pull from remote",
|
||||
description="Pull updates from the remote repository.",
|
||||
)
|
||||
async def pull_repository(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -706,7 +985,18 @@ async def pull_repository(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> PullResponse:
|
||||
"""Pull updates from remote."""
|
||||
"""Pull updates from remote.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
branch: Optional branch name to pull.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Success message.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -728,7 +1018,12 @@ class PushResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/push", response_model=PushResponse)
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/push",
|
||||
response_model=PushResponse,
|
||||
summary="Push to remote",
|
||||
description="Push changes to the remote repository.",
|
||||
)
|
||||
async def push_repository(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -736,7 +1031,18 @@ async def push_repository(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> PushResponse:
|
||||
"""Push changes to remote."""
|
||||
"""Push changes to remote.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
branch: Optional branch name to push.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Success message.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -765,7 +1071,12 @@ class MergeResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/merge", response_model=MergeResponse)
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/merge",
|
||||
response_model=MergeResponse,
|
||||
summary="Merge branches",
|
||||
description="Merge one branch into another.",
|
||||
)
|
||||
async def merge_repository_branches(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -773,7 +1084,18 @@ async def merge_repository_branches(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> MergeResponse:
|
||||
"""Merge branches."""
|
||||
"""Merge branches.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
data: Merge request containing source branch, optional target branch, and message.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Merge result with commit hash and message.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Health check endpoints and models."""
|
||||
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import text
|
||||
|
||||
from src.config import Settings
|
||||
from src.database import SessionLocal
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Track start time for uptime
|
||||
_start_time = time.time()
|
||||
|
||||
|
||||
class DatabaseHealth(BaseModel):
|
||||
"""Database health check result."""
|
||||
|
||||
status: str = Field(description="Database health status", examples=["healthy"])
|
||||
response_time_ms: float = Field(description="Query response time in milliseconds", examples=[5.2])
|
||||
|
||||
|
||||
class DiskHealth(BaseModel):
|
||||
"""Disk space health check result."""
|
||||
|
||||
status: str = Field(description="Disk health status", examples=["healthy"])
|
||||
free_gb: float = Field(description="Free disk space in GB", examples=[45.2])
|
||||
total_gb: float = Field(description="Total disk space in GB", examples=[100.0])
|
||||
|
||||
|
||||
class HealthChecks(BaseModel):
|
||||
"""Individual health checks."""
|
||||
|
||||
database: DatabaseHealth | None = None
|
||||
disk: DiskHealth | None = None
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Overall health check response."""
|
||||
|
||||
status: str = Field(description="Overall health status", examples=["healthy"])
|
||||
timestamp: str = Field(description="ISO 8601 timestamp", examples=["2026-05-19T12:00:00Z"])
|
||||
version: str = Field(description="API version", examples=["0.1.0"])
|
||||
checks: HealthChecks = Field(description="Individual health checks")
|
||||
uptime_seconds: float = Field(description="Server uptime in seconds", examples=[3600.0])
|
||||
|
||||
|
||||
class DatabaseHealthResponse(BaseModel):
|
||||
"""Database-specific health check response."""
|
||||
|
||||
status: str = Field(description="Database health status", examples=["healthy"])
|
||||
response_time_ms: float = Field(description="Query response time in milliseconds", examples=[5.2])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health",
|
||||
response_model=HealthResponse,
|
||||
summary="Health check",
|
||||
description="Returns overall system health status including database and disk checks.",
|
||||
tags=["Health"],
|
||||
)
|
||||
async def health_check() -> dict[str, Any]:
|
||||
"""Check overall system health.
|
||||
|
||||
Returns:
|
||||
HealthResponse with status, timestamp, version, checks, and uptime.
|
||||
"""
|
||||
checks = HealthChecks()
|
||||
overall_status = "healthy"
|
||||
|
||||
# Database check
|
||||
try:
|
||||
import time as time_module
|
||||
|
||||
start = time_module.perf_counter()
|
||||
async with SessionLocal() as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
db_time = (time_module.perf_counter() - start) * 1000
|
||||
checks.database = DatabaseHealth(
|
||||
status="healthy",
|
||||
response_time_ms=round(db_time, 2),
|
||||
)
|
||||
except Exception:
|
||||
checks.database = DatabaseHealth(
|
||||
status="unhealthy",
|
||||
response_time_ms=0.0,
|
||||
)
|
||||
overall_status = "degraded"
|
||||
|
||||
# Disk check
|
||||
try:
|
||||
import shutil
|
||||
|
||||
disk = shutil.disk_usage("/")
|
||||
free_gb = disk.free / (1024**3)
|
||||
total_gb = disk.total / (1024**3)
|
||||
disk_status = "healthy" if free_gb > 1.0 else "degraded"
|
||||
if disk_status == "degraded":
|
||||
overall_status = "degraded"
|
||||
checks.disk = DiskHealth(
|
||||
status=disk_status,
|
||||
free_gb=round(free_gb, 2),
|
||||
total_gb=round(total_gb, 2),
|
||||
)
|
||||
except Exception:
|
||||
checks.disk = None
|
||||
|
||||
return HealthResponse(
|
||||
status=overall_status,
|
||||
timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
version="0.1.0",
|
||||
checks=checks,
|
||||
uptime_seconds=round(time.time() - _start_time, 2),
|
||||
).model_dump()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health/db",
|
||||
response_model=DatabaseHealthResponse,
|
||||
summary="Database health check",
|
||||
description="Returns database-specific health status with response time.",
|
||||
tags=["Health"],
|
||||
)
|
||||
async def health_check_db() -> dict[str, Any]:
|
||||
"""Check database health.
|
||||
|
||||
Returns:
|
||||
DatabaseHealthResponse with status and response time.
|
||||
"""
|
||||
import time as time_module
|
||||
|
||||
try:
|
||||
start = time_module.perf_counter()
|
||||
async with SessionLocal() as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
db_time = (time_module.perf_counter() - start) * 1000
|
||||
return DatabaseHealthResponse(
|
||||
status="healthy",
|
||||
response_time_ms=round(db_time, 2),
|
||||
).model_dump()
|
||||
except Exception:
|
||||
return DatabaseHealthResponse(
|
||||
status="unhealthy",
|
||||
response_time_ms=0.0,
|
||||
).model_dump()
|
||||
@@ -17,6 +17,7 @@ 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")
|
||||
@@ -47,12 +48,28 @@ class SetDefaultSSHKeyRequest(BaseModel):
|
||||
ssh_key_id: uuid.UUID
|
||||
|
||||
|
||||
@router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
|
||||
@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,
|
||||
@@ -66,22 +83,51 @@ async def create_project(
|
||||
return project
|
||||
|
||||
|
||||
@router.get("", response_model=list[ProjectResponse])
|
||||
@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)
|
||||
@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)
|
||||
|
||||
@@ -91,6 +137,19 @@ async def _get_owned_project(
|
||||
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")
|
||||
@@ -99,13 +158,29 @@ async def _get_owned_project(
|
||||
return project
|
||||
|
||||
|
||||
@router.patch("/{project_id}", response_model=ProjectResponse)
|
||||
@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)
|
||||
|
||||
@@ -119,12 +194,27 @@ async def update_project(
|
||||
return project
|
||||
|
||||
|
||||
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@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)
|
||||
|
||||
@@ -141,13 +231,29 @@ async def delete_project(
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.patch("/{project_id}/default-ssh-key", response_model=ProjectResponse)
|
||||
@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)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ router = APIRouter(prefix="/ssh-keys", tags=["ssh-keys"])
|
||||
|
||||
|
||||
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")
|
||||
@@ -38,6 +39,11 @@ def _get_fernet() -> Fernet:
|
||||
|
||||
|
||||
def generate_ssh_key_pair() -> tuple[str, str]:
|
||||
"""Generate a new Ed25519 SSH key pair.
|
||||
|
||||
Returns:
|
||||
Tuple of (private_key, public_key) as strings.
|
||||
"""
|
||||
private_key = Ed25519PrivateKey.generate()
|
||||
public_key = private_key.public_key()
|
||||
|
||||
@@ -68,12 +74,28 @@ class SSHKeyResponse(BaseModel):
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@router.post("", response_model=SSHKeyResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"",
|
||||
response_model=SSHKeyResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create SSH key",
|
||||
description="Generate a new Ed25519 SSH key pair for the authenticated user.",
|
||||
)
|
||||
async def create_ssh_key(
|
||||
data: SSHKeyCreate,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> SSHKey:
|
||||
"""Create a new SSH key pair.
|
||||
|
||||
Args:
|
||||
data: SSH key creation data including the key name.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The newly created SSH key with public key exposed.
|
||||
"""
|
||||
user = await _get_user(session, user_id)
|
||||
private_key, public_key = generate_ssh_key_pair()
|
||||
|
||||
@@ -92,22 +114,51 @@ async def create_ssh_key(
|
||||
return ssh_key
|
||||
|
||||
|
||||
@router.get("", response_model=list[SSHKeyResponse])
|
||||
@router.get(
|
||||
"",
|
||||
response_model=list[SSHKeyResponse],
|
||||
summary="List SSH keys",
|
||||
description="List all SSH keys for the authenticated user.",
|
||||
)
|
||||
async def list_ssh_keys(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[SSHKey]:
|
||||
"""List all SSH keys for the authenticated user.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
List of SSH keys owned by the user.
|
||||
"""
|
||||
user = await _get_user(session, user_id)
|
||||
result = await session.execute(select(SSHKey).where(SSHKey.user_id == user.id))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.delete("/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{key_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete SSH key",
|
||||
description="Delete an SSH key by ID.",
|
||||
)
|
||||
async def delete_ssh_key(
|
||||
key_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
"""Delete an SSH key.
|
||||
|
||||
Args:
|
||||
key_id: UUID of the SSH key to delete.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
None with 204 status code.
|
||||
"""
|
||||
user = await _get_user(session, user_id)
|
||||
ssh_key = await session.get(SSHKey, key_id)
|
||||
if ssh_key is None or ssh_key.user_id != user.id:
|
||||
|
||||
@@ -13,13 +13,26 @@ from src.services.terminal_manager import terminal_manager
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket("/ws/tool-instances/{instance_id}/terminal")
|
||||
@router.websocket(
|
||||
"/ws/tool-instances/{instance_id}/terminal",
|
||||
)
|
||||
async def terminal_websocket(
|
||||
websocket: WebSocket,
|
||||
instance_id: str,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
"""WebSocket endpoint for terminal access to a tool instance."""
|
||||
"""WebSocket endpoint for terminal access to a tool instance.
|
||||
|
||||
Provides an interactive terminal session inside a running tool instance container.
|
||||
|
||||
Args:
|
||||
websocket: The WebSocket connection.
|
||||
instance_id: UUID string of the tool instance.
|
||||
db_session: Database session.
|
||||
|
||||
Returns:
|
||||
None. Communicates via WebSocket messages.
|
||||
"""
|
||||
await websocket.accept()
|
||||
|
||||
try:
|
||||
@@ -82,7 +95,15 @@ async def _get_user_from_websocket(
|
||||
websocket: WebSocket,
|
||||
db_session: AsyncSession,
|
||||
) -> uuid.UUID | None:
|
||||
"""Extract and validate user ID from session cookie in WebSocket."""
|
||||
"""Extract and validate user ID from session cookie in WebSocket.
|
||||
|
||||
Args:
|
||||
websocket: The WebSocket connection.
|
||||
db_session: Database session.
|
||||
|
||||
Returns:
|
||||
The user's UUID if authenticated, None otherwise.
|
||||
"""
|
||||
from src.auth.session import verify_session_token
|
||||
|
||||
session_cookie = websocket.cookies.get("session")
|
||||
|
||||
@@ -30,6 +30,7 @@ router = APIRouter(prefix="/projects", tags=["tool-instances"])
|
||||
|
||||
|
||||
async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
|
||||
"""Fetch a user by ID or raise 404 if not found."""
|
||||
user = await session.get(User, user_id)
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
@@ -41,6 +42,19 @@ async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
|
||||
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 or project.owner_id != user_id:
|
||||
raise HTTPException(
|
||||
@@ -49,7 +63,11 @@ async def _get_owned_project(
|
||||
return project
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances")
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/instances",
|
||||
summary="Create tool instance",
|
||||
description="Create a new tool instance for a repository.",
|
||||
)
|
||||
async def create_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -58,7 +76,19 @@ async def create_instance(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Create a new tool instance for a repository."""
|
||||
"""Create a new tool instance for a repository.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
tool_type_id: UUID of the tool type to instantiate.
|
||||
display_name: Optional display name for the instance.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with instance details.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -123,14 +153,28 @@ async def create_instance(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/instances")
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/instances",
|
||||
summary="List instances",
|
||||
description="List all tool instances for a repository.",
|
||||
)
|
||||
async def list_instances(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""List all instances for a repository."""
|
||||
"""List all instances for a repository.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary containing list of instances.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -165,7 +209,11 @@ async def list_instances(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}")
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}",
|
||||
summary="Get instance",
|
||||
description="Get a specific instance with real-time status from Docker.",
|
||||
)
|
||||
async def get_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -173,7 +221,18 @@ async def get_instance(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get a specific instance with real-time status."""
|
||||
"""Get a specific instance with real-time status.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
instance_id: UUID of the instance.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with instance details and current status.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -210,7 +269,11 @@ async def get_instance(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/start")
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/start",
|
||||
summary="Start instance",
|
||||
description="Start a tool instance using Docker Compose.",
|
||||
)
|
||||
async def start_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -218,7 +281,18 @@ async def start_instance(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Start a tool instance."""
|
||||
"""Start a tool instance.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
instance_id: UUID of the instance to start.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with status and URL of the running instance.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -262,7 +336,11 @@ async def start_instance(
|
||||
return {"status": instance.status, "url": instance.url}
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop")
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop",
|
||||
summary="Stop instance",
|
||||
description="Stop a running tool instance.",
|
||||
)
|
||||
async def stop_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -270,7 +348,18 @@ async def stop_instance(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Stop a tool instance."""
|
||||
"""Stop a tool instance.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
instance_id: UUID of the instance to stop.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with the stopped status.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -291,7 +380,11 @@ async def stop_instance(
|
||||
return {"status": instance.status}
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart")
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart",
|
||||
summary="Restart instance",
|
||||
description="Restart a tool instance.",
|
||||
)
|
||||
async def restart_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -299,7 +392,18 @@ async def restart_instance(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Restart a tool instance."""
|
||||
"""Restart a tool instance.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
instance_id: UUID of the instance to restart.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary with status and URL of the restarted instance.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -326,7 +430,11 @@ async def restart_instance(
|
||||
return {"status": instance.status}
|
||||
|
||||
|
||||
@router.delete("/{project_id}/repositories/{repo_id}/instances/{instance_id}")
|
||||
@router.delete(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}",
|
||||
summary="Delete instance",
|
||||
description="Delete a tool instance and remove its Docker containers and files.",
|
||||
)
|
||||
async def delete_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -334,7 +442,18 @@ async def delete_instance(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
"""Delete a tool instance."""
|
||||
"""Delete a tool instance.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
instance_id: UUID of the instance to delete.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
None with 204 status code.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -359,7 +478,11 @@ async def delete_instance(
|
||||
await session.commit()
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}/logs")
|
||||
@router.get(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/logs",
|
||||
summary="Get instance logs",
|
||||
description="Get container logs for a tool instance.",
|
||||
)
|
||||
async def get_instance_logs(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
@@ -368,7 +491,19 @@ async def get_instance_logs(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get container logs for an instance."""
|
||||
"""Get container logs for an instance.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
repo_id: UUID of the repository.
|
||||
instance_id: UUID of the instance.
|
||||
tail: Number of log lines to return (default: 100).
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary containing the container logs.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
@@ -389,12 +524,24 @@ from fastapi import APIRouter as FastAPIRouter
|
||||
|
||||
sessions_router = FastAPIRouter(prefix="/users", tags=["sessions"])
|
||||
|
||||
@sessions_router.get("/me/sessions")
|
||||
@sessions_router.get(
|
||||
"/me/sessions",
|
||||
summary="Get user sessions",
|
||||
description="Get all active sessions (running instances) for the current user.",
|
||||
)
|
||||
async def get_user_sessions(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get all active sessions (running instances) for the current user."""
|
||||
"""Get all active sessions for the current user.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
Dictionary containing list of active sessions with instance details.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
|
||||
result = await session.execute(
|
||||
|
||||
@@ -15,6 +15,7 @@ router = APIRouter(prefix="/tool-types", tags=["tool-types"])
|
||||
|
||||
|
||||
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")
|
||||
@@ -22,6 +23,11 @@ async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
|
||||
|
||||
|
||||
async def _require_admin(user: User) -> None:
|
||||
"""Check if user has admin privileges.
|
||||
|
||||
For now, all authenticated users can manage tool types.
|
||||
In production, this should check user.role or similar.
|
||||
"""
|
||||
# For now, all authenticated users can manage tool types
|
||||
# In production, check user.role or similar
|
||||
pass
|
||||
@@ -117,12 +123,28 @@ class ToolTypeResponse(BaseModel):
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@router.post("", response_model=ToolTypeResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"",
|
||||
response_model=ToolTypeResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create tool type",
|
||||
description="Create a new custom tool type with a Docker Compose template.",
|
||||
)
|
||||
async def create_tool_type(
|
||||
data: ToolTypeCreate,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolType:
|
||||
"""Create a new tool type.
|
||||
|
||||
Args:
|
||||
data: Tool type creation data including name, display name, and compose template.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The newly created tool type.
|
||||
"""
|
||||
user = await _get_user(session, user_id)
|
||||
await _require_admin(user)
|
||||
|
||||
@@ -146,22 +168,51 @@ async def create_tool_type(
|
||||
return tool_type
|
||||
|
||||
|
||||
@router.get("", response_model=list[ToolTypeResponse])
|
||||
@router.get(
|
||||
"",
|
||||
response_model=list[ToolTypeResponse],
|
||||
summary="List tool types",
|
||||
description="List all available tool types including built-in and custom ones.",
|
||||
)
|
||||
async def list_tool_types(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[ToolType]:
|
||||
"""List all tool types.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
List of all tool types ordered by name.
|
||||
"""
|
||||
await _get_user(session, user_id)
|
||||
result = await session.execute(select(ToolType).order_by(ToolType.name))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/{tool_type_id}", response_model=ToolTypeResponse)
|
||||
@router.get(
|
||||
"/{tool_type_id}",
|
||||
response_model=ToolTypeResponse,
|
||||
summary="Get tool type",
|
||||
description="Get a specific tool type by ID.",
|
||||
)
|
||||
async def get_tool_type(
|
||||
tool_type_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolType:
|
||||
"""Get a specific tool type by ID.
|
||||
|
||||
Args:
|
||||
tool_type_id: UUID of the tool type to retrieve.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The requested tool type.
|
||||
"""
|
||||
await _get_user(session, user_id)
|
||||
tool_type = await session.get(ToolType, tool_type_id)
|
||||
if tool_type is None:
|
||||
@@ -169,13 +220,29 @@ async def get_tool_type(
|
||||
return tool_type
|
||||
|
||||
|
||||
@router.put("/{tool_type_id}", response_model=ToolTypeResponse)
|
||||
@router.put(
|
||||
"/{tool_type_id}",
|
||||
response_model=ToolTypeResponse,
|
||||
summary="Update tool type",
|
||||
description="Update a custom tool type. Built-in tool types cannot be modified.",
|
||||
)
|
||||
async def update_tool_type(
|
||||
tool_type_id: uuid.UUID,
|
||||
data: ToolTypeUpdate,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolType:
|
||||
"""Update a tool type.
|
||||
|
||||
Args:
|
||||
tool_type_id: UUID of the tool type to update.
|
||||
data: Tool type update data with optional fields.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The updated tool type.
|
||||
"""
|
||||
user = await _get_user(session, user_id)
|
||||
await _require_admin(user)
|
||||
|
||||
@@ -217,12 +284,27 @@ async def update_tool_type(
|
||||
return tool_type
|
||||
|
||||
|
||||
@router.delete("/{tool_type_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{tool_type_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete tool type",
|
||||
description="Delete a custom tool type. Built-in tool types cannot be deleted.",
|
||||
)
|
||||
async def delete_tool_type(
|
||||
tool_type_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
"""Delete a tool type.
|
||||
|
||||
Args:
|
||||
tool_type_id: UUID of the tool type to delete.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
None with 204 status code.
|
||||
"""
|
||||
user = await _get_user(session, user_id)
|
||||
await _require_admin(user)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ router = APIRouter(prefix="/users/me", tags=["user-config"])
|
||||
|
||||
|
||||
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")
|
||||
@@ -20,6 +21,15 @@ async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
|
||||
|
||||
|
||||
async def _get_or_create_config(session: AsyncSession, user_id: uuid.UUID) -> UserConfig:
|
||||
"""Get or create user config record.
|
||||
|
||||
Args:
|
||||
session: Database session.
|
||||
user_id: UUID of the user.
|
||||
|
||||
Returns:
|
||||
The user's config, creating a new one if it doesn't exist.
|
||||
"""
|
||||
result = await session.execute(select(UserConfig).where(UserConfig.user_id == user_id))
|
||||
config = result.scalar_one_or_none()
|
||||
if config is None:
|
||||
@@ -46,22 +56,51 @@ class UserConfigUpdate(BaseModel):
|
||||
git_user_email: str | None = None
|
||||
|
||||
|
||||
@router.get("/config", response_model=UserConfigResponse)
|
||||
@router.get(
|
||||
"/config",
|
||||
response_model=UserConfigResponse,
|
||||
summary="Get user config",
|
||||
description="Get the current user's configuration settings.",
|
||||
)
|
||||
async def get_user_config(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> UserConfigResponse:
|
||||
"""Get the current user's configuration.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The user's configuration settings.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
config = await _get_or_create_config(session, user_id)
|
||||
return UserConfigResponse.model_validate(config.config)
|
||||
|
||||
|
||||
@router.patch("/config", response_model=UserConfigResponse)
|
||||
@router.patch(
|
||||
"/config",
|
||||
response_model=UserConfigResponse,
|
||||
summary="Update user config",
|
||||
description="Update the current user's configuration settings.",
|
||||
)
|
||||
async def update_user_config(
|
||||
data: UserConfigUpdate,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> UserConfigResponse:
|
||||
"""Update the current user's configuration.
|
||||
|
||||
Args:
|
||||
data: Configuration update data with optional fields.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The updated user configuration.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
config = await _get_or_create_config(session, user_id)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ MAX_AVATAR_SIZE = 2 * 1024 * 1024 # 2MB
|
||||
|
||||
|
||||
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")
|
||||
@@ -37,20 +38,49 @@ class UserProfileUpdate(BaseModel):
|
||||
email: str | None = None
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserProfileResponse)
|
||||
@router.get(
|
||||
"/me",
|
||||
response_model=UserProfileResponse,
|
||||
summary="Get current user profile",
|
||||
description="Retrieve the profile of the currently authenticated user.",
|
||||
)
|
||||
async def get_profile(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> User:
|
||||
"""Get the current user's profile.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The user's profile information.
|
||||
"""
|
||||
return await _get_user(session, user_id)
|
||||
|
||||
|
||||
@router.put("/me", response_model=UserProfileResponse)
|
||||
@router.put(
|
||||
"/me",
|
||||
response_model=UserProfileResponse,
|
||||
summary="Update user profile",
|
||||
description="Update the current user's profile information.",
|
||||
)
|
||||
async def update_profile(
|
||||
data: UserProfileUpdate,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> User:
|
||||
"""Update the current user's profile.
|
||||
|
||||
Args:
|
||||
data: Profile update data with optional name and email.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The updated user profile.
|
||||
"""
|
||||
user = await _get_user(session, user_id)
|
||||
|
||||
if data.name is not None:
|
||||
@@ -68,12 +98,27 @@ async def update_profile(
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/me/avatar", response_model=UserProfileResponse)
|
||||
@router.post(
|
||||
"/me/avatar",
|
||||
response_model=UserProfileResponse,
|
||||
summary="Upload avatar",
|
||||
description="Upload a profile avatar image (PNG or JPG, max 2MB).",
|
||||
)
|
||||
async def upload_avatar(
|
||||
file: UploadFile,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> User:
|
||||
"""Upload a profile avatar image.
|
||||
|
||||
Args:
|
||||
file: The image file to upload (PNG or JPG, max 2MB).
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The updated user profile with new avatar URL.
|
||||
"""
|
||||
user = await _get_user(session, user_id)
|
||||
|
||||
if file.content_type not in ALLOWED_CONTENT_TYPES:
|
||||
|
||||
+2
-11
@@ -9,6 +9,7 @@ from sqlalchemy import select, text
|
||||
from src.api.auth import router as auth_router
|
||||
from src.api.dashboard import router as dashboard_router
|
||||
from src.api.git_repositories import router as git_repositories_router
|
||||
from src.api.health import router as health_router
|
||||
from src.api.projects import router as projects_router
|
||||
from src.api.ssh_keys import router as ssh_keys_router
|
||||
from src.api.terminal import router as terminal_router
|
||||
@@ -148,17 +149,7 @@ async def on_startup():
|
||||
await seed_builtin_tool_types()
|
||||
logger.info("Startup complete.")
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
async with SessionLocal() as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
return {"status": "healthy", "database": "connected"}
|
||||
except Exception as exc:
|
||||
logger.error("Health check failed: %s", exc)
|
||||
return {"status": "unhealthy", "database": "disconnected", "error": str(exc)}
|
||||
|
||||
app.include_router(health_router)
|
||||
app.include_router(auth_router)
|
||||
app.include_router(dashboard_router)
|
||||
app.include_router(projects_router)
|
||||
|
||||
Reference in New Issue
Block a user