"""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