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

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ 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
+197
View File
@@ -0,0 +1,197 @@
"""FastAPI dependencies: authentication, tenant resolution and capability checks.
The single most important rule of this file: **every** organization-scoped
endpoint resolves its tenant through ``get_org_context``, which refuses to
return anything unless the caller has an active membership. There is no
"trust the org_id in the path" path.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from fastapi import Depends, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_session
from app.core.errors import AuthenticationError, NotFoundError, PermissionDeniedError
from app.core.security import decode_access_token
from app.domain.identity.models import Membership, Organization, Role, User
from app.infrastructure.repositories.identity import (
MembershipRepository,
OrganizationRepository,
UserRepository,
)
from app.interfaces.http.request_context import note_actor
if TYPE_CHECKING:
from app.application.services.billing_service import BillingService
from app.application.services.knowledge_service import KnowledgeService
from app.application.services.project_service import ProjectService
from app.application.services.sandbox_service import SandboxService
from app.application.services.workforce_service import WorkforceService
_bearer = HTTPBearer(auto_error=False)
@dataclass(slots=True)
class OrgContext:
"""Resolved tenant context for an organization-scoped request."""
organization: Organization
membership: Membership
user: User
@property
def org_id(self) -> str:
return self.organization.id
def require(self, capability: str) -> None:
if not self.membership.has(capability):
raise PermissionDeniedError("شما دسترسی لازم برای این عملیات را ندارید.")
async def get_current_user(
request: Request,
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer),
session: AsyncSession = Depends(get_session),
) -> User:
if credentials is None or not credentials.credentials:
raise AuthenticationError("برای این عملیات باید وارد شوید.")
payload = decode_access_token(credentials.credentials)
user_id = payload.get("sub")
if not user_id:
raise AuthenticationError("توکن نامعتبر است.")
user = await UserRepository(session).get(str(user_id))
if user is None or not user.is_active:
raise AuthenticationError("حساب کاربری یافت نشد یا غیرفعال است.")
# Record the verified actor so any later failure is attributable. Uses the
# id from the database, not the one in the token.
note_actor(request, user_id=user.id)
return user
async def get_optional_user(
request: Request,
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer),
session: AsyncSession = Depends(get_session),
) -> User | None:
if credentials is None or not credentials.credentials:
return None
try:
return await get_current_user(request, credentials, session)
except AuthenticationError:
return None
async def require_platform_owner(user: User = Depends(get_current_user)) -> User:
if not user.is_platform_owner:
raise PermissionDeniedError("این بخش فقط برای مالک سامانه قابل دسترسی است.")
return user
async def get_org_context(
request: Request,
org_id: str,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> OrgContext:
"""Resolve ``org_id`` into an authorized tenant context.
Returns 404 (not 403) when the caller has no membership, so the API does not
leak the existence of other organizations.
"""
organization = await OrganizationRepository(session).get(org_id)
if organization is None:
raise NotFoundError("سازمان یافت نشد.")
membership = await MembershipRepository(session).get(org_id, user.id)
if membership is None or not membership.is_active:
raise NotFoundError("سازمان یافت نشد.")
# The tenant is only known *after* the membership check passes, so this is
# the earliest point at which it is safe to attribute a failure to an org.
note_actor(request, organization_id=organization.id, user_id=user.id)
return OrgContext(organization=organization, membership=membership, user=user)
def require_capability(capability: str):
"""Dependency factory: ``Depends(require_capability(CAN_MANAGE_PROJECTS))``."""
async def _checker(context: OrgContext = Depends(get_org_context)) -> OrgContext:
context.require(capability)
return context
return _checker
def require_role_at_least(minimum: Role):
from app.domain.identity.models import ROLE_RANK
async def _checker(context: OrgContext = Depends(get_org_context)) -> OrgContext:
if ROLE_RANK[context.membership.role] < ROLE_RANK[minimum]:
raise PermissionDeniedError("سطح دسترسی شما برای این عملیات کافی نیست.")
return context
return _checker
def client_meta(request: Request) -> tuple[str, str]:
"""``(ip, user_agent)`` for the audit trail."""
forwarded = request.headers.get("x-forwarded-for", "")
ip = (
forwarded.split(",")[0].strip()
if forwarded
else (request.client.host if request.client else "")
)
return ip, request.headers.get("user-agent", "")[:255]
# The builders themselves live in ``app.application.container`` so that the
# standalone workers compose services exactly the way the API does. These
# functions are thin FastAPI dependency wrappers around them.
async def get_project_service(
session: AsyncSession = Depends(get_session),
) -> ProjectService:
"""Build the project/domain service with the configured adapters."""
from app.application.container import build_project_service
return build_project_service(session)
async def get_workforce_service(
session: AsyncSession = Depends(get_session),
) -> WorkforceService:
"""Build the workforce/chat/run service with the configured adapters."""
from app.application.container import build_workforce_service
return build_workforce_service(session)
async def get_billing_service(
session: AsyncSession = Depends(get_session),
) -> BillingService:
"""Build the billing service with the configured payment adapter."""
from app.application.container import build_billing_service
return build_billing_service(session)
async def get_knowledge_service(
session: AsyncSession = Depends(get_session),
) -> KnowledgeService:
"""Build the knowledge service with the configured vector adapter."""
from app.application.container import build_knowledge_service
return build_knowledge_service(session)
async def get_sandbox_service(
session: AsyncSession = Depends(get_session),
) -> SandboxService:
"""Build the sandbox gateway with the configured execution adapter."""
from app.application.container import build_sandbox_service
return build_sandbox_service(session)