""" Router autenticazione – login, refresh, logout, 2FA TOTP. Endpoint: POST /api/v1/auth/login → access + refresh token POST /api/v1/auth/refresh → rinnova token POST /api/v1/auth/logout → revoca refresh token GET /api/v1/auth/me → utente corrente POST /api/v1/auth/totp/setup → genera segreto TOTP + QR POST /api/v1/auth/totp/verify → verifica e attiva TOTP POST /api/v1/auth/totp/disable → disabilita TOTP POST /api/v1/auth/change-password → cambio password """ from typing import Annotated from fastapi import APIRouter, Depends, Request from slowapi import Limiter from slowapi.util import get_remote_address from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings from app.core.exceptions import InvalidCredentialsError from app.database import get_db from app.dependencies import CurrentUser, DB from app.schemas.auth import ( LoginRequest, PasswordChangeRequest, RefreshRequest, TOTPSetupResponse, TOTPStatusResponse, TOTPVerifyRequest, TokenResponse, ) from app.schemas.user import UserResponse from app.services.auth_service import AuthService settings = get_settings() router = APIRouter(prefix="/auth", tags=["Autenticazione"]) limiter = Limiter(key_func=get_remote_address) @router.post( "/login", response_model=TokenResponse, summary="Login con email e password", description="Autentica l'utente. Se 2FA è attivo, richiede anche il codice TOTP.", ) async def login( request: Request, body: LoginRequest, db: DB, ) -> TokenResponse: ip = request.client.host if request.client else None ua = request.headers.get("user-agent") service = AuthService(db) access_token, refresh_token = await service.login( email=body.email, password=body.password, totp_code=body.totp_code, ip_address=ip, user_agent=ua, ) return TokenResponse( access_token=access_token, refresh_token=refresh_token, expires_in=settings.access_token_expire_minutes * 60, ) @router.post( "/refresh", response_model=TokenResponse, summary="Rinnova access token", ) async def refresh_tokens( body: RefreshRequest, db: DB, ) -> TokenResponse: service = AuthService(db) access_token, refresh_token = await service.refresh_tokens(body.refresh_token) return TokenResponse( access_token=access_token, refresh_token=refresh_token, expires_in=settings.access_token_expire_minutes * 60, ) @router.post( "/logout", status_code=204, summary="Logout – revoca refresh token", ) async def logout( body: RefreshRequest, db: DB, ) -> None: service = AuthService(db) await service.logout(body.refresh_token) @router.get( "/me", response_model=UserResponse, summary="Utente corrente autenticato", ) async def me(current_user: CurrentUser) -> UserResponse: return UserResponse.model_validate(current_user) @router.post( "/totp/setup", response_model=TOTPSetupResponse, summary="Avvia setup 2FA TOTP", description="Genera segreto TOTP e QR code. Il 2FA viene attivato solo dopo la verifica.", ) async def totp_setup( current_user: CurrentUser, db: DB, ) -> TOTPSetupResponse: service = AuthService(db) data = await service.setup_totp(current_user) return TOTPSetupResponse(**data) @router.post( "/totp/verify", response_model=TOTPStatusResponse, summary="Verifica codice TOTP e attiva 2FA", ) async def totp_verify( body: TOTPVerifyRequest, current_user: CurrentUser, db: DB, ) -> TOTPStatusResponse: service = AuthService(db) await service.verify_and_enable_totp(current_user, body.totp_code) return TOTPStatusResponse(totp_enabled=True) @router.post( "/totp/disable", response_model=TOTPStatusResponse, summary="Disabilita 2FA TOTP", ) async def totp_disable( current_user: CurrentUser, db: DB, ) -> TOTPStatusResponse: service = AuthService(db) await service.disable_totp(current_user) return TOTPStatusResponse(totp_enabled=False) @router.post( "/change-password", status_code=204, summary="Cambio password utente corrente", ) async def change_password( body: PasswordChangeRequest, current_user: CurrentUser, db: DB, ) -> None: from app.core.security import verify_password, hash_password if not verify_password(body.current_password, current_user.password_hash): raise InvalidCredentialsError() current_user.password_hash = hash_password(body.new_password)