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

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ 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,182 @@
"""Authentication endpoints."""
from __future__ import annotations
from fastapi import APIRouter, Depends, Request, status
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.services.audit_service import record_audit
from app.core.database import get_session
from app.core.errors import AuthenticationError, ConflictError
from app.core.security import create_access_token, hash_password, verify_password
from app.domain.audit.models import AuditAction
from app.domain.identity.models import User
from app.infrastructure.repositories.identity import (
MembershipRepository,
OrganizationRepository,
UserRepository,
)
from app.interfaces.http.deps import client_meta, get_current_user
router = APIRouter(prefix="/auth", tags=["auth"])
class SignupRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=8, max_length=128)
name: str = Field(default="", max_length=160)
class LoginRequest(BaseModel):
email: EmailStr
password: str
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
email: str
name: str
avatar_url: str | None = None
is_platform_owner: bool = False
class OrganizationSummary(BaseModel):
id: str
name: str
slug: str
role: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
user: UserOut
class MeResponse(BaseModel):
user: UserOut
organizations: list[OrganizationSummary]
def _to_user_out(user: User) -> UserOut:
return UserOut(
id=user.id,
email=user.email,
name=user.display_name,
avatar_url=user.avatar_url,
is_platform_owner=user.is_platform_owner,
)
@router.post("/signup", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
async def signup(
payload: SignupRequest,
request: Request,
session: AsyncSession = Depends(get_session),
) -> TokenResponse:
users = UserRepository(session)
if await users.get_by_email(payload.email) is not None:
raise ConflictError("این ایمیل قبلاً ثبت شده است.")
user = await users.create(
User(
email=payload.email,
password_hash=hash_password(payload.password),
name=payload.name.strip(),
)
)
ip, ua = client_meta(request)
await record_audit(
session,
action=AuditAction.SIGNUP,
actor_user_id=user.id,
target_type="user",
target_id=user.id,
ip=ip,
user_agent=ua,
)
return TokenResponse(access_token=create_access_token(user.id), user=_to_user_out(user))
@router.post("/login", response_model=TokenResponse)
async def login(
payload: LoginRequest,
request: Request,
session: AsyncSession = Depends(get_session),
) -> TokenResponse:
user = await UserRepository(session).get_by_email(payload.email)
if user is None or not verify_password(payload.password, user.password_hash):
raise AuthenticationError("ایمیل یا رمز عبور نادرست است.")
if not user.is_active:
raise AuthenticationError("حساب کاربری غیرفعال است.")
ip, ua = client_meta(request)
await record_audit(
session,
action=AuditAction.LOGIN,
actor_user_id=user.id,
target_type="user",
target_id=user.id,
ip=ip,
user_agent=ua,
)
return TokenResponse(access_token=create_access_token(user.id), user=_to_user_out(user))
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout(
request: Request,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> None:
"""JWTs are stateless; logout is an audit marker today and a token-revocation
hook once a denylist (Redis) is wired in."""
ip, ua = client_meta(request)
await record_audit(
session,
action=AuditAction.LOGOUT,
actor_user_id=user.id,
target_type="user",
target_id=user.id,
ip=ip,
user_agent=ua,
)
@router.get("/me", response_model=MeResponse)
async def me(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> MeResponse:
orgs = await OrganizationRepository(session).list_for_user(user.id)
return MeResponse(
user=_to_user_out(user),
organizations=[
OrganizationSummary(id=org.id, name=org.name, slug=org.slug, role=role.value)
for org, role in orgs
],
)
@router.get("/me/memberships")
async def my_memberships(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> list[dict]:
orgs = await OrganizationRepository(session).list_for_user(user.id)
memberships = MembershipRepository(session)
out: list[dict] = []
for org, role in orgs:
membership = await memberships.get(org.id, user.id)
out.append(
{
"organization_id": org.id,
"organization_name": org.name,
"role": role.value,
"status": membership.status.value if membership else "unknown",
}
)
return out