نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
"""Project, repository and preview-domain endpoints.
|
||||
|
||||
All routes are nested under ``/organizations/{org_id}/projects`` so the tenant
|
||||
context is resolved by ``get_org_context`` before any handler body runs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request, status
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.services.audit_service import record_audit
|
||||
from app.application.services.project_service import ProjectService
|
||||
from app.core.database import get_session
|
||||
from app.core.errors import NotFoundError
|
||||
from app.domain.audit.models import AuditAction
|
||||
from app.domain.identity.models import CAN_MANAGE_PROJECTS
|
||||
from app.interfaces.http.deps import OrgContext, client_meta, get_org_context, get_project_service
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/organizations/{org_id}/projects", tags=["projects"]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ProjectOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
organization_id: str
|
||||
name: str
|
||||
slug: str
|
||||
description: str
|
||||
default_branch: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class CreateProjectRequest(BaseModel):
|
||||
name: str = Field(min_length=2, max_length=160)
|
||||
description: str = Field(default="", max_length=2000)
|
||||
|
||||
|
||||
class RepoOut(BaseModel):
|
||||
id: str
|
||||
provider: str
|
||||
owner: str
|
||||
repo_name: str
|
||||
clone_url: str
|
||||
web_url: str
|
||||
default_branch: str
|
||||
private: bool = True
|
||||
|
||||
|
||||
class RepoStatusOut(BaseModel):
|
||||
exists: bool
|
||||
private: bool
|
||||
default_branch: str
|
||||
branches: list[str]
|
||||
open_pull_requests: int
|
||||
|
||||
|
||||
class CreateBranchRequest(BaseModel):
|
||||
branch_name: str = Field(min_length=1, max_length=120)
|
||||
from_branch: str = Field(default="main", max_length=120)
|
||||
|
||||
|
||||
class BranchOut(BaseModel):
|
||||
name: str
|
||||
sha: str
|
||||
|
||||
|
||||
class OpenPullRequestRequest(BaseModel):
|
||||
head: str = Field(min_length=1, max_length=120)
|
||||
base: str = Field(default="main", max_length=120)
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
body: str = Field(default="", max_length=10_000)
|
||||
|
||||
|
||||
class PullRequestOut(BaseModel):
|
||||
number: int
|
||||
url: str
|
||||
state: str
|
||||
|
||||
|
||||
class PreviewDomainOut(BaseModel):
|
||||
id: str
|
||||
host: str
|
||||
port: int
|
||||
status: str
|
||||
protected: bool
|
||||
url: str
|
||||
|
||||
|
||||
class CreatePreviewDomainRequest(BaseModel):
|
||||
subdomain: str = Field(min_length=3, max_length=40)
|
||||
port: int | None = Field(default=None, ge=1, le=65535)
|
||||
|
||||
|
||||
class AllocatePortRequest(BaseModel):
|
||||
service_name: str = Field(default="web", max_length=80)
|
||||
|
||||
|
||||
class PortOut(BaseModel):
|
||||
port: int
|
||||
service_name: str
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Projects
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("", response_model=ProjectOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_project(
|
||||
payload: CreateProjectRequest,
|
||||
request: Request,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> ProjectOut:
|
||||
context.require(CAN_MANAGE_PROJECTS)
|
||||
|
||||
project = await service.create_project(
|
||||
organization_id=context.org_id,
|
||||
organization_slug=context.organization.slug,
|
||||
created_by_user_id=context.user.id,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
)
|
||||
|
||||
ip, ua = client_meta(request)
|
||||
await record_audit(
|
||||
session, action=AuditAction.PROJECT_CREATE, organization_id=context.org_id,
|
||||
actor_user_id=context.user.id, target_type="project", target_id=project.id,
|
||||
metadata={"name": project.name, "slug": project.slug}, ip=ip, user_agent=ua,
|
||||
)
|
||||
return ProjectOut(
|
||||
id=project.id, organization_id=project.organization_id, name=project.name,
|
||||
slug=project.slug, description=project.description,
|
||||
default_branch=project.default_branch, created_at=project.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[ProjectOut])
|
||||
async def list_projects(
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> list[ProjectOut]:
|
||||
projects = await service.projects.list_for_org(context.org_id, limit=limit, offset=offset)
|
||||
return [
|
||||
ProjectOut(
|
||||
id=p.id, organization_id=p.organization_id, name=p.name, slug=p.slug,
|
||||
description=p.description, default_branch=p.default_branch,
|
||||
created_at=p.created_at,
|
||||
)
|
||||
for p in projects
|
||||
]
|
||||
|
||||
|
||||
@router.get("/{project_id}", response_model=ProjectOut)
|
||||
async def get_project(
|
||||
project_id: str,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
) -> ProjectOut:
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
return ProjectOut(
|
||||
id=project.id, organization_id=project.organization_id, name=project.name,
|
||||
slug=project.slug, description=project.description,
|
||||
default_branch=project.default_branch, created_at=project.created_at,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repository
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/{project_id}/repo/ensure", response_model=RepoOut)
|
||||
async def ensure_repo(
|
||||
project_id: str,
|
||||
request: Request,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> RepoOut:
|
||||
context.require(CAN_MANAGE_PROJECTS)
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
|
||||
repo = await service.ensure_repository(
|
||||
organization_slug=context.organization.slug, project=project
|
||||
)
|
||||
|
||||
ip, ua = client_meta(request)
|
||||
await record_audit(
|
||||
session, action=AuditAction.REPO_CREATE, organization_id=context.org_id,
|
||||
actor_user_id=context.user.id, target_type="repository", target_id=repo.id,
|
||||
metadata={"repo_name": repo.repo_name, "provider": repo.provider},
|
||||
ip=ip, user_agent=ua,
|
||||
)
|
||||
return RepoOut(
|
||||
id=repo.id, provider=repo.provider, owner=repo.owner, repo_name=repo.repo_name,
|
||||
clone_url=repo.clone_url, web_url=repo.web_url,
|
||||
default_branch=repo.default_branch, private=True,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/repo", response_model=RepoOut)
|
||||
async def get_repo(
|
||||
project_id: str,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
) -> RepoOut:
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
repo = await service.repos.get_for_project(project.id)
|
||||
if repo is None:
|
||||
raise NotFoundError("مخزنی برای این پروژه ثبت نشده است.")
|
||||
# clone_url is a plain https URL; credentials are never included.
|
||||
return RepoOut(
|
||||
id=repo.id, provider=repo.provider, owner=repo.owner, repo_name=repo.repo_name,
|
||||
clone_url=repo.clone_url, web_url=repo.web_url,
|
||||
default_branch=repo.default_branch, private=True,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/repo/status", response_model=RepoStatusOut)
|
||||
async def get_repo_status(
|
||||
project_id: str,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
) -> RepoStatusOut:
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
status_result = await service.repo_status(project=project)
|
||||
return RepoStatusOut(
|
||||
exists=status_result.exists,
|
||||
private=status_result.private,
|
||||
default_branch=status_result.default_branch,
|
||||
branches=list(status_result.branches),
|
||||
open_pull_requests=status_result.open_pull_requests,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{project_id}/repo/branches", response_model=BranchOut)
|
||||
async def create_branch(
|
||||
project_id: str,
|
||||
payload: CreateBranchRequest,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
) -> BranchOut:
|
||||
context.require(CAN_MANAGE_PROJECTS)
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
branch = await service.git.create_branch(
|
||||
project_id=project.id, branch_name=payload.branch_name, from_branch=payload.from_branch
|
||||
)
|
||||
return BranchOut(name=branch.name, sha=branch.sha)
|
||||
|
||||
|
||||
@router.post("/{project_id}/repo/pull-requests", response_model=PullRequestOut)
|
||||
async def open_pull_request(
|
||||
project_id: str,
|
||||
payload: OpenPullRequestRequest,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
) -> PullRequestOut:
|
||||
context.require(CAN_MANAGE_PROJECTS)
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
pr = await service.git.open_pull_request(
|
||||
project_id=project.id, head=payload.head, base=payload.base,
|
||||
title=payload.title, body=payload.body,
|
||||
)
|
||||
return PullRequestOut(number=pr.number, url=pr.url, state=pr.state)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Preview domains
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/{project_id}/preview/ports", response_model=PortOut)
|
||||
async def allocate_port(
|
||||
project_id: str,
|
||||
payload: AllocatePortRequest,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
) -> PortOut:
|
||||
context.require(CAN_MANAGE_PROJECTS)
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
port = await service.allocate_port(project=project, service_name=payload.service_name)
|
||||
return PortOut(port=port, service_name=payload.service_name)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/preview/domains",
|
||||
response_model=PreviewDomainOut,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_preview_domain(
|
||||
project_id: str,
|
||||
payload: CreatePreviewDomainRequest,
|
||||
request: Request,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> PreviewDomainOut:
|
||||
context.require(CAN_MANAGE_PROJECTS)
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
|
||||
record = await service.create_preview_domain(
|
||||
project=project,
|
||||
requested_subdomain=payload.subdomain,
|
||||
created_by_user_id=context.user.id,
|
||||
port=payload.port,
|
||||
)
|
||||
|
||||
ip, ua = client_meta(request)
|
||||
await record_audit(
|
||||
session, action=AuditAction.DOMAIN_CREATE, organization_id=context.org_id,
|
||||
actor_user_id=context.user.id, target_type="preview_domain",
|
||||
target_id=record["id"],
|
||||
metadata={"host": record["host"], "port": record["port"]},
|
||||
ip=ip, user_agent=ua,
|
||||
)
|
||||
return PreviewDomainOut(
|
||||
id=record["id"], host=record["host"], port=record["port"],
|
||||
status=record["status"], protected=record["protected"], url=record["url"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/preview/domains", response_model=list[PreviewDomainOut])
|
||||
async def list_preview_domains(
|
||||
project_id: str,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
) -> list[PreviewDomainOut]:
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
records = await service.list_preview_domains(project=project)
|
||||
return [
|
||||
PreviewDomainOut(
|
||||
id=r["id"], host=r["host"], port=r["port"], status=r["status"],
|
||||
protected=r["protected"], url=r["url"],
|
||||
)
|
||||
for r in records
|
||||
]
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{project_id}/preview/domains/{domain_id}", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def delete_preview_domain(
|
||||
project_id: str,
|
||||
domain_id: str,
|
||||
request: Request,
|
||||
context: OrgContext = Depends(get_org_context),
|
||||
service: ProjectService = Depends(get_project_service),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
context.require(CAN_MANAGE_PROJECTS)
|
||||
project = await service.get_or_404(organization_id=context.org_id, project_id=project_id)
|
||||
await service.remove_preview_domain(project=project, domain_id=domain_id)
|
||||
|
||||
ip, ua = client_meta(request)
|
||||
await record_audit(
|
||||
session, action=AuditAction.DOMAIN_DELETE, organization_id=context.org_id,
|
||||
actor_user_id=context.user.id, target_type="preview_domain", target_id=domain_id,
|
||||
ip=ip, user_agent=ua,
|
||||
)
|
||||
Reference in New Issue
Block a user