""" Token Service - ServiceManagerWeb Servicio para gesti├│n de refresh tokens persistentes. """ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, delete from datetime import datetime, timedelta, timezone from typing import Optional import uuid import structlog from app.models.refresh_token import RefreshToken from app.models.user import User from app.core.config import get_settings logger = structlog.get_logger(__name__) settings = get_settings() class TokenService: """ Servicio para gesti├│n de refresh tokens. Proporciona m├®todos para crear, validar, revocar y limpiar refresh tokens persistentes. NOTA: Los tokens se almacenan directamente en BD (no hash) ya que los JWTs son firmados y verificables. """ @staticmethod async def create_refresh_token( db: AsyncSession, user: User, refresh_token: str, device_id: Optional[str] = None, device_name: Optional[str] = None, user_agent: Optional[str] = None, ip_address: Optional[str] = None ) -> RefreshToken: """ Crear y persistir un refresh token. Args: db: Sesi├│n de base de datos user: Usuario propietario del token refresh_token: Token JWT generado (se almacena directamente) device_id: ID ├║nico del dispositivo (UUID generado por cliente) device_name: Nombre del dispositivo (ej: "Chrome en Windows") user_agent: User agent completo del navegador ip_address: IP del cliente Returns: RefreshToken creado """ # Calcular expiraci├│n expires_at = datetime.now(timezone.utc) + timedelta( days=settings.REFRESH_TOKEN_EXPIRE_DAYS ) # Crear registro - almacena JWT directamente (columna UNIQUE) db_token = RefreshToken( user_id=user.id, token=refresh_token, # JWT almacenado directamente device_id=device_id, device_name=device_name, user_agent=user_agent, ip_address=ip_address, expires_at=expires_at, revoked=False, usage_count=0 ) db.add(db_token) await db.flush() logger.info( "Refresh token created", user_id=str(user.id), token_id=str(db_token.id), device_name=device_name, expires_at=expires_at.isoformat() ) return db_token @staticmethod async def verify_refresh_token( db: AsyncSession, refresh_token: str ) -> Optional[RefreshToken]: """ Verificar que el refresh token exista y sea v├ílido. Busca el JWT directamente en la BD y verifica su estado. Args: db: Sesi├│n de base de datos refresh_token: Token JWT a verificar Returns: RefreshToken si es v├ílido, None si no existe o est├í revocado/expirado """ # Buscar token directamente en BD (sin hash) query = select(RefreshToken).where( RefreshToken.token == refresh_token ) result = await db.execute(query) db_token = result.scalar_one_or_none() if not db_token: logger.warning("Refresh token not found in database") return None # Verificar si es v├ílido (usa property is_valid del modelo) if not db_token.is_valid: logger.warning( "Invalid refresh token", token_id=str(db_token.id), revoked=db_token.revoked, expired=db_token.is_expired ) return None # Actualizar estad├¡sticas de uso db_token.track_usage() await db.flush() logger.info( "Refresh token verified and usage tracked", token_id=str(db_token.id), usage_count=db_token.usage_count ) return db_token @staticmethod async def revoke_token( db: AsyncSession, refresh_token: str, revoked_by_user_id: Optional[uuid.UUID] = None ) -> bool: """ Revocar un refresh token espec├¡fico. Args: db: Sesi├│n de base de datos refresh_token: Token JWT a revocar revoked_by_user_id: ID del usuario que revoca (para auditor├¡a) Returns: True si se revoc├│, False si no se encontr├│ """ # Buscar token directamente (sin hash) query = select(RefreshToken).where( RefreshToken.token == refresh_token ) result = await db.execute(query) db_token = result.scalar_one_or_none() if not db_token: logger.warning("Refresh token not found for revocation") return False # Revocar usando m├®todo del modelo db_token.revoke(revoked_by=revoked_by_user_id) await db.flush() logger.info( "Refresh token revoked", token_id=str(db_token.id), revoked_by=str(revoked_by_user_id) if revoked_by_user_id else None ) return True @staticmethod async def revoke_all_user_tokens( db: AsyncSession, user_id: uuid.UUID, revoked_by_user_id: Optional[uuid.UUID] = None ) -> int: """ Revocar todos los tokens activos de un usuario. ├Ütil para logout en todos los dispositivos. Args: db: Sesi├│n de base de datos user_id: ID del usuario revoked_by_user_id: ID del usuario que ejecuta la revocaci├│n (para auditor├¡a) Returns: N├║mero de tokens revocados """ # Buscar todos los tokens activos del usuario query = select(RefreshToken).where( RefreshToken.user_id == user_id, RefreshToken.revoked == False ) result = await db.execute(query) tokens = result.scalars().all() count = 0 for token in tokens: token.revoke(revoked_by=revoked_by_user_id) count += 1 await db.flush() logger.info( "All user tokens revoked", user_id=str(user_id), count=count, revoked_by=str(revoked_by_user_id) if revoked_by_user_id else None ) return count @staticmethod async def cleanup_expired_tokens( db: AsyncSession ) -> int: """ Eliminar tokens expirados de la base de datos. Tarea de mantenimiento para limpiar tokens antiguos. Args: db: Sesi├│n de base de datos Returns: N├║mero de tokens eliminados """ # Eliminar tokens expirados hace m├ís de 7 d├¡as cutoff_date = datetime.now(timezone.utc) - timedelta(days=7) query = delete(RefreshToken).where( RefreshToken.expires_at < cutoff_date ) result = await db.execute(query) await db.flush() deleted_count = result.rowcount logger.info("Expired tokens cleaned up", count=deleted_count) return deleted_count @staticmethod async def get_user_tokens( db: AsyncSession, user_id: uuid.UUID ) -> list[RefreshToken]: """ Obtener todos los tokens activos de un usuario. Args: db: Sesi├│n de base de datos user_id: ID del usuario Returns: Lista de RefreshTokens activos """ query = select(RefreshToken).where( RefreshToken.user_id == user_id, RefreshToken.revoked == False, RefreshToken.expires_at > datetime.utcnow() ).order_by(RefreshToken.created_at.desc()) result = await db.execute(query) return list(result.scalars().all())