نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user