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

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ 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,243 @@
"""Project and preview-domain use cases.
This is the application layer: it orchestrates domain rules, repositories and the
driven ports (Git, domain router). It knows nothing about HTTP.
Security properties enforced here (not in the router, not only in the adapter):
* a project repository is always created **private**
* the repository name is collision-free across tenants
* the per-project Git password is encrypted before it is stored and never
returned to a caller
* a preview host must be a single label under the configured root domain
* a preview host is unique platform-wide
* the proxy target port must be allocatable and must not be a protected
platform port
* a project may hold at most ``max_subdomains_per_project`` hosts
"""
from __future__ import annotations
import hashlib
from app.application.ports import (
DomainRouterPort,
GitCredentials,
GitProviderPort,
RepoInfo,
RepoStatus,
)
from app.core.config import settings
from app.core.crypto import decrypt, encrypt
from app.core.errors import ConflictError, NotFoundError, PermissionDeniedError, ValidationError
from app.domain.base import ensure_unique_slug, slugify
from app.domain.platform.rules import (
assert_not_protected_port,
assert_port_allocatable,
build_host,
validate_subdomain_label,
)
from app.domain.projects.models import (
Project,
ProjectGitCredential,
ProjectGitRepo,
)
from app.infrastructure.repositories.projects import (
PreviewDomainRepository,
ProjectGitCredentialRepository,
ProjectGitRepoRepository,
ProjectRepository,
)
class ProjectService:
def __init__(
self,
*,
projects: ProjectRepository,
repos: ProjectGitRepoRepository,
credentials: ProjectGitCredentialRepository,
domains: PreviewDomainRepository,
git: GitProviderPort,
router: DomainRouterPort,
) -> None:
self.projects = projects
self.repos = repos
self.credentials = credentials
self.domains = domains
self.git = git
self.router = router
# -- projects ------------------------------------------------------------
async def create_project(
self,
*,
organization_id: str,
organization_slug: str,
created_by_user_id: str,
name: str,
description: str = "",
) -> Project:
slug = ensure_unique_slug(slugify(name), await self.projects.list_slugs(organization_id))
return await self.projects.create(
Project(
organization_id=organization_id,
name=name.strip(),
slug=slug,
description=description,
created_by_user_id=created_by_user_id,
)
)
async def get_or_404(self, *, organization_id: str, project_id: str) -> Project:
project = await self.projects.get(project_id)
# Returning 404 (not 403) keeps the existence of other tenants' projects
# out of the response.
if project is None or project.organization_id != organization_id:
raise NotFoundError("پروژه یافت نشد.")
return project
@staticmethod
def repo_name_for(*, organization_slug: str, project: Project) -> str:
"""``orgslug-projectslug-shortid`` — stable and collision-free."""
digest = hashlib.sha1(f"{organization_slug}:{project.id}".encode()).hexdigest()[:6]
raw = f"{organization_slug}-{project.slug}-{digest}"
return raw[:100].strip("-")
# -- git -----------------------------------------------------------------
async def ensure_repository(
self, *, organization_slug: str, project: Project
) -> ProjectGitRepo:
repo_name = self.repo_name_for(organization_slug=organization_slug, project=project)
info: RepoInfo = await self.git.ensure_project_repo(
project_id=project.id,
repo_name=repo_name,
description=f"مخزن پروژهٔ {project.name}",
)
credentials: GitCredentials = await self.git.ensure_credentials(
project_id=project.id, owner=info.owner, repo_name=info.repo_name
)
await self.credentials.upsert(
ProjectGitCredential(
project_id=project.id,
username=credentials.username,
# Encrypted at rest. The plaintext only exists inside this method.
encrypted_password=encrypt(credentials.password),
)
)
return await self.repos.upsert(
ProjectGitRepo(
project_id=project.id,
provider=settings.git_provider,
owner=info.owner,
repo_name=info.repo_name,
clone_url=info.clone_url,
web_url=info.web_url,
default_branch=info.default_branch,
)
)
async def repo_status(self, *, project: Project) -> RepoStatus:
record = await self.repos.get_for_project(project.id)
if record is None:
return RepoStatus(exists=False)
# Identity comes from our own record so the adapter stays stateless.
return await self.git.get_repo_status(
project_id=project.id, owner=record.owner, repo_name=record.repo_name
)
async def clone_credentials(self, *, project: Project) -> tuple[str, str]:
"""``(username, password)`` for a one-off clone.
Only the application layer may call this, and the router must never
serialise the result.
"""
record = await self.credentials.get_for_project(project.id)
if record is None:
raise NotFoundError("اطلاعات دسترسی مخزن ثبت نشده است.")
return record.username, decrypt(record.encrypted_password)
# -- preview domains -----------------------------------------------------
async def allocate_port(self, *, project: Project, service_name: str = "web") -> int:
existing = await self.domains.get_port(project.id, service_name)
if existing is not None:
return existing
start, end = settings.preview_port_start, settings.preview_port_end
used = await self.domains.list_used_ports(start=start, end=end)
for port in range(start, end + 1):
if port in used:
continue
try:
assert_port_allocatable(port, start=start, end=end)
except ValueError:
continue
await self.domains.add_port(
organization_id=project.organization_id,
project_id=project.id,
service_name=service_name,
port=port,
)
return port
raise ConflictError("پورت آزادی در بازهٔ تعیین‌شده وجود ندارد.")
async def create_preview_domain(
self,
*,
project: Project,
requested_subdomain: str,
created_by_user_id: str,
port: int | None = None,
) -> dict:
label = validate_subdomain_label(requested_subdomain)
host = build_host(label, settings.preview_root_domain)
if await self.domains.get_by_host(host) is not None:
raise ConflictError(f"دامنهٔ {host} قبلاً ثبت شده است.")
count = await self.domains.count_for_project(project.id)
if count >= settings.max_subdomains_per_project:
raise ConflictError(
f"هر پروژه حداکثر {settings.max_subdomains_per_project} دامنه می‌تواند داشته باشد."
)
target_port = port if port is not None else await self.allocate_port(project=project)
# A tenant must never be able to expose an internal platform service.
assert_not_protected_port(target_port)
result = await self.router.configure_reverse_proxy(host=host, port=target_port)
if not result.ok:
raise ValidationError(result.message or "پیکربندی دامنه ناموفق بود.")
record = await self.domains.create(
organization_id=project.organization_id,
project_id=project.id,
host=host,
port=target_port,
created_by_user_id=created_by_user_id,
)
record["url"] = result.url or f"https://{host}"
return record
async def remove_preview_domain(self, *, project: Project, domain_id: str) -> None:
record = await self.domains.get(domain_id)
if record is None or record["project_id"] != project.id:
raise NotFoundError("دامنه یافت نشد.")
if record["protected"]:
raise PermissionDeniedError(
"این دامنه محافظت‌شده است و فقط مالک سامانه می‌تواند آن را تغییر دهد."
)
await self.router.remove_preview_domain(domain_id=record["host"])
await self.domains.delete(domain_id)
async def list_preview_domains(self, *, project: Project) -> list[dict]:
records = await self.domains.list_for_project(project.id)
for record in records:
record["url"] = f"https://{record['host']}"
return records