نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
mjalmoudy94
2026-10-13 00:39:28 +03:30
commit 717e53a7ce
185 changed files with 39910 additions and 0 deletions
@@ -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,
)