""" Authentication API Endpoints - Login, Token Management, Telegram Binding """ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel, EmailStr from typing import Optional from sqlalchemy.orm import Session from app.db.database import get_db from app.services.auth_service import AuthService from app.security.jwt_manager import jwt_manager import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/auth", tags=["authentication"]) # Request/Response Models class LoginRequest(BaseModel): email: EmailStr password: str class LoginResponse(BaseModel): access_token: str refresh_token: str user_id: int expires_in: int # seconds class TelegramBindingStartRequest(BaseModel): chat_id: int class TelegramBindingStartResponse(BaseModel): code: str expires_in: int # seconds class TelegramBindingConfirmRequest(BaseModel): code: str chat_id: int username: Optional[str] = None first_name: Optional[str] = None last_name: Optional[str] = None class TelegramBindingConfirmResponse(BaseModel): success: bool user_id: int jwt_token: str expires_at: str class TokenRefreshRequest(BaseModel): refresh_token: str class TokenRefreshResponse(BaseModel): access_token: str expires_in: int @router.post( "/login", response_model=LoginResponse, summary="User login with email & password", ) async def login( request: LoginRequest, db: Session = Depends(get_db), ) -> LoginResponse: """ Authenticate user and create session. **Returns:** - access_token: Short-lived JWT (15 min) - refresh_token: Long-lived refresh token (30 days) **Usage:** ``` Authorization: Bearer X-Device-Id: device_uuid # For tracking ``` """ # TODO: Verify email + password # For MVP: Assume credentials are valid from app.db.models import User user = db.query(User).filter(User.email == request.email).first() if not user: raise HTTPException(status_code=401, detail="Invalid credentials") service = AuthService(db) access_token, refresh_token = await service.create_session( user_id=user.id, device_id=request.__dict__.get("device_id"), ) return LoginResponse( access_token=access_token, refresh_token=refresh_token, user_id=user.id, expires_in=15 * 60, # 15 minutes ) @router.post( "/refresh", response_model=TokenRefreshResponse, summary="Refresh access token", ) async def refresh_token( request: TokenRefreshRequest, db: Session = Depends(get_db), ) -> TokenRefreshResponse: """ Issue new access token using refresh token. **Flow:** 1. Access token expires 2. Send refresh_token to this endpoint 3. Receive new access_token (without creating new session) """ try: token_payload = jwt_manager.verify_token(request.refresh_token) if token_payload.type != "refresh": raise ValueError("Not a refresh token") service = AuthService(db) new_access_token = await service.refresh_access_token( refresh_token=request.refresh_token, user_id=token_payload.sub, ) return TokenRefreshResponse( access_token=new_access_token, expires_in=15 * 60, ) except ValueError as e: raise HTTPException(status_code=401, detail=str(e)) @router.post( "/telegram/start", response_model=TelegramBindingStartResponse, summary="Start Telegram binding flow", ) async def telegram_binding_start( request: TelegramBindingStartRequest, db: Session = Depends(get_db), ): """ Generate binding code for Telegram user. **Bot Flow:** 1. User sends /start 2. Bot calls this endpoint: POST /auth/telegram/start 3. Bot receives code and generates link 4. Bot sends message with link to user 5. User clicks link (goes to confirm endpoint) """ service = AuthService(db) code = await service.create_telegram_binding_code(chat_id=request.chat_id) return TelegramBindingStartResponse( code=code, expires_in=600, # 10 minutes ) @router.post( "/telegram/confirm", response_model=TelegramBindingConfirmResponse, summary="Confirm Telegram binding", ) async def telegram_binding_confirm( request: TelegramBindingConfirmRequest, current_request: Request, db: Session = Depends(get_db), ): """ Confirm Telegram binding and issue JWT. **Flow:** 1. User logs in or creates account 2. User clicks binding link with code 3. Frontend calls this endpoint with code + user context 4. Backend creates TelegramIdentity record 5. Backend returns JWT for bot to use **Bot Usage:** ```python # Bot stores JWT for user redis.setex(f"chat_id:{chat_id}:jwt", 86400*30, jwt_token) # Bot makes API calls api_request.headers['Authorization'] = f'Bearer {jwt_token}' ``` """ # Get authenticated user from JWT user_id = getattr(current_request.state, "user_id", None) if not user_id: raise HTTPException(status_code=401, detail="User must be authenticated") service = AuthService(db) result = await service.confirm_telegram_binding( user_id=user_id, chat_id=request.chat_id, code=request.code, username=request.username, first_name=request.first_name, last_name=request.last_name, ) if not result.get("success"): raise HTTPException(status_code=400, detail="Binding failed") return TelegramBindingConfirmResponse(**result) @router.post( "/telegram/authenticate", response_model=dict, summary="Authenticate by Telegram chat_id", ) async def telegram_authenticate( chat_id: int, db: Session = Depends(get_db), ): """ Get JWT token for Telegram user. **Usage in Bot:** ```python # After user binding is confirmed response = api.post("/auth/telegram/authenticate?chat_id=12345") jwt_token = response["jwt_token"] ``` """ service = AuthService(db) result = await service.authenticate_telegram_user(chat_id=chat_id) if not result: raise HTTPException(status_code=404, detail="Telegram identity not found") return result @router.post( "/logout", summary="Logout user", ) async def logout( request: Request, db: Session = Depends(get_db), ): """ Revoke session and blacklist tokens. **TODO:** Implement token blacklisting in Redis """ user_id = getattr(request.state, "user_id", None) if not user_id: raise HTTPException(status_code=401, detail="Not authenticated") # TODO: Add token to Redis blacklist # redis.setex(f"blacklist:{token}", token_expiry_time, "1") return {"message": "Logged out successfully"}