refactor: extract shared auth dependencies (Task 3.1)
- Add get_owned_project() to auth/dependencies.py - Remove duplicated _get_user() and _get_owned_project() from all routers - Update tool_instances, git_repositories, projects, ssh_keys, users, user_config, tool_types routers to use FastAPI dependency injection - Route handlers now receive User/Project models via Depends() instead of calling inline async helpers Quality gates: Python syntax check (pass), no duplicated helpers (pass) Refs: repo-restructure Task 3.1
This commit is contained in:
@@ -10,7 +10,7 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user, get_db_session, get_owned_project
|
||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
from src.models.git_repository import GitRepository
|
from src.models.git_repository import GitRepository
|
||||||
from src.models.project import Project
|
from src.models.project import Project
|
||||||
@@ -40,38 +40,6 @@ router = APIRouter(prefix="/projects", tags=["git-repositories"])
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def _get_repo_path(user_id: uuid.UUID, project_id: uuid.UUID, name: str) -> str:
|
def _get_repo_path(user_id: uuid.UUID, project_id: uuid.UUID, name: str) -> str:
|
||||||
@@ -214,7 +182,8 @@ class GitRepositoryResponse(BaseModel):
|
|||||||
)
|
)
|
||||||
async def list_repositories(
|
async def list_repositories(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> list[GitRepository]:
|
) -> list[GitRepository]:
|
||||||
"""List all repositories in a project.
|
"""List all repositories in a project.
|
||||||
@@ -227,8 +196,6 @@ async def list_repositories(
|
|||||||
Returns:
|
Returns:
|
||||||
List of repositories in the project.
|
List of repositories in the project.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(GitRepository).where(GitRepository.project_id == project_id)
|
select(GitRepository).where(GitRepository.project_id == project_id)
|
||||||
@@ -245,7 +212,8 @@ async def list_repositories(
|
|||||||
async def delete_repository(
|
async def delete_repository(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""Delete a repository.
|
"""Delete a repository.
|
||||||
@@ -259,8 +227,6 @@ async def delete_repository(
|
|||||||
Returns:
|
Returns:
|
||||||
Empty response with 204 status code.
|
Empty response with 204 status code.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -304,7 +270,8 @@ async def parse_repository_url(data: URLParseRequest) -> URLParseResponse:
|
|||||||
async def create_repository(
|
async def create_repository(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
data: GitRepositoryCreate,
|
data: GitRepositoryCreate,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> GitRepository:
|
) -> GitRepository:
|
||||||
"""Create a new git repository.
|
"""Create a new git repository.
|
||||||
@@ -318,8 +285,6 @@ async def create_repository(
|
|||||||
Returns:
|
Returns:
|
||||||
The newly created repository.
|
The newly created repository.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
# Check for duplicate name
|
# Check for duplicate name
|
||||||
existing = await session.execute(
|
existing = await session.execute(
|
||||||
@@ -352,7 +317,7 @@ async def create_repository(
|
|||||||
if remote_url:
|
if remote_url:
|
||||||
_preflight_remote_repository(remote_url)
|
_preflight_remote_repository(remote_url)
|
||||||
|
|
||||||
repo_path = _get_repo_path(user_id, project_id, data.name)
|
repo_path = _get_repo_path(user.id, project_id, data.name)
|
||||||
|
|
||||||
# Ensure parent directory exists
|
# Ensure parent directory exists
|
||||||
os.makedirs(os.path.dirname(repo_path), exist_ok=True)
|
os.makedirs(os.path.dirname(repo_path), exist_ok=True)
|
||||||
@@ -366,7 +331,7 @@ async def create_repository(
|
|||||||
name=data.name,
|
name=data.name,
|
||||||
path=repo_path,
|
path=repo_path,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owner_id=user_id,
|
owner_id=user.id,
|
||||||
is_mirror=False,
|
is_mirror=False,
|
||||||
remote_url=remote_url,
|
remote_url=remote_url,
|
||||||
)
|
)
|
||||||
@@ -388,7 +353,8 @@ async def get_repository_history(
|
|||||||
branch: str | None = None,
|
branch: str | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Get commit history for a repository.
|
"""Get commit history for a repository.
|
||||||
@@ -406,8 +372,6 @@ async def get_repository_history(
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary containing commit history data.
|
Dictionary containing commit history data.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -432,7 +396,8 @@ async def get_repository_commit(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
commit_hash: str,
|
commit_hash: str,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Get detailed information about a specific commit.
|
"""Get detailed information about a specific commit.
|
||||||
@@ -447,8 +412,6 @@ async def get_repository_commit(
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary containing commit details.
|
Dictionary containing commit details.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -513,7 +476,8 @@ async def list_repository_files(
|
|||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
branch: str = "main",
|
branch: str = "main",
|
||||||
path: str = "",
|
path: str = "",
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> FileListResponse:
|
) -> FileListResponse:
|
||||||
"""List files and directories in a repository path.
|
"""List files and directories in a repository path.
|
||||||
@@ -529,8 +493,6 @@ async def list_repository_files(
|
|||||||
Returns:
|
Returns:
|
||||||
List of files and directories in the specified path.
|
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)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -579,7 +541,8 @@ async def get_repository_file_content(
|
|||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
branch: str,
|
branch: str,
|
||||||
path: str,
|
path: str,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> FileContentResponse:
|
) -> FileContentResponse:
|
||||||
"""Get the content of a file.
|
"""Get the content of a file.
|
||||||
@@ -595,8 +558,6 @@ async def get_repository_file_content(
|
|||||||
Returns:
|
Returns:
|
||||||
File content and metadata.
|
File content and metadata.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -632,7 +593,8 @@ async def get_repository_file_content(
|
|||||||
async def get_repository_branches(
|
async def get_repository_branches(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> BranchesResponse:
|
) -> BranchesResponse:
|
||||||
"""List all branches in the repository.
|
"""List all branches in the repository.
|
||||||
@@ -646,8 +608,6 @@ async def get_repository_branches(
|
|||||||
Returns:
|
Returns:
|
||||||
List of branches and the default branch name.
|
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)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -689,7 +649,8 @@ async def update_repository_file(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
data: FileUpdateRequest,
|
data: FileUpdateRequest,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> FileUpdateResponse:
|
) -> FileUpdateResponse:
|
||||||
"""Update a file and create a commit.
|
"""Update a file and create a commit.
|
||||||
@@ -704,8 +665,6 @@ async def update_repository_file(
|
|||||||
Returns:
|
Returns:
|
||||||
Commit information for the file update.
|
Commit information for the file update.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -715,7 +674,6 @@ async def update_repository_file(
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
|
||||||
|
|
||||||
# Get user info for commit
|
# Get user info for commit
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
author_name = user.name or "Unknown"
|
author_name = user.name or "Unknown"
|
||||||
author_email = user.email or "unknown@example.com"
|
author_email = user.email or "unknown@example.com"
|
||||||
|
|
||||||
@@ -761,7 +719,8 @@ class StatusResponse(BaseModel):
|
|||||||
async def get_repository_status(
|
async def get_repository_status(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> StatusResponse:
|
) -> StatusResponse:
|
||||||
"""Get the working directory status.
|
"""Get the working directory status.
|
||||||
@@ -775,8 +734,6 @@ async def get_repository_status(
|
|||||||
Returns:
|
Returns:
|
||||||
Repository status including branch, modified files, and ahead/behind counts.
|
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)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -819,7 +776,8 @@ async def create_repository_branch(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
data: BranchCreateRequest,
|
data: BranchCreateRequest,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Create a new branch.
|
"""Create a new branch.
|
||||||
@@ -834,8 +792,6 @@ async def create_repository_branch(
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with success message and branch name.
|
Dictionary with success message and branch name.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -861,7 +817,8 @@ async def delete_repository_branch(
|
|||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
branch_name: str,
|
branch_name: str,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Delete a branch.
|
"""Delete a branch.
|
||||||
@@ -877,8 +834,6 @@ async def delete_repository_branch(
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with success message.
|
Dictionary with success message.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -903,7 +858,8 @@ async def checkout_repository_branch(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
data: CheckoutRequest,
|
data: CheckoutRequest,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Checkout a branch.
|
"""Checkout a branch.
|
||||||
@@ -918,8 +874,6 @@ async def checkout_repository_branch(
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with success message and checked out branch name.
|
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)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -955,7 +909,8 @@ async def commit_repository_changes(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
data: CommitRequest,
|
data: CommitRequest,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> CommitResponse:
|
) -> CommitResponse:
|
||||||
"""Commit changes to the repository.
|
"""Commit changes to the repository.
|
||||||
@@ -970,8 +925,6 @@ async def commit_repository_changes(
|
|||||||
Returns:
|
Returns:
|
||||||
Commit information including hash and message.
|
Commit information including hash and message.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -981,7 +934,6 @@ async def commit_repository_changes(
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
|
||||||
|
|
||||||
# Get user info for commit
|
# Get user info for commit
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
author_name = user.name or "Unknown"
|
author_name = user.name or "Unknown"
|
||||||
author_email = user.email or "unknown@example.com"
|
author_email = user.email or "unknown@example.com"
|
||||||
|
|
||||||
@@ -1014,7 +966,8 @@ class FetchResponse(BaseModel):
|
|||||||
async def fetch_repository(
|
async def fetch_repository(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> FetchResponse:
|
) -> FetchResponse:
|
||||||
"""Fetch from remote.
|
"""Fetch from remote.
|
||||||
@@ -1028,8 +981,6 @@ async def fetch_repository(
|
|||||||
Returns:
|
Returns:
|
||||||
Success message.
|
Success message.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -1059,7 +1010,8 @@ async def pull_repository(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
branch: str | None = None,
|
branch: str | None = None,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> PullResponse:
|
) -> PullResponse:
|
||||||
"""Pull updates from remote.
|
"""Pull updates from remote.
|
||||||
@@ -1074,8 +1026,6 @@ async def pull_repository(
|
|||||||
Returns:
|
Returns:
|
||||||
Success message.
|
Success message.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -1105,7 +1055,8 @@ async def push_repository(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
branch: str | None = None,
|
branch: str | None = None,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> PushResponse:
|
) -> PushResponse:
|
||||||
"""Push changes to remote.
|
"""Push changes to remote.
|
||||||
@@ -1120,8 +1071,6 @@ async def push_repository(
|
|||||||
Returns:
|
Returns:
|
||||||
Success message.
|
Success message.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
@@ -1158,7 +1107,8 @@ async def merge_repository_branches(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
repo_id: uuid.UUID,
|
repo_id: uuid.UUID,
|
||||||
data: MergeRequest,
|
data: MergeRequest,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> MergeResponse:
|
) -> MergeResponse:
|
||||||
"""Merge branches.
|
"""Merge branches.
|
||||||
@@ -1173,8 +1123,6 @@ async def merge_repository_branches(
|
|||||||
Returns:
|
Returns:
|
||||||
Merge result with commit hash and message.
|
Merge result with commit hash and message.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
|
||||||
_project = await _get_owned_project(project_id, user_id, session)
|
|
||||||
|
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user, get_db_session, get_owned_project
|
||||||
from src.models.git_repository import GitRepository
|
from src.models.git_repository import GitRepository
|
||||||
from src.models.project import Project
|
from src.models.project import Project
|
||||||
from src.models.ssh_key import SSHKey
|
from src.models.ssh_key import SSHKey
|
||||||
@@ -16,13 +16,6 @@ from src.models.user import User
|
|||||||
router = APIRouter(prefix="/projects", tags=["projects"])
|
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):
|
class ProjectCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
@@ -57,7 +50,7 @@ class SetDefaultSSHKeyRequest(BaseModel):
|
|||||||
)
|
)
|
||||||
async def create_project(
|
async def create_project(
|
||||||
data: ProjectCreate,
|
data: ProjectCreate,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> Project:
|
) -> Project:
|
||||||
"""Create a new project.
|
"""Create a new project.
|
||||||
@@ -70,7 +63,6 @@ async def create_project(
|
|||||||
Returns:
|
Returns:
|
||||||
The newly created project.
|
The newly created project.
|
||||||
"""
|
"""
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
project = Project(
|
project = Project(
|
||||||
name=data.name,
|
name=data.name,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
@@ -90,7 +82,7 @@ async def create_project(
|
|||||||
description="Retrieve all projects owned by the authenticated user.",
|
description="Retrieve all projects owned by the authenticated user.",
|
||||||
)
|
)
|
||||||
async def list_projects(
|
async def list_projects(
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> list[Project]:
|
) -> list[Project]:
|
||||||
"""List all projects for the authenticated user.
|
"""List all projects for the authenticated user.
|
||||||
@@ -102,7 +94,6 @@ async def list_projects(
|
|||||||
Returns:
|
Returns:
|
||||||
List of projects owned by the user.
|
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))
|
result = await session.execute(select(Project).where(Project.owner_id == user.id))
|
||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
|
|
||||||
@@ -115,7 +106,8 @@ async def list_projects(
|
|||||||
)
|
)
|
||||||
async def get_project(
|
async def get_project(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> Project:
|
) -> Project:
|
||||||
"""Get a specific project by ID.
|
"""Get a specific project by ID.
|
||||||
@@ -128,36 +120,10 @@ async def get_project(
|
|||||||
Returns:
|
Returns:
|
||||||
The requested project.
|
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
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch(
|
@router.patch(
|
||||||
"/{project_id}",
|
"/{project_id}",
|
||||||
response_model=ProjectResponse,
|
response_model=ProjectResponse,
|
||||||
@@ -167,7 +133,8 @@ async def _get_owned_project(
|
|||||||
async def update_project(
|
async def update_project(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
data: ProjectUpdate,
|
data: ProjectUpdate,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> Project:
|
) -> Project:
|
||||||
"""Update a project.
|
"""Update a project.
|
||||||
@@ -181,8 +148,6 @@ async def update_project(
|
|||||||
Returns:
|
Returns:
|
||||||
The updated project.
|
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:
|
if data.name is not None:
|
||||||
project.name = data.name
|
project.name = data.name
|
||||||
@@ -202,7 +167,8 @@ async def update_project(
|
|||||||
)
|
)
|
||||||
async def delete_project(
|
async def delete_project(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""Delete a project and all its repositories.
|
"""Delete a project and all its repositories.
|
||||||
@@ -215,8 +181,6 @@ async def delete_project(
|
|||||||
Returns:
|
Returns:
|
||||||
Empty response with 204 status code.
|
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
|
# Delete repositories from disk and database
|
||||||
result = await session.execute(select(GitRepository).where(GitRepository.project_id == project_id))
|
result = await session.execute(select(GitRepository).where(GitRepository.project_id == project_id))
|
||||||
@@ -240,7 +204,8 @@ async def delete_project(
|
|||||||
async def set_default_ssh_key(
|
async def set_default_ssh_key(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
data: SetDefaultSSHKeyRequest,
|
data: SetDefaultSSHKeyRequest,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
|
project: Project = Depends(get_owned_project),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> Project:
|
) -> Project:
|
||||||
"""Set the default SSH key for a project.
|
"""Set the default SSH key for a project.
|
||||||
@@ -254,8 +219,6 @@ async def set_default_ssh_key(
|
|||||||
Returns:
|
Returns:
|
||||||
The updated project.
|
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)
|
ssh_key = await session.get(SSHKey, data.ssh_key_id)
|
||||||
if ssh_key is None or ssh_key.user_id != user.id:
|
if ssh_key is None or ssh_key.user_id != user.id:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user, get_db_session
|
||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
from src.models.ssh_key import SSHKey
|
from src.models.ssh_key import SSHKey
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
@@ -17,13 +17,6 @@ from src.models.user import User
|
|||||||
router = APIRouter(prefix="/ssh-keys", tags=["ssh-keys"])
|
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")
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def _get_fernet() -> Fernet:
|
def _get_fernet() -> Fernet:
|
||||||
"""Generate a valid Fernet key from the session secret."""
|
"""Generate a valid Fernet key from the session secret."""
|
||||||
@@ -83,7 +76,7 @@ class SSHKeyResponse(BaseModel):
|
|||||||
)
|
)
|
||||||
async def create_ssh_key(
|
async def create_ssh_key(
|
||||||
data: SSHKeyCreate,
|
data: SSHKeyCreate,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> SSHKey:
|
) -> SSHKey:
|
||||||
"""Create a new SSH key pair.
|
"""Create a new SSH key pair.
|
||||||
@@ -96,7 +89,6 @@ async def create_ssh_key(
|
|||||||
Returns:
|
Returns:
|
||||||
The newly created SSH key with public key exposed.
|
The newly created SSH key with public key exposed.
|
||||||
"""
|
"""
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
private_key, public_key = generate_ssh_key_pair()
|
private_key, public_key = generate_ssh_key_pair()
|
||||||
|
|
||||||
fernet = _get_fernet()
|
fernet = _get_fernet()
|
||||||
@@ -121,7 +113,7 @@ async def create_ssh_key(
|
|||||||
description="List all SSH keys for the authenticated user.",
|
description="List all SSH keys for the authenticated user.",
|
||||||
)
|
)
|
||||||
async def list_ssh_keys(
|
async def list_ssh_keys(
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> list[SSHKey]:
|
) -> list[SSHKey]:
|
||||||
"""List all SSH keys for the authenticated user.
|
"""List all SSH keys for the authenticated user.
|
||||||
@@ -133,7 +125,6 @@ async def list_ssh_keys(
|
|||||||
Returns:
|
Returns:
|
||||||
List of SSH keys owned by the user.
|
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))
|
result = await session.execute(select(SSHKey).where(SSHKey.user_id == user.id))
|
||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
|
|
||||||
@@ -146,7 +137,7 @@ async def list_ssh_keys(
|
|||||||
)
|
)
|
||||||
async def delete_ssh_key(
|
async def delete_ssh_key(
|
||||||
key_id: uuid.UUID,
|
key_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete an SSH key.
|
"""Delete an SSH key.
|
||||||
@@ -159,7 +150,6 @@ async def delete_ssh_key(
|
|||||||
Returns:
|
Returns:
|
||||||
None with 204 status code.
|
None with 204 status code.
|
||||||
"""
|
"""
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
ssh_key = await session.get(SSHKey, key_id)
|
ssh_key = await session.get(SSHKey, key_id)
|
||||||
if ssh_key is None or ssh_key.user_id != user.id:
|
if ssh_key is None or ssh_key.user_id != user.id:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,20 +7,13 @@ from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user, get_db_session
|
||||||
from src.models.tool_type import ToolType
|
from src.models.tool_type import ToolType
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
|
|
||||||
router = APIRouter(prefix="/tool-types", tags=["tool-types"])
|
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")
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
async def _require_admin(user: User) -> None:
|
async def _require_admin(user: User) -> None:
|
||||||
"""Check if user has admin privileges.
|
"""Check if user has admin privileges.
|
||||||
@@ -266,7 +259,7 @@ class ToolTypeResponse(BaseModel):
|
|||||||
)
|
)
|
||||||
async def create_tool_type(
|
async def create_tool_type(
|
||||||
data: ToolTypeCreate,
|
data: ToolTypeCreate,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> ToolType:
|
) -> ToolType:
|
||||||
"""Create a new tool type.
|
"""Create a new tool type.
|
||||||
@@ -279,7 +272,6 @@ async def create_tool_type(
|
|||||||
Returns:
|
Returns:
|
||||||
The newly created tool type.
|
The newly created tool type.
|
||||||
"""
|
"""
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
await _require_admin(user)
|
await _require_admin(user)
|
||||||
|
|
||||||
# Check for duplicate name
|
# Check for duplicate name
|
||||||
@@ -316,7 +308,7 @@ async def create_tool_type(
|
|||||||
description="List all available tool types including built-in and custom ones.",
|
description="List all available tool types including built-in and custom ones.",
|
||||||
)
|
)
|
||||||
async def list_tool_types(
|
async def list_tool_types(
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> list[ToolType]:
|
) -> list[ToolType]:
|
||||||
"""List all tool types.
|
"""List all tool types.
|
||||||
@@ -328,7 +320,6 @@ async def list_tool_types(
|
|||||||
Returns:
|
Returns:
|
||||||
List of all tool types ordered by name.
|
List of all tool types ordered by name.
|
||||||
"""
|
"""
|
||||||
await _get_user(session, user_id)
|
|
||||||
result = await session.execute(select(ToolType).order_by(ToolType.name))
|
result = await session.execute(select(ToolType).order_by(ToolType.name))
|
||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
|
|
||||||
@@ -341,7 +332,7 @@ async def list_tool_types(
|
|||||||
)
|
)
|
||||||
async def get_tool_type(
|
async def get_tool_type(
|
||||||
tool_type_id: uuid.UUID,
|
tool_type_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> ToolType:
|
) -> ToolType:
|
||||||
"""Get a specific tool type by ID.
|
"""Get a specific tool type by ID.
|
||||||
@@ -354,7 +345,6 @@ async def get_tool_type(
|
|||||||
Returns:
|
Returns:
|
||||||
The requested tool type.
|
The requested tool type.
|
||||||
"""
|
"""
|
||||||
await _get_user(session, user_id)
|
|
||||||
tool_type = await session.get(ToolType, tool_type_id)
|
tool_type = await session.get(ToolType, tool_type_id)
|
||||||
if tool_type is None:
|
if tool_type is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="tool type not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="tool type not found")
|
||||||
@@ -370,7 +360,7 @@ async def get_tool_type(
|
|||||||
async def update_tool_type(
|
async def update_tool_type(
|
||||||
tool_type_id: uuid.UUID,
|
tool_type_id: uuid.UUID,
|
||||||
data: ToolTypeUpdate,
|
data: ToolTypeUpdate,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> ToolType:
|
) -> ToolType:
|
||||||
"""Update a tool type.
|
"""Update a tool type.
|
||||||
@@ -384,7 +374,6 @@ async def update_tool_type(
|
|||||||
Returns:
|
Returns:
|
||||||
The updated tool type.
|
The updated tool type.
|
||||||
"""
|
"""
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
await _require_admin(user)
|
await _require_admin(user)
|
||||||
|
|
||||||
tool_type = await session.get(ToolType, tool_type_id)
|
tool_type = await session.get(ToolType, tool_type_id)
|
||||||
@@ -480,7 +469,7 @@ class ToolTypeValidateRequest(BaseModel):
|
|||||||
)
|
)
|
||||||
async def validate_tool_type_template(
|
async def validate_tool_type_template(
|
||||||
data: ToolTypeValidateRequest,
|
data: ToolTypeValidateRequest,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Validate a tool type template syntax.
|
"""Validate a tool type template syntax.
|
||||||
@@ -493,7 +482,6 @@ async def validate_tool_type_template(
|
|||||||
Returns:
|
Returns:
|
||||||
Validation result with success status and any errors.
|
Validation result with success status and any errors.
|
||||||
"""
|
"""
|
||||||
await _get_user(session, user_id)
|
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
@@ -534,7 +522,7 @@ async def validate_tool_type_template(
|
|||||||
)
|
)
|
||||||
async def validate_tool_type(
|
async def validate_tool_type(
|
||||||
tool_type_id: uuid.UUID,
|
tool_type_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Validate a tool type's template syntax.
|
"""Validate a tool type's template syntax.
|
||||||
@@ -547,7 +535,6 @@ async def validate_tool_type(
|
|||||||
Returns:
|
Returns:
|
||||||
Validation result with success status and any errors.
|
Validation result with success status and any errors.
|
||||||
"""
|
"""
|
||||||
await _get_user(session, user_id)
|
|
||||||
tool_type = await session.get(ToolType, tool_type_id)
|
tool_type = await session.get(ToolType, tool_type_id)
|
||||||
if tool_type is None:
|
if tool_type is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="tool type not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="tool type not found")
|
||||||
@@ -589,7 +576,7 @@ async def validate_tool_type(
|
|||||||
)
|
)
|
||||||
async def delete_tool_type(
|
async def delete_tool_type(
|
||||||
tool_type_id: uuid.UUID,
|
tool_type_id: uuid.UUID,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete a tool type.
|
"""Delete a tool type.
|
||||||
@@ -602,7 +589,6 @@ async def delete_tool_type(
|
|||||||
Returns:
|
Returns:
|
||||||
None with 204 status code.
|
None with 204 status code.
|
||||||
"""
|
"""
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
await _require_admin(user)
|
await _require_admin(user)
|
||||||
|
|
||||||
tool_type = await session.get(ToolType, tool_type_id)
|
tool_type = await session.get(ToolType, tool_type_id)
|
||||||
|
|||||||
@@ -8,20 +8,13 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user, get_db_session
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
from src.models.user_config import UserConfig
|
from src.models.user_config import UserConfig
|
||||||
|
|
||||||
router = APIRouter(prefix="/users/me", tags=["user-config"])
|
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")
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_or_create_config(session: AsyncSession, user_id: uuid.UUID) -> UserConfig:
|
async def _get_or_create_config(session: AsyncSession, user_id: uuid.UUID) -> UserConfig:
|
||||||
"""Get or create user config record.
|
"""Get or create user config record.
|
||||||
@@ -33,10 +26,10 @@ async def _get_or_create_config(session: AsyncSession, user_id: uuid.UUID) -> Us
|
|||||||
Returns:
|
Returns:
|
||||||
The user's config, creating a new one if it doesn't exist.
|
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))
|
result = await session.execute(select(UserConfig).where(UserConfig.user_id == user.id))
|
||||||
config = result.scalar_one_or_none()
|
config = result.scalar_one_or_none()
|
||||||
if config is None:
|
if config is None:
|
||||||
config = UserConfig(user_id=user_id, config={})
|
config = UserConfig(user_id=user.id, config={})
|
||||||
session.add(config)
|
session.add(config)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(config)
|
await session.refresh(config)
|
||||||
@@ -68,7 +61,7 @@ class UserConfigUpdate(BaseModel):
|
|||||||
description="Get the current user's configuration settings.",
|
description="Get the current user's configuration settings.",
|
||||||
)
|
)
|
||||||
async def get_user_config(
|
async def get_user_config(
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> UserConfigResponse:
|
) -> UserConfigResponse:
|
||||||
"""Get the current user's configuration.
|
"""Get the current user's configuration.
|
||||||
@@ -80,8 +73,7 @@ async def get_user_config(
|
|||||||
Returns:
|
Returns:
|
||||||
The user's configuration settings.
|
The user's configuration settings.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
config = await _get_or_create_config(session, user.id)
|
||||||
config = await _get_or_create_config(session, user_id)
|
|
||||||
return UserConfigResponse.model_validate(config.config)
|
return UserConfigResponse.model_validate(config.config)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +85,7 @@ async def get_user_config(
|
|||||||
)
|
)
|
||||||
async def update_user_config(
|
async def update_user_config(
|
||||||
data: UserConfigUpdate,
|
data: UserConfigUpdate,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> UserConfigResponse:
|
) -> UserConfigResponse:
|
||||||
"""Update the current user's configuration.
|
"""Update the current user's configuration.
|
||||||
@@ -106,12 +98,11 @@ async def update_user_config(
|
|||||||
Returns:
|
Returns:
|
||||||
The updated user configuration.
|
The updated user configuration.
|
||||||
"""
|
"""
|
||||||
_user = await _get_user(session, user_id)
|
config = await _get_or_create_config(session, user.id)
|
||||||
config = await _get_or_create_config(session, user_id)
|
|
||||||
|
|
||||||
# Merge updates
|
# Merge updates
|
||||||
update_data = data.model_dump(exclude_unset=True)
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
logger.info("Updating user config for user %s: %s", user_id, update_data)
|
logger.info("Updating user config for user %s: %s", user.id, update_data)
|
||||||
# SQLAlchemy JSON doesn't track dict mutations, so we replace the whole dict
|
# SQLAlchemy JSON doesn't track dict mutations, so we replace the whole dict
|
||||||
config.config = {**config.config, **update_data}
|
config.config = {**config.config, **update_data}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
|
|||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user, get_db_session
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
|
|
||||||
router = APIRouter(prefix="/users", tags=["users"])
|
router = APIRouter(prefix="/users", tags=["users"])
|
||||||
@@ -16,13 +16,6 @@ ALLOWED_CONTENT_TYPES = {"image/png", "image/jpeg", "image/jpg"}
|
|||||||
MAX_AVATAR_SIZE = 2 * 1024 * 1024 # 2MB
|
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")
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
class UserProfileResponse(BaseModel):
|
class UserProfileResponse(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@@ -45,7 +38,7 @@ class UserProfileUpdate(BaseModel):
|
|||||||
description="Retrieve the profile of the currently authenticated user.",
|
description="Retrieve the profile of the currently authenticated user.",
|
||||||
)
|
)
|
||||||
async def get_profile(
|
async def get_profile(
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> User:
|
) -> User:
|
||||||
"""Get the current user's profile.
|
"""Get the current user's profile.
|
||||||
@@ -57,7 +50,7 @@ async def get_profile(
|
|||||||
Returns:
|
Returns:
|
||||||
The user's profile information.
|
The user's profile information.
|
||||||
"""
|
"""
|
||||||
return await _get_user(session, user_id)
|
return user
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
@@ -68,7 +61,7 @@ async def get_profile(
|
|||||||
)
|
)
|
||||||
async def update_profile(
|
async def update_profile(
|
||||||
data: UserProfileUpdate,
|
data: UserProfileUpdate,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> User:
|
) -> User:
|
||||||
"""Update the current user's profile.
|
"""Update the current user's profile.
|
||||||
@@ -81,7 +74,6 @@ async def update_profile(
|
|||||||
Returns:
|
Returns:
|
||||||
The updated user profile.
|
The updated user profile.
|
||||||
"""
|
"""
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
|
|
||||||
if data.name is not None:
|
if data.name is not None:
|
||||||
if len(data.name.strip()) == 0:
|
if len(data.name.strip()) == 0:
|
||||||
@@ -106,7 +98,7 @@ async def update_profile(
|
|||||||
)
|
)
|
||||||
async def upload_avatar(
|
async def upload_avatar(
|
||||||
file: UploadFile,
|
file: UploadFile,
|
||||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db_session),
|
session: AsyncSession = Depends(get_db_session),
|
||||||
) -> User:
|
) -> User:
|
||||||
"""Upload a profile avatar image.
|
"""Upload a profile avatar image.
|
||||||
@@ -119,7 +111,6 @@ async def upload_avatar(
|
|||||||
Returns:
|
Returns:
|
||||||
The updated user profile with new avatar URL.
|
The updated user profile with new avatar URL.
|
||||||
"""
|
"""
|
||||||
user = await _get_user(session, user_id)
|
|
||||||
|
|
||||||
if file.content_type not in ALLOWED_CONTENT_TYPES:
|
if file.content_type not in ALLOWED_CONTENT_TYPES:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Task 3.1 Apply Report: Extract Shared Auth Dependencies
|
||||||
|
|
||||||
|
**Status:** Success
|
||||||
|
|
||||||
|
**Files Created (1):**
|
||||||
|
- `apps/api/src/auth/dependencies.py` — Added `get_owned_project()` dependency function
|
||||||
|
|
||||||
|
**Files Modified (7):**
|
||||||
|
- `apps/api/src/api/tool_instances.py` — Removed `_get_user` and `_get_owned_project` definitions; replaced with `get_current_user` and `get_owned_project` FastAPI dependencies
|
||||||
|
- `apps/api/src/api/git_repositories.py` — Same refactoring
|
||||||
|
- `apps/api/src/api/projects.py` — Same refactoring
|
||||||
|
- `apps/api/src/api/ssh_keys.py` — Removed `_get_user`; replaced with `get_current_user` dependency
|
||||||
|
- `apps/api/src/api/users.py` — Same as ssh_keys.py
|
||||||
|
- `apps/api/src/api/user_config.py` — Same as ssh_keys.py
|
||||||
|
- `apps/api/src/api/tool_types.py` — Same as ssh_keys.py
|
||||||
|
|
||||||
|
**Files NOT Modified (intentionally):**
|
||||||
|
- `api/config_profiles.py` — Has `_get_owned_profile` (domain-specific, not a generic auth dependency)
|
||||||
|
- `api/tool_configs.py` — No inline auth helpers to extract
|
||||||
|
- `api/config_folders.py` — No inline auth helpers to extract
|
||||||
|
- `api/terminal.py` — No inline auth helpers to extract; `_get_user_from_websocket` is websocket-specific
|
||||||
|
|
||||||
|
**Files Deleted:** None
|
||||||
|
|
||||||
|
**Quality Gate Results:**
|
||||||
|
- Python syntax check (`py_compile`) for all modified files: **PASS**
|
||||||
|
- `grep -rn "def _get_user" apps/api/src/api/`: **PASS** — Only `terminal.py` has `_get_user_from_websocket` (websocket-specific, not the duplicated helper)
|
||||||
|
- `grep -rn "def _get_owned_project" apps/api/src/api/`: **PASS** — Zero results
|
||||||
|
- `pytest`: Not available in environment (system Python, no venv), but all files compile cleanly
|
||||||
|
|
||||||
|
**Blockers/Deviations:**
|
||||||
|
- None. All duplicated auth helpers successfully extracted to `auth/dependencies.py`.
|
||||||
|
- The `_get_user_from_websocket` in `terminal.py` was intentionally left untouched as it serves a different purpose (WebSocket cookie parsing vs. HTTP dependency injection).
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- `get_current_user` already existed in `auth/dependencies.py`; it was leveraged directly
|
||||||
|
- `get_owned_project` was newly added as a FastAPI dependency that injects `Project` after verifying ownership
|
||||||
|
- All route handlers now use proper FastAPI dependency injection instead of inline async calls
|
||||||
|
- Variable naming changed from `user_id` (UUID) to `user` (User model) in route handlers, with `user.id` used where the UUID is needed
|
||||||
Reference in New Issue
Block a user