""" Authentication Service - User login, Telegram binding, Token management """ from datetime import datetime, timedelta from typing import Optional, Dict, Any, Tuple import secrets from sqlalchemy.orm import Session from app.db.models import User from app.security.jwt_manager import jwt_manager import logging logger = logging.getLogger(__name__) class AuthService: """ Handles user authentication, token management, and Telegram binding. """ # Configuration TELEGRAM_BINDING_CODE_TTL = 600 # 10 minutes BINDING_CODE_LENGTH = 24 def __init__(self, db: Session): self.db = db async def create_telegram_binding_code(self, chat_id: int) -> str: """ Generate temporary code for Telegram user binding. Flow: 1. User sends /start to bot 2. Bot generates binding code 3. Bot sends user a link: https://app.com/auth/telegram?code=ABC123 4. User clicks link (authenticated or creates account) 5. Code is confirmed, JWT issued Returns: Binding code (24-char random) """ # Generate code code = secrets.token_urlsafe(TELEGRAM_BINDING_CODE_LENGTH) # Store in Redis (would be: redis.setex(f"telegram:code:{code}", TTL, chat_id)) # For MVP: Store in memory or DB with expiry logger.info(f"Generated Telegram binding code for chat_id={chat_id}") return code async def confirm_telegram_binding( self, user_id: int, chat_id: int, code: str, username: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, ) -> Dict[str, Any]: """ Confirm Telegram binding and create identity. Returns: { "success": true, "user_id": 123, "chat_id": 12345, "jwt_token": "eyJhbGc...", "expires_at": "2024-01-09T12:30:00Z" } """ # Verify code (would check Redis) # For MVP: Assume code is valid # Create or update Telegram identity identity = self.db.query(TelegramIdentity).filter( TelegramIdentity.user_id == user_id ).first() if identity: # Update existing identity.chat_id = chat_id identity.username = username identity.first_name = first_name identity.last_name = last_name identity.verified_at = datetime.utcnow() identity.updated_at = datetime.utcnow() else: # Create new identity = TelegramIdentity( user_id=user_id, chat_id=chat_id, username=username, first_name=first_name, last_name=last_name, verified_at=datetime.utcnow(), created_at=datetime.utcnow(), updated_at=datetime.utcnow(), ) self.db.add(identity) # Generate JWT token for bot jwt_token = jwt_manager.create_access_token( user_id=user_id, family_ids=[], # Will be loaded from user's families ) self.db.commit() logger.info(f"Telegram binding confirmed: user_id={user_id}, chat_id={chat_id}") return { "success": True, "user_id": user_id, "chat_id": chat_id, "jwt_token": jwt_token, "expires_at": (datetime.utcnow() + timedelta(minutes=15)).isoformat() + "Z", } async def authenticate_telegram_user( self, chat_id: int, ) -> Optional[Dict[str, Any]]: """ Authenticate user by Telegram chat_id. Returns: { "user_id": 123, "chat_id": 12345, "jwt_token": "...", } Or None if not found """ # Find Telegram identity identity = self.db.query(TelegramIdentity).filter( TelegramIdentity.chat_id == chat_id, TelegramIdentity.verified_at.isnot(None), ).first() if not identity: return None # Generate JWT jwt_token = jwt_manager.create_access_token( user_id=identity.user_id, ) return { "user_id": identity.user_id, "chat_id": chat_id, "jwt_token": jwt_token, } async def create_session( self, user_id: int, device_id: Optional[str] = None, ip_address: Optional[str] = None, user_agent: Optional[str] = None, ) -> Tuple[str, str]: """ Create new session with refresh token. Returns: (access_token, refresh_token) """ # Create access token access_token = jwt_manager.create_access_token(user_id=user_id, device_id=device_id) # Create refresh token refresh_token = jwt_manager.create_refresh_token(user_id=user_id, device_id=device_id) # Store session in DB session = DBSession( user_id=user_id, refresh_token_hash=self._hash_token(refresh_token), device_id=device_id, ip_address=ip_address, user_agent=user_agent, expires_at=datetime.utcnow() + timedelta(days=30), created_at=datetime.utcnow(), ) self.db.add(session) self.db.commit() return access_token, refresh_token async def refresh_access_token( self, refresh_token: str, user_id: int, ) -> str: """Issue new access token using refresh token""" # Verify refresh token try: token_payload = jwt_manager.verify_token(refresh_token) if token_payload.type != "refresh": raise ValueError("Not a refresh token") except ValueError: raise ValueError("Invalid refresh token") # Create new access token new_access_token = jwt_manager.create_access_token(user_id=user_id) return new_access_token @staticmethod def _hash_token(token: str) -> str: """Hash token for storage""" import hashlib return hashlib.sha256(token.encode()).hexdigest()