""" Authentication Service - User login, token management """ from datetime import datetime, timedelta from typing import Optional, Dict, Any 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()