""" 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/store-token", response_model=dict, summary="Store JWT token for Telegram user (called after binding confirmation)", ) async def telegram_store_token( chat_id: int, jwt_token: str, db: Session = Depends(get_db), ): """ Store JWT token in Redis after successful binding confirmation. **Flow:** 1. User confirms binding via /telegram/confirm 2. Frontend receives jwt_token 3. Frontend calls this endpoint to cache token in bot's Redis 4. Bot can now use token for API calls **Usage:** ``` POST /auth/telegram/store-token?chat_id=12345&jwt_token=eyJ... ``` """ import redis # Get Redis client from settings from app.core.config import settings redis_client = redis.from_url(settings.redis_url) # Validate JWT token structure try: jwt_manager.verify_token(jwt_token) except ValueError as e: raise HTTPException(status_code=400, detail=f"Invalid token: {e}") # Store JWT in Redis with 30-day TTL cache_key = f"chat_id:{chat_id}:jwt" redis_client.setex(cache_key, 86400 * 30, jwt_token) logger.info(f"JWT token stored for chat_id={chat_id}") return { "success": True, "message": "Token stored successfully", "chat_id": chat_id, } @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 (bot authentication). **Usage in Bot:** ```python # Get token for authenticated user 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( "/telegram/register", response_model=dict, summary="Create new user with Telegram binding", ) async def telegram_register( chat_id: int, username: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, db: Session = Depends(get_db), ): """ Quick registration for new Telegram user. **Flow:** 1. Bot calls this endpoint on /start 2. Creates new User with telegram_id 3. Returns JWT for immediate API access 4. User can update email/password later **Usage in Bot:** ```python result = api.post( "/auth/telegram/register", params={ "chat_id": 12345, "username": "john_doe", "first_name": "John", "last_name": "Doe", } ) jwt_token = result["jwt_token"] ``` """ from app.db.models.user import User # Check if user already exists existing = db.query(User).filter_by(telegram_id=chat_id).first() if existing: service = AuthService(db) result = await service.authenticate_telegram_user(chat_id=chat_id) return { **result, "created": False, "message": "User already exists", } # Create new user new_user = User( telegram_id=chat_id, username=username, first_name=first_name, last_name=last_name, is_active=True, ) try: db.add(new_user) db.commit() db.refresh(new_user) except Exception as e: db.rollback() logger.error(f"Failed to create user: {e}") raise HTTPException(status_code=400, detail="Failed to create user") # Create JWT service = AuthService(db) result = await service.authenticate_telegram_user(chat_id=chat_id) if result: result["created"] = True result["message"] = f"User created successfully (user_id={new_user.id})" logger.info(f"New Telegram user registered: chat_id={chat_id}, user_id={new_user.id}") return result or {"success": False, "error": "Failed to create user"} @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"}