test_task_crm/app/api/v1/auth.py

115 lines
4.2 KiB
Python

"""Authentication API endpoints and payloads."""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from app.api.deps import get_auth_service, get_user_repository
from app.core.security import password_hasher
from app.models.organization import Organization
from app.models.organization_member import OrganizationMember, OrganizationRole
from app.models.token import LoginRequest, RefreshRequest, TokenResponse
from app.models.user import UserCreate
from app.repositories.user_repo import UserRepository
from app.services.auth_service import AuthService, InvalidCredentialsError, InvalidRefreshTokenError
class RegisterRequest(BaseModel):
email: EmailStr
password: str
name: str
organization_name: str | None = None
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
async def register_user(
payload: RegisterRequest,
repo: UserRepository = Depends(get_user_repository),
auth_service: AuthService = Depends(get_auth_service),
) -> TokenResponse:
"""Register a new owner along with the first organization and return JWT."""
existing = await repo.get_by_email(payload.email)
if existing is not None:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="User already exists")
organization: Organization | None = None
if payload.organization_name:
existing_org = await repo.session.scalar(
select(Organization).where(Organization.name == payload.organization_name),
)
if existing_org is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Organization already exists",
)
organization = Organization(name=payload.organization_name)
repo.session.add(organization)
await repo.session.flush()
user_data = UserCreate(email=payload.email, password=payload.password, name=payload.name)
hashed_password = password_hasher.hash(payload.password)
try:
user = await repo.create(data=user_data, hashed_password=hashed_password)
if organization is not None:
membership = OrganizationMember(
organization_id=organization.id,
user_id=user.id,
role=OrganizationRole.OWNER,
)
repo.session.add(membership)
await repo.session.commit()
except IntegrityError as exc:
await repo.session.rollback()
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Organization or user already exists",
) from exc
await repo.session.refresh(user)
return auth_service.issue_tokens(user)
@router.post("/login", response_model=TokenResponse)
async def login(
credentials: LoginRequest,
service: AuthService = Depends(get_auth_service),
) -> TokenResponse:
"""Authenticate user credentials and issue a JWT."""
try:
user = await service.authenticate(credentials.email, credentials.password)
except InvalidCredentialsError as exc: # pragma: no cover - thin API
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
return service.issue_tokens(user)
@router.post("/token", response_model=TokenResponse)
async def login_for_access_token(
credentials: LoginRequest,
service: AuthService = Depends(get_auth_service),
) -> TokenResponse:
try:
user = await service.authenticate(credentials.email, credentials.password)
except InvalidCredentialsError as exc: # pragma: no cover - thin API
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
return service.issue_tokens(user)
@router.post("/refresh", response_model=TokenResponse)
async def refresh_tokens(
payload: RefreshRequest,
service: AuthService = Depends(get_auth_service),
) -> TokenResponse:
try:
return await service.refresh_tokens(payload.refresh_token)
except InvalidRefreshTokenError as exc:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc