""" 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 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.utcnow() + 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.utcnow() - 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())